@embedpdf/plugin-stage 3.0.0-next.1 → 3.0.0-next.11
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/contract-BU33R4oL.cjs +13 -0
- package/dist/contract-BU33R4oL.cjs.map +1 -0
- package/dist/contract-CPSX8ANN.js +8 -0
- package/dist/contract-CPSX8ANN.js.map +1 -0
- package/dist/contract.cjs +10 -0
- package/dist/contract.d.cts +3 -0
- package/dist/contract.d.ts +3 -0
- package/dist/contract.js +2 -0
- package/dist/destination.cjs +147 -0
- package/dist/destination.cjs.map +1 -0
- package/dist/destination.d.cts +19 -0
- package/dist/destination.d.ts +19 -0
- package/dist/destination.js +146 -0
- package/dist/destination.js.map +1 -0
- package/dist/index.cjs +706 -245
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -556
- package/dist/index.d.ts +57 -556
- package/dist/index.js +703 -246
- package/dist/index.js.map +1 -1
- package/dist/types-CdIFKQnj.d.ts +656 -0
- package/dist/types-qdO_v-V-.d.cts +656 -0
- package/package.json +16 -6
package/dist/index.js
CHANGED
|
@@ -1,8 +1,155 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { destinationToReveal } from "./destination.js";
|
|
2
|
+
import { n as StageToken, t as ZoomMode } from "./contract-CPSX8ANN.js";
|
|
3
|
+
import { definePlugin } from "@embedpdf/core";
|
|
4
|
+
import { ActionsToken } from "@embedpdf/plugin-actions/contract";
|
|
5
|
+
import { ActionsToken as ActionsToken$1 } from "@embedpdf/plugin-actions/contract/host";
|
|
3
6
|
import * as S from "@embedpdf/core-stage";
|
|
4
|
-
import { ZoomMode
|
|
5
|
-
import { addRotations, applyPoint, applyRect, displaySize, intersectRects,
|
|
7
|
+
import { ZoomMode as ZoomMode$1 } from "@embedpdf/core-stage";
|
|
8
|
+
import { addRotations, applyPoint, applyRect, displaySize, intersectRects, pageTransform, rotateScaleMatrix, snapToDevice } from "@embedpdf/core-geometry";
|
|
9
|
+
//#region src/motion.ts
|
|
10
|
+
/**
|
|
11
|
+
* The camera's MOTION vocabulary — every formula, pure.
|
|
12
|
+
*
|
|
13
|
+
* The capability owns the impure drivers (scheduler frames, dispatch, gesture
|
|
14
|
+
* state); the LAWS those drivers integrate live here, parameterized by dt/t
|
|
15
|
+
* with no state, no scheduler, no dispatch — unit-testable in isolation, and
|
|
16
|
+
* with zero plugin dependencies, liftable into stage-core if the native port
|
|
17
|
+
* ever wants them.
|
|
18
|
+
*
|
|
19
|
+
* Two families:
|
|
20
|
+
* • ballistic — one per-axis law for every free motion: `glideStep` (the
|
|
21
|
+
* touch fling, decaying on UIScrollView's curve) and `springStep` (a
|
|
22
|
+
* critically-damped return to the clamp). A glide that reaches a content
|
|
23
|
+
* edge converts to a spring with its velocity carried in, so the bounce
|
|
24
|
+
* grows out of physics rather than a scripted overshoot.
|
|
25
|
+
* • resistance — the iOS rubber-band curve `rubberOut` and its exact
|
|
26
|
+
* inverse `rubberIn`: dragging `d` px past an edge displays an
|
|
27
|
+
* asymptotically-bounded stretch, and re-entering a gesture over a
|
|
28
|
+
* stretched camera (the mid-bounce catch) reconstructs the
|
|
29
|
+
* finger-integrated position the curve came from.
|
|
30
|
+
* Plus the tween interpolators: `easeOutCubic` for coordinate tweens and
|
|
31
|
+
* `zoomLerp` — GEOMETRIC zoom interpolation (zoom is multiplicative; equal
|
|
32
|
+
* ratios per unit time, not equal deltas — the linear lerp's visible "dip").
|
|
33
|
+
*/
|
|
34
|
+
/** Resistance coefficient of the rubber-band curve (the iOS feel). */
|
|
35
|
+
const RUBBER = .55;
|
|
36
|
+
/** Glide decay per ms — UIScrollView's normal deceleration rate. */
|
|
37
|
+
const FLING_DECAY = .998;
|
|
38
|
+
/** rad/ms — critically damped, ~400ms to settle. */
|
|
39
|
+
const SPRING_OMEGA = .015;
|
|
40
|
+
/**
|
|
41
|
+
* Overshoot distance → displayed stretch: asymptotic to the viewport
|
|
42
|
+
* dimension `dim`, so the content never leaves the screen however far the
|
|
43
|
+
* finger travels. `rubberOut(0) === 0` — inside the bounds the curve is
|
|
44
|
+
* invisible.
|
|
45
|
+
*/
|
|
46
|
+
const rubberOut = (d, dim) => (1 - 1 / (d * RUBBER / dim + 1)) * dim;
|
|
47
|
+
/** Exact inverse of `rubberOut` (defined for `out < dim`). */
|
|
48
|
+
const rubberIn = (out, dim) => out * dim / (RUBBER * (dim - out));
|
|
49
|
+
/**
|
|
50
|
+
* One frame of free glide: exponential velocity decay, position advanced by
|
|
51
|
+
* the midpoint integral of the decay over `dt` (exact for the exponential —
|
|
52
|
+
* frame-rate independent).
|
|
53
|
+
*/
|
|
54
|
+
const glideStep = (p, v, dt) => {
|
|
55
|
+
const k = Math.pow(FLING_DECAY, dt);
|
|
56
|
+
const np = p + v * (dt * (1 + k) / 2);
|
|
57
|
+
const nv = v * k;
|
|
58
|
+
return Math.abs(nv) < .02 ? {
|
|
59
|
+
p: np,
|
|
60
|
+
v: 0,
|
|
61
|
+
done: true
|
|
62
|
+
} : {
|
|
63
|
+
p: np,
|
|
64
|
+
v: nv,
|
|
65
|
+
done: false
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* One frame of critically-damped spring toward `edge` — semi-implicit Euler
|
|
70
|
+
* on x'' = −ω²(x−e) − 2ωx'. Lands exactly ON the edge (position snapped,
|
|
71
|
+
* velocity zeroed) once inside half a pixel at negligible speed.
|
|
72
|
+
*/
|
|
73
|
+
const springStep = (p, v, edge, dt) => {
|
|
74
|
+
const nv = v + (-.015 * SPRING_OMEGA * (p - edge) - 2 * SPRING_OMEGA * v) * dt;
|
|
75
|
+
const np = p + nv * dt;
|
|
76
|
+
if (Math.abs(np - edge) < .5 && Math.abs(nv) < .01) return {
|
|
77
|
+
p: edge,
|
|
78
|
+
v: 0,
|
|
79
|
+
done: true
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
p: np,
|
|
83
|
+
v: nv,
|
|
84
|
+
done: false
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
|
|
88
|
+
/**
|
|
89
|
+
* Geometric zoom interpolation: `z0·(z1/z0)^k`. Zoom is multiplicative, so a
|
|
90
|
+
* constant-RATE zoom moves equal ratios per unit time; interpolating the
|
|
91
|
+
* level linearly instead makes the start rush and the end crawl (and, under
|
|
92
|
+
* a focal anchor, swings the anchored point along a curved path).
|
|
93
|
+
*/
|
|
94
|
+
const zoomLerp = (z0, z1, k) => z0 * Math.pow(z1 / z0, k);
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/responsive.ts
|
|
97
|
+
/**
|
|
98
|
+
* Container queries for the settings bag — the pure half. The capability owns
|
|
99
|
+
* the driver (when to re-resolve, how to react); the LAWS live here: what a
|
|
100
|
+
* box is, when a query matches, and what the effective settings are. No state,
|
|
101
|
+
* no dispatch — property-testable in isolation (the `wheel.ts` convention).
|
|
102
|
+
*
|
|
103
|
+
* The query vocabulary is deliberately just the box: a headless, DOM-free
|
|
104
|
+
* stage knows exactly one environmental fact — the viewport it was told.
|
|
105
|
+
* Anything else (pointer coarseness, platform, app state) is either per-event
|
|
106
|
+
* (`PointerSample.pointerType`) or the app's own business via `update()`.
|
|
107
|
+
*/
|
|
108
|
+
/** The reported viewport as a queryable box. A square box is 'portrait' (CSS). */
|
|
109
|
+
const boxOf = (vp) => ({
|
|
110
|
+
width: vp.width,
|
|
111
|
+
height: vp.height,
|
|
112
|
+
orientation: vp.height >= vp.width ? "portrait" : "landscape"
|
|
113
|
+
});
|
|
114
|
+
/** All bounds inclusive, all fields optional, conditions AND-ed (CSS ranges). */
|
|
115
|
+
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);
|
|
116
|
+
const ruleMatches = (rule, box) => typeof rule.when === "function" ? !!rule.when(box) : matchesQuery(rule.when, box);
|
|
117
|
+
/** Merge a patch over settings, skipping undefined values (Partial semantics). */
|
|
118
|
+
const mergeSettings = (into, patch) => {
|
|
119
|
+
const out = { ...into };
|
|
120
|
+
let key;
|
|
121
|
+
for (key in patch) {
|
|
122
|
+
const value = patch[key];
|
|
123
|
+
if (value !== void 0) Object.assign(out, { [key]: value });
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Setting-value equality: primitives by identity, the flat objects the
|
|
129
|
+
* settings vocabulary uses (`{ px }`, `{ x, y }`, page frames, zoom specs) by
|
|
130
|
+
* one level of own-key comparison. Settings values are never nested deeper.
|
|
131
|
+
*/
|
|
132
|
+
const eqSetting = (a, b) => {
|
|
133
|
+
if (a === b) return true;
|
|
134
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
135
|
+
const ka = Object.keys(a);
|
|
136
|
+
const kb = Object.keys(b);
|
|
137
|
+
return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
|
|
138
|
+
};
|
|
139
|
+
const resolveResponsive = (base, rules, box) => {
|
|
140
|
+
let effective = base;
|
|
141
|
+
const active = [];
|
|
142
|
+
for (const rule of rules) {
|
|
143
|
+
if (!ruleMatches(rule, box)) continue;
|
|
144
|
+
if (rule.name !== void 0) active.push(rule.name);
|
|
145
|
+
if (rule.settings) effective = mergeSettings(effective, rule.settings);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
effective,
|
|
149
|
+
active
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
//#endregion
|
|
6
153
|
//#region src/settings.ts
|
|
7
154
|
/**
|
|
8
155
|
* Out-of-the-box defaults — a sensible document-reading feel. They are JUST
|
|
@@ -48,6 +195,18 @@ const DEFAULT_SETTINGS = {
|
|
|
48
195
|
scrollBehavior: "smooth",
|
|
49
196
|
viewUnitsPerPoint: 96 / 72
|
|
50
197
|
};
|
|
198
|
+
/**
|
|
199
|
+
* Out-of-the-box responsive rules — the platform feel with zero configuration:
|
|
200
|
+
* a compact container gets the thin gutter phones use (space, not device — an
|
|
201
|
+
* embedded 500px pane on a desktop is compact too). Override the whole list
|
|
202
|
+
* with `stagePlugin({ responsive: [...] })`; `responsive: []` opts out. The
|
|
203
|
+
* 'compact' name is queryable app-side via `stage.matches('compact')`.
|
|
204
|
+
*/
|
|
205
|
+
const DEFAULT_RESPONSIVE = [{
|
|
206
|
+
name: "compact",
|
|
207
|
+
when: { maxWidth: 600 },
|
|
208
|
+
settings: { padding: 4 }
|
|
209
|
+
}];
|
|
51
210
|
const SETTINGS_EFFECT = {
|
|
52
211
|
flow: "reflow",
|
|
53
212
|
layout: "scene",
|
|
@@ -86,8 +245,9 @@ const settingsEqual = (a, b) => SETTING_KEYS.every((k) => valueEq(a[k], b[k]));
|
|
|
86
245
|
* Layering, stated honestly:
|
|
87
246
|
* • stage-core — PURE spatial math (no DOM, no time). Ports to Rust later.
|
|
88
247
|
* • this capability — the IMPURE platform shell. It dispatches, caches, and owns
|
|
89
|
-
* the camera tween. It is NOT pure;
|
|
90
|
-
*
|
|
248
|
+
* the camera tween. It is NOT pure; its one host dependency (frame timing)
|
|
249
|
+
* enters through the Scheduler seam — injectable, defaulting to the host's
|
|
250
|
+
* own frame clock when one exists (see the seam below).
|
|
91
251
|
*
|
|
92
252
|
* The model: EVERY camera move is defined by what it holds fixed.
|
|
93
253
|
* • gestures (pan/pinch/wheel) — the content under the pointer. Physics, no setting.
|
|
@@ -373,12 +533,100 @@ function createStageCapability(ctx, config = {}) {
|
|
|
373
533
|
fitAlign: ctx.getState().fitAlign,
|
|
374
534
|
direction: ctx.getState().direction
|
|
375
535
|
});
|
|
536
|
+
let placementStarted = false;
|
|
537
|
+
let gestureDepth = 0;
|
|
538
|
+
let gestureZoomed = false;
|
|
539
|
+
let gestureElastic = false;
|
|
540
|
+
let gestureRaw = null;
|
|
541
|
+
const REST_MS = 150;
|
|
542
|
+
let restRaf = 0;
|
|
543
|
+
const armRest = () => {
|
|
544
|
+
if (!canAnimate || !placementStarted) return;
|
|
545
|
+
if (ctx.getState().cameraResting) ctx.dispatch({
|
|
546
|
+
type: "CAMERA_REST",
|
|
547
|
+
resting: false
|
|
548
|
+
});
|
|
549
|
+
if (restRaf) scheduler.caf(restRaf);
|
|
550
|
+
let t0 = 0;
|
|
551
|
+
const tick = (ts) => {
|
|
552
|
+
restRaf = 0;
|
|
553
|
+
if (!t0) t0 = ts;
|
|
554
|
+
if (ts - t0 >= REST_MS) {
|
|
555
|
+
ctx.dispatch({
|
|
556
|
+
type: "CAMERA_REST",
|
|
557
|
+
resting: true
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
restRaf = scheduler.raf(tick);
|
|
562
|
+
};
|
|
563
|
+
restRaf = scheduler.raf(tick);
|
|
564
|
+
};
|
|
376
565
|
const setCam = (next, bounds = stayBounds()) => {
|
|
566
|
+
const clamped = S.clampCamera(next, bounds, vp(), constraint());
|
|
567
|
+
if (clamped.zoom !== cam().zoom) if (gestureDepth > 0) {
|
|
568
|
+
gestureZoomed = true;
|
|
569
|
+
if (ctx.getState().cameraResting) ctx.dispatch({
|
|
570
|
+
type: "CAMERA_REST",
|
|
571
|
+
resting: false
|
|
572
|
+
});
|
|
573
|
+
if (restRaf) {
|
|
574
|
+
scheduler.caf(restRaf);
|
|
575
|
+
restRaf = 0;
|
|
576
|
+
}
|
|
577
|
+
} else armRest();
|
|
377
578
|
ctx.dispatch({
|
|
378
579
|
type: "CAMERA",
|
|
379
|
-
camera:
|
|
580
|
+
camera: clamped
|
|
380
581
|
});
|
|
381
582
|
};
|
|
583
|
+
const axisTravels = (origin, content, view, zoom) => !S.travelRange(origin, content, view, zoom, constraint().padding).fits;
|
|
584
|
+
/** Unclamped finger-integrated camera → the DISPLAYED camera: clamp, then
|
|
585
|
+
* re-apply the overshoot through the resistance curve, per travelling axis.
|
|
586
|
+
* Inside the bounds this is exactly the clamp (rubber of zero is zero);
|
|
587
|
+
* on a fitting axis it is exactly the clamp ALWAYS. */
|
|
588
|
+
const rubberize = (raw) => {
|
|
589
|
+
const clamped = S.clampCamera(raw, stayBounds(), vp(), constraint());
|
|
590
|
+
const v = vp();
|
|
591
|
+
const b = stayBounds();
|
|
592
|
+
const axis = (rawA, clampedA, dim, travels) => {
|
|
593
|
+
if (!travels) return clampedA;
|
|
594
|
+
const dWorld = rawA - clampedA;
|
|
595
|
+
if (dWorld === 0) return clampedA;
|
|
596
|
+
const out = rubberOut(Math.abs(dWorld) * raw.zoom, Math.max(1, dim));
|
|
597
|
+
return clampedA + Math.sign(dWorld) * out / raw.zoom;
|
|
598
|
+
};
|
|
599
|
+
return {
|
|
600
|
+
zoom: raw.zoom,
|
|
601
|
+
x: axis(raw.x, clamped.x, v.width, axisTravels(b.x, b.width, v.width, raw.zoom)),
|
|
602
|
+
y: axis(raw.y, clamped.y, v.height, axisTravels(b.y, b.height, v.height, raw.zoom))
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
/** Displayed camera → the raw position `rubberize` would have produced it
|
|
606
|
+
* from. Overshoot is capped just under the asymptote so the inverse stays
|
|
607
|
+
* finite whatever state a catch finds the camera in. */
|
|
608
|
+
const unrubberize = (displayed) => {
|
|
609
|
+
const clamped = S.clampCamera(displayed, stayBounds(), vp(), constraint());
|
|
610
|
+
const v = vp();
|
|
611
|
+
const b = stayBounds();
|
|
612
|
+
const axis = (dispA, clampedA, dim, travels) => {
|
|
613
|
+
if (!travels) return clampedA;
|
|
614
|
+
const dWorld = dispA - clampedA;
|
|
615
|
+
if (dWorld === 0) return clampedA;
|
|
616
|
+
const d = Math.max(1, dim);
|
|
617
|
+
const out = Math.min(Math.abs(dWorld) * displayed.zoom, d - 1);
|
|
618
|
+
return clampedA + Math.sign(dWorld) * rubberIn(out, d) / displayed.zoom;
|
|
619
|
+
};
|
|
620
|
+
return {
|
|
621
|
+
zoom: displayed.zoom,
|
|
622
|
+
x: axis(displayed.x, clamped.x, v.width, axisTravels(b.x, b.width, v.width, displayed.zoom)),
|
|
623
|
+
y: axis(displayed.y, clamped.y, v.height, axisTravels(b.y, b.height, v.height, displayed.zoom))
|
|
624
|
+
};
|
|
625
|
+
};
|
|
626
|
+
const setCamRaw = (c) => ctx.dispatch({
|
|
627
|
+
type: "CAMERA",
|
|
628
|
+
camera: c
|
|
629
|
+
});
|
|
382
630
|
/**
|
|
383
631
|
* Cursor reconciliation, one direction per interaction:
|
|
384
632
|
* navigation → cursor is INTENT, set explicitly; the camera honors it as far
|
|
@@ -412,7 +660,6 @@ function createStageCapability(ctx, config = {}) {
|
|
|
412
660
|
}
|
|
413
661
|
cancelAnim();
|
|
414
662
|
const from = cam();
|
|
415
|
-
const ease = (t) => 1 - Math.pow(1 - t, 3);
|
|
416
663
|
let started = false;
|
|
417
664
|
let t0 = 0;
|
|
418
665
|
const tick = (now) => {
|
|
@@ -420,7 +667,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
420
667
|
started = true;
|
|
421
668
|
t0 = now;
|
|
422
669
|
}
|
|
423
|
-
const k =
|
|
670
|
+
const k = easeOutCubic(Math.min(1, (now - t0) / ms));
|
|
424
671
|
setCam({
|
|
425
672
|
x: lerp(from.x, target.x, k),
|
|
426
673
|
y: lerp(from.y, target.y, k),
|
|
@@ -434,6 +681,116 @@ function createStageCapability(ctx, config = {}) {
|
|
|
434
681
|
};
|
|
435
682
|
raf = scheduler.raf(tick);
|
|
436
683
|
};
|
|
684
|
+
/**
|
|
685
|
+
* Zoom-anchored tween — animates THE INVARIANT, not the coordinates. A
|
|
686
|
+
* camera move is defined by what it holds fixed; for an anchored zoom that
|
|
687
|
+
* is "the focal page-point stays at `pt` while scale changes". Lerping
|
|
688
|
+
* x/y/zoom independently (the plain tween) breaks that mid-flight: holding
|
|
689
|
+
* a screen point requires `x(t) = focal.x − pt.x / zoom(t)` — hyperbolic in
|
|
690
|
+
* the zoom — so linear coordinates swing the tapped point along a curved
|
|
691
|
+
* path (the visible "dip"). Here the zoom interpolates GEOMETRICALLY
|
|
692
|
+
* (constant rate — zoom is multiplicative) and each frame's camera derives
|
|
693
|
+
* from the anchor at that zoom, then clamps: the focal point is stationary
|
|
694
|
+
* by construction, and a zoom-out rails into its fit progressively as the
|
|
695
|
+
* shrinking travel range lets the clamp take over.
|
|
696
|
+
*/
|
|
697
|
+
const animateZoomAnchored = (focal, pt, targetZoom, ms = 240, then) => {
|
|
698
|
+
if (!canAnimate) {
|
|
699
|
+
const sc = buildScene();
|
|
700
|
+
if (sc.itemCount) setCam(S.cameraForAnchorAtScreen(focal, sc, pt, targetZoom));
|
|
701
|
+
then?.();
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
cancelAnim();
|
|
705
|
+
const z0 = cam().zoom;
|
|
706
|
+
let started = false;
|
|
707
|
+
let t0 = 0;
|
|
708
|
+
const tick = (now) => {
|
|
709
|
+
if (!started) {
|
|
710
|
+
started = true;
|
|
711
|
+
t0 = now;
|
|
712
|
+
}
|
|
713
|
+
const k = easeOutCubic(Math.min(1, (now - t0) / ms));
|
|
714
|
+
const z = zoomLerp(z0, targetZoom, k);
|
|
715
|
+
const sc = buildScene();
|
|
716
|
+
if (sc.itemCount) setCam(S.cameraForAnchorAtScreen(focal, sc, pt, z));
|
|
717
|
+
if (k < 1) raf = scheduler.raf(tick);
|
|
718
|
+
else {
|
|
719
|
+
raf = 0;
|
|
720
|
+
then?.();
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
raf = scheduler.raf(tick);
|
|
724
|
+
};
|
|
725
|
+
const startCameraPhysics = (vxFinger, vyFinger) => {
|
|
726
|
+
if (!canAnimate) {
|
|
727
|
+
setCam(cam());
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
cancelAnim();
|
|
731
|
+
const zoom = cam().zoom;
|
|
732
|
+
const c0 = cam();
|
|
733
|
+
const cl0 = S.clampCamera(c0, stayBounds(), vp(), constraint());
|
|
734
|
+
const b0 = stayBounds();
|
|
735
|
+
const v0 = vp();
|
|
736
|
+
const mk = (camA, clampedA, fingerV) => ({
|
|
737
|
+
p: camA * zoom,
|
|
738
|
+
v: -fingerV / 1e3,
|
|
739
|
+
springing: camA !== clampedA,
|
|
740
|
+
done: false
|
|
741
|
+
});
|
|
742
|
+
const ax = mk(c0.x, cl0.x, axisTravels(b0.x, b0.width, v0.width, zoom) ? vxFinger : 0);
|
|
743
|
+
const ay = mk(c0.y, cl0.y, axisTravels(b0.y, b0.height, v0.height, zoom) ? vyFinger : 0);
|
|
744
|
+
if (!ax.springing && !ay.springing && Math.hypot(ax.v, ay.v) < .02) return;
|
|
745
|
+
let started = false;
|
|
746
|
+
let last = 0;
|
|
747
|
+
const tick = (now) => {
|
|
748
|
+
raf = 0;
|
|
749
|
+
const dt = started ? Math.min(64, Math.max(1, now - last)) : 16;
|
|
750
|
+
started = true;
|
|
751
|
+
last = now;
|
|
752
|
+
const cl = S.clampCamera({
|
|
753
|
+
x: ax.p / zoom,
|
|
754
|
+
y: ay.p / zoom,
|
|
755
|
+
zoom
|
|
756
|
+
}, stayBounds(), vp(), constraint());
|
|
757
|
+
const step = (a, edgeWorld) => {
|
|
758
|
+
if (a.done) return;
|
|
759
|
+
const r = a.springing ? springStep(a.p, a.v, edgeWorld * zoom, dt) : glideStep(a.p, a.v, dt);
|
|
760
|
+
a.p = r.p;
|
|
761
|
+
a.v = r.v;
|
|
762
|
+
a.done = r.done;
|
|
763
|
+
};
|
|
764
|
+
step(ax, cl.x);
|
|
765
|
+
step(ay, cl.y);
|
|
766
|
+
const ncl = S.clampCamera({
|
|
767
|
+
x: ax.p / zoom,
|
|
768
|
+
y: ay.p / zoom,
|
|
769
|
+
zoom
|
|
770
|
+
}, stayBounds(), vp(), constraint());
|
|
771
|
+
const convert = (a, clampedA) => {
|
|
772
|
+
if (!a.springing && Math.abs(clampedA * zoom - a.p) > 1e-6) {
|
|
773
|
+
a.p = clampedA * zoom;
|
|
774
|
+
a.springing = true;
|
|
775
|
+
a.done = false;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
convert(ax, ncl.x);
|
|
779
|
+
convert(ay, ncl.y);
|
|
780
|
+
setCamRaw({
|
|
781
|
+
x: ax.p / zoom,
|
|
782
|
+
y: ay.p / zoom,
|
|
783
|
+
zoom
|
|
784
|
+
});
|
|
785
|
+
syncCursorFromCamera();
|
|
786
|
+
if (ax.done && ay.done) {
|
|
787
|
+
setCam(cam());
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
raf = scheduler.raf(tick);
|
|
791
|
+
};
|
|
792
|
+
raf = scheduler.raf(tick);
|
|
793
|
+
};
|
|
437
794
|
const anchorAt = (at) => {
|
|
438
795
|
const sc = buildScene();
|
|
439
796
|
return sc.itemCount ? S.anchorFromCamera(cam(), sc, vp(), at) : {
|
|
@@ -478,7 +835,16 @@ function createStageCapability(ctx, config = {}) {
|
|
|
478
835
|
if (isFitAll()) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
479
836
|
else applyAnchor(anchor, align);
|
|
480
837
|
};
|
|
838
|
+
/** Tag what drives the NEXT camera/cursor change — dispatched on flips
|
|
839
|
+
* only, read by the page-state feed (see StageState.motionCause). */
|
|
840
|
+
const markCause = (cause) => {
|
|
841
|
+
if (ctx.getState().motionCause !== cause) ctx.dispatch({
|
|
842
|
+
type: "MOTION_CAUSE",
|
|
843
|
+
cause
|
|
844
|
+
});
|
|
845
|
+
};
|
|
481
846
|
const goToTarget = (pageIndex, opts) => {
|
|
847
|
+
markCause("programmatic");
|
|
482
848
|
cancelAnim();
|
|
483
849
|
const doc = ctx.document();
|
|
484
850
|
if (!doc || doc.pageCount === 0) return;
|
|
@@ -561,8 +927,11 @@ function createStageCapability(ctx, config = {}) {
|
|
|
561
927
|
} : unrotatedPoints(box);
|
|
562
928
|
const c = cam();
|
|
563
929
|
const ratio = dpr();
|
|
564
|
-
const
|
|
565
|
-
const
|
|
930
|
+
const rawX = (box.x - c.x) * c.zoom;
|
|
931
|
+
const rawY = (box.y - c.y) * c.zoom;
|
|
932
|
+
const resting = ctx.getState().cameraResting;
|
|
933
|
+
const screenX = resting ? snapToDevice(rawX, ratio) : rawX;
|
|
934
|
+
const screenY = resting ? snapToDevice(rawY, ratio) : rawY;
|
|
566
935
|
const transform = pageTransform({
|
|
567
936
|
pageSize,
|
|
568
937
|
rotation: box.rotation,
|
|
@@ -582,7 +951,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
582
951
|
width: transform.viewWidth,
|
|
583
952
|
height: transform.viewHeight
|
|
584
953
|
});
|
|
585
|
-
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.
|
|
954
|
+
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.viewToContentRect(onScreen) : {
|
|
586
955
|
x: 0,
|
|
587
956
|
y: 0,
|
|
588
957
|
width: 0,
|
|
@@ -597,8 +966,19 @@ function createStageCapability(ctx, config = {}) {
|
|
|
597
966
|
visibleRect
|
|
598
967
|
};
|
|
599
968
|
};
|
|
969
|
+
const EMPTY_SCROLL_METRICS = {
|
|
970
|
+
scrollLeft: 0,
|
|
971
|
+
scrollTop: 0,
|
|
972
|
+
scrollWidth: 0,
|
|
973
|
+
scrollHeight: 0,
|
|
974
|
+
clientWidth: 0,
|
|
975
|
+
clientHeight: 0,
|
|
976
|
+
scrollableX: false,
|
|
977
|
+
scrollableY: false
|
|
978
|
+
};
|
|
600
979
|
let scrollMemo = null;
|
|
601
980
|
const scrollMetricsNow = () => {
|
|
981
|
+
if (!ctx.getState().placed) return EMPTY_SCROLL_METRICS;
|
|
602
982
|
const sc = buildScene();
|
|
603
983
|
const m = S.scrollMetrics(cam(), sc.itemCount ? stayBounds() : {
|
|
604
984
|
x: 0,
|
|
@@ -611,14 +991,16 @@ function createStageCapability(ctx, config = {}) {
|
|
|
611
991
|
scrollMemo = m;
|
|
612
992
|
return m;
|
|
613
993
|
};
|
|
994
|
+
const EMPTY_VISIBLE_PAGES = [];
|
|
614
995
|
let visSig = "";
|
|
615
996
|
let vis = [];
|
|
616
997
|
const visiblePages = () => {
|
|
998
|
+
if (!ctx.getState().placed) return EMPTY_VISIBLE_PAGES;
|
|
617
999
|
const c = cam();
|
|
618
1000
|
const v = vp();
|
|
619
1001
|
const sc = buildScene();
|
|
620
1002
|
const items = paged() ? sc.items.slice(0, 1) : sc.query(S.cameraWorldRect(c, v));
|
|
621
|
-
const sig = `${sceneCache.key}|${ctx.getState().flow}|${c.x},${c.y},${c.zoom}|${v.width}x${v.height}|${dpr()}`;
|
|
1003
|
+
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}`;
|
|
622
1004
|
if (sig === visSig) return vis;
|
|
623
1005
|
visSig = sig;
|
|
624
1006
|
vis = items.flatMap((it) => it.pages).map(withTransform);
|
|
@@ -630,8 +1012,73 @@ function createStageCapability(ctx, config = {}) {
|
|
|
630
1012
|
return out;
|
|
631
1013
|
};
|
|
632
1014
|
const snapshotSettings = () => pickSettings(ctx.getState());
|
|
1015
|
+
/**
|
|
1016
|
+
* Settings change, reacted per the registry — the strongest effect among the
|
|
1017
|
+
* touched settings wins: 'reflow' ⊃ 'scene'/'refit' ⊃ 'reclamp' ⊃ 'none'.
|
|
1018
|
+
* The ONE reactive applier: the public `update()` and the responsive driver
|
|
1019
|
+
* both land here, so a breakpoint crossing gets exactly the invariant a
|
|
1020
|
+
* hand-written update would.
|
|
1021
|
+
*/
|
|
1022
|
+
const applySettings = (patch) => {
|
|
1023
|
+
cancelAnim();
|
|
1024
|
+
const touched = (effect) => SETTING_KEYS.some((k) => patch[k] !== void 0 && SETTINGS_EFFECT[k] === effect);
|
|
1025
|
+
const align = touched("refit") && !touched("scene") && !touched("reflow") ? ctx.getState().zoomAlign : ctx.getState().anchorAlign;
|
|
1026
|
+
const anchor = anchorAt(alignPoint(align, vp()));
|
|
1027
|
+
ctx.dispatch({
|
|
1028
|
+
type: "PATCH",
|
|
1029
|
+
patch
|
|
1030
|
+
});
|
|
1031
|
+
if (touched("scene")) sceneCache = null;
|
|
1032
|
+
if (touched("reflow")) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
1033
|
+
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
1034
|
+
else if (touched("reclamp")) setCam(cam());
|
|
1035
|
+
};
|
|
1036
|
+
let base = snapshotSettings();
|
|
1037
|
+
let rules = config.responsive ?? DEFAULT_RESPONSIVE;
|
|
1038
|
+
let lastEffective = base;
|
|
1039
|
+
const publishActive = (active) => {
|
|
1040
|
+
const prev = ctx.getState().activeRules;
|
|
1041
|
+
if (prev.length !== active.length || active.some((n, i) => n !== prev[i])) ctx.dispatch({
|
|
1042
|
+
type: "RESPONSIVE",
|
|
1043
|
+
active
|
|
1044
|
+
});
|
|
1045
|
+
};
|
|
1046
|
+
/**
|
|
1047
|
+
* Re-resolve and apply what CHANGED since the last resolution. `react: true`
|
|
1048
|
+
* routes the patch through `applySettings` (full anchor-preserving update);
|
|
1049
|
+
* `react: false` (inside `setViewport`, whose caller runs its own reframe)
|
|
1050
|
+
* just lands the patch + cache invalidation and reports the strongest
|
|
1051
|
+
* effect so the caller can escalate a rule-driven flow flip.
|
|
1052
|
+
*/
|
|
1053
|
+
const syncResponsive = (react) => {
|
|
1054
|
+
const { effective, active } = resolveResponsive(base, rules, boxOf(vp()));
|
|
1055
|
+
const patch = {};
|
|
1056
|
+
for (const k of SETTING_KEYS) if (!eqSetting(effective[k], lastEffective[k])) Object.assign(patch, { [k]: effective[k] });
|
|
1057
|
+
lastEffective = effective;
|
|
1058
|
+
publishActive(active);
|
|
1059
|
+
const keys = Object.keys(patch);
|
|
1060
|
+
if (keys.length === 0) return "none";
|
|
1061
|
+
if (react) {
|
|
1062
|
+
applySettings(patch);
|
|
1063
|
+
return "none";
|
|
1064
|
+
}
|
|
1065
|
+
ctx.dispatch({
|
|
1066
|
+
type: "PATCH",
|
|
1067
|
+
patch
|
|
1068
|
+
});
|
|
1069
|
+
const rank = {
|
|
1070
|
+
none: 0,
|
|
1071
|
+
reclamp: 1,
|
|
1072
|
+
refit: 2,
|
|
1073
|
+
scene: 3,
|
|
1074
|
+
reflow: 4
|
|
1075
|
+
};
|
|
1076
|
+
let strongest = "none";
|
|
1077
|
+
for (const k of keys) if (rank[SETTINGS_EFFECT[k]] > rank[strongest]) strongest = SETTINGS_EFFECT[k];
|
|
1078
|
+
if (strongest === "scene" || strongest === "reflow") sceneCache = null;
|
|
1079
|
+
return strongest;
|
|
1080
|
+
};
|
|
633
1081
|
const initialViewProviders = [];
|
|
634
|
-
let hasPlaced = false;
|
|
635
1082
|
const api = {
|
|
636
1083
|
camera: cam,
|
|
637
1084
|
viewport: vp,
|
|
@@ -649,6 +1096,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
649
1096
|
label: p.label ?? null
|
|
650
1097
|
})),
|
|
651
1098
|
pageRect: (pon) => {
|
|
1099
|
+
if (!ctx.getState().placed) return null;
|
|
652
1100
|
const meta = ctx.document();
|
|
653
1101
|
const index = meta ? meta.pages.findIndex((p) => p.pageObjectNumber === pon) : -1;
|
|
654
1102
|
if (index < 0) return null;
|
|
@@ -663,7 +1111,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
663
1111
|
const ly = screen.y - p.screenY;
|
|
664
1112
|
if (lx >= 0 && ly >= 0 && lx <= p.transform.viewWidth && ly <= p.transform.viewHeight) return {
|
|
665
1113
|
pon: p.pon,
|
|
666
|
-
point: p.transform.
|
|
1114
|
+
point: p.transform.viewToContent({
|
|
667
1115
|
x: lx,
|
|
668
1116
|
y: ly
|
|
669
1117
|
}),
|
|
@@ -677,7 +1125,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
677
1125
|
pointOnPage: (pon, screen) => {
|
|
678
1126
|
const p = visiblePages().find((v) => v.pon === pon);
|
|
679
1127
|
if (!p) return null;
|
|
680
|
-
return p.transform.
|
|
1128
|
+
return p.transform.viewToContent({
|
|
681
1129
|
x: screen.x - p.screenX,
|
|
682
1130
|
y: screen.y - p.screenY
|
|
683
1131
|
});
|
|
@@ -749,11 +1197,12 @@ function createStageCapability(ctx, config = {}) {
|
|
|
749
1197
|
anchor: currentAnchor()
|
|
750
1198
|
}),
|
|
751
1199
|
setViewport: (v) => {
|
|
752
|
-
if (!
|
|
1200
|
+
if (!placementStarted) {
|
|
753
1201
|
ctx.dispatch({
|
|
754
1202
|
type: "VP",
|
|
755
1203
|
vp: v
|
|
756
1204
|
});
|
|
1205
|
+
syncResponsive(false);
|
|
757
1206
|
if (v.width > 0 && v.height > 0 && (ctx.document()?.pageCount ?? 0) > 0) api.placeInitial();
|
|
758
1207
|
return;
|
|
759
1208
|
}
|
|
@@ -763,7 +1212,8 @@ function createStageCapability(ctx, config = {}) {
|
|
|
763
1212
|
type: "VP",
|
|
764
1213
|
vp: v
|
|
765
1214
|
});
|
|
766
|
-
|
|
1215
|
+
if (syncResponsive(false) === "reflow") goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
1216
|
+
else reapply(anchor);
|
|
767
1217
|
},
|
|
768
1218
|
setDevicePixelRatio: (ratio) => {
|
|
769
1219
|
if (ratio > 0 && ratio !== dpr()) ctx.dispatch({
|
|
@@ -772,16 +1222,22 @@ function createStageCapability(ctx, config = {}) {
|
|
|
772
1222
|
});
|
|
773
1223
|
},
|
|
774
1224
|
setCamera: (c) => {
|
|
1225
|
+
markCause("user");
|
|
775
1226
|
cancelAnim();
|
|
776
1227
|
setCam(c);
|
|
777
1228
|
syncCursorFromCamera();
|
|
778
1229
|
},
|
|
779
1230
|
panBy: (dx, dy) => {
|
|
1231
|
+
markCause("user");
|
|
780
1232
|
cancelAnim();
|
|
781
|
-
|
|
1233
|
+
if (gestureDepth > 0 && gestureElastic) {
|
|
1234
|
+
gestureRaw = S.panByScreen(gestureRaw ?? unrubberize(cam()), dx, dy);
|
|
1235
|
+
setCamRaw(rubberize(gestureRaw));
|
|
1236
|
+
} else setCam(S.panByScreen(cam(), dx, dy));
|
|
782
1237
|
syncCursorFromCamera();
|
|
783
1238
|
},
|
|
784
1239
|
scrollTo: (opts) => {
|
|
1240
|
+
markCause("user");
|
|
785
1241
|
cancelAnim();
|
|
786
1242
|
if (!buildScene().itemCount) return;
|
|
787
1243
|
const next = S.cameraFromScroll(cam(), stayBounds(), vp(), pad(), opts);
|
|
@@ -804,13 +1260,71 @@ function createStageCapability(ctx, config = {}) {
|
|
|
804
1260
|
const before = buildScene();
|
|
805
1261
|
const focal = before.itemCount ? S.anchorAtPoint(before, S.toWorld(cam(), pt)) : null;
|
|
806
1262
|
setCam(S.zoomAround(cam(), pt, factor));
|
|
807
|
-
ctx.dispatch({
|
|
1263
|
+
if (gestureDepth === 0) ctx.dispatch({
|
|
808
1264
|
type: "PATCH",
|
|
809
1265
|
patch: { zoom: { level: cam().zoom } }
|
|
810
1266
|
});
|
|
811
1267
|
const after = buildScene();
|
|
812
1268
|
if (after !== before && focal && after.itemCount) setCam(S.cameraForAnchorAtScreen(focal, after, pt, cam().zoom));
|
|
1269
|
+
if (gestureDepth > 0 && gestureElastic) gestureRaw = unrubberize(cam());
|
|
1270
|
+
syncCursorFromCamera();
|
|
1271
|
+
},
|
|
1272
|
+
beginGesture: (options) => {
|
|
1273
|
+
gestureDepth++;
|
|
1274
|
+
if (gestureDepth === 1) {
|
|
1275
|
+
cancelAnim();
|
|
1276
|
+
gestureZoomed = false;
|
|
1277
|
+
gestureElastic = options?.elastic === true;
|
|
1278
|
+
gestureRaw = null;
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
endGesture: () => {
|
|
1282
|
+
if (gestureDepth === 0) return;
|
|
1283
|
+
gestureDepth--;
|
|
1284
|
+
if (gestureDepth > 0) return;
|
|
1285
|
+
gestureRaw = null;
|
|
1286
|
+
const wasElastic = gestureElastic;
|
|
1287
|
+
gestureElastic = false;
|
|
1288
|
+
if (gestureZoomed) {
|
|
1289
|
+
gestureZoomed = false;
|
|
1290
|
+
ctx.dispatch({
|
|
1291
|
+
type: "PATCH",
|
|
1292
|
+
patch: { zoom: { level: cam().zoom } }
|
|
1293
|
+
});
|
|
1294
|
+
armRest();
|
|
1295
|
+
}
|
|
813
1296
|
syncCursorFromCamera();
|
|
1297
|
+
if (wasElastic) {
|
|
1298
|
+
const c = cam();
|
|
1299
|
+
const cl = S.clampCamera(c, stayBounds(), vp(), constraint());
|
|
1300
|
+
if (cl.x !== c.x || cl.y !== c.y) startCameraPhysics(0, 0);
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
fling: (vx, vy) => startCameraPhysics(vx, vy),
|
|
1304
|
+
cameraInMotion: () => raf !== 0,
|
|
1305
|
+
doubleTapZoom: (pt) => {
|
|
1306
|
+
cancelAnim();
|
|
1307
|
+
const sc0 = buildScene();
|
|
1308
|
+
if (!sc0.itemCount) return;
|
|
1309
|
+
const item = paged() ? sc0.items[0] : sc0.items[itemIndexOfPage(ctx.getState().cursor)];
|
|
1310
|
+
const fit = fitBox(item);
|
|
1311
|
+
const auto = S.resolveZoom({ mode: S.ZoomMode.Automatic }, fit, vp(), pad());
|
|
1312
|
+
const stops = [
|
|
1313
|
+
auto,
|
|
1314
|
+
S.resolveZoom({ mode: S.ZoomMode.FitWidth }, fit, vp(), pad()),
|
|
1315
|
+
Math.min(auto * 2.5, S.ZOOM_MAX)
|
|
1316
|
+
].sort((a, b) => a - b).filter((z, i, all) => i === 0 || z > all[i - 1] * 1.1);
|
|
1317
|
+
const near = (z, stop) => z > stop / 1.1 && z < stop * 1.1;
|
|
1318
|
+
const onRung = stops.findIndex((s) => near(cam().zoom, s));
|
|
1319
|
+
const target = onRung >= 0 ? stops[(onRung + 1) % stops.length] : stops[0];
|
|
1320
|
+
ctx.dispatch({
|
|
1321
|
+
type: "PATCH",
|
|
1322
|
+
patch: { zoom: { level: target } }
|
|
1323
|
+
});
|
|
1324
|
+
const sc = buildScene();
|
|
1325
|
+
if (!sc.itemCount) return;
|
|
1326
|
+
const focal = S.anchorAtPoint(sc, S.toWorld(cam(), pt));
|
|
1327
|
+
animateZoomAnchored(focal, pt, target, 240, syncCursorFromCamera);
|
|
814
1328
|
},
|
|
815
1329
|
zoomIn: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1.2),
|
|
816
1330
|
zoomOut: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1 / 1.2),
|
|
@@ -820,16 +1334,17 @@ function createStageCapability(ctx, config = {}) {
|
|
|
820
1334
|
fitAll: () => api.update({ zoom: { mode: S.ZoomMode.FitAll } }),
|
|
821
1335
|
automatic: () => api.update({ zoom: { mode: S.ZoomMode.Automatic } }),
|
|
822
1336
|
refit: () => {
|
|
823
|
-
if (!
|
|
1337
|
+
if (!placementStarted) return;
|
|
824
1338
|
cancelAnim();
|
|
825
1339
|
reapply(currentAnchor());
|
|
826
1340
|
},
|
|
827
1341
|
goToPage: (pageIndex, opts) => goToTarget(pageIndex, opts),
|
|
828
1342
|
reveal: (pageIndex, opts) => {
|
|
1343
|
+
markCause("programmatic");
|
|
829
1344
|
const doc = ctx.document();
|
|
830
1345
|
if (!doc || doc.pageCount === 0) return;
|
|
831
1346
|
const target = Math.max(0, Math.min(pageIndex, doc.pageCount - 1));
|
|
832
|
-
if (!(!!opts && (opts.rect
|
|
1347
|
+
if (!(!!opts && (opts.rect != null || opts.anchor !== void 0 || opts.zoom !== void 0 && opts.zoom !== "keep"))) {
|
|
833
1348
|
if (paged()) {
|
|
834
1349
|
goToTarget(target, opts);
|
|
835
1350
|
return;
|
|
@@ -895,20 +1410,17 @@ function createStageCapability(ctx, config = {}) {
|
|
|
895
1410
|
},
|
|
896
1411
|
next: (opts) => step(1, opts),
|
|
897
1412
|
prev: (opts) => step(-1, opts),
|
|
1413
|
+
lensId: () => ctx.id,
|
|
898
1414
|
update: (patch) => {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const align = touched("refit") && !touched("scene") && !touched("reflow") ? ctx.getState().zoomAlign : ctx.getState().anchorAlign;
|
|
902
|
-
const anchor = anchorAt(alignPoint(align, vp()));
|
|
903
|
-
ctx.dispatch({
|
|
904
|
-
type: "PATCH",
|
|
905
|
-
patch
|
|
906
|
-
});
|
|
907
|
-
if (touched("scene")) sceneCache = null;
|
|
908
|
-
if (touched("reflow")) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
909
|
-
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
910
|
-
else if (touched("reclamp")) setCam(cam());
|
|
1415
|
+
base = mergeSettings(base, patch);
|
|
1416
|
+
syncResponsive(true);
|
|
911
1417
|
},
|
|
1418
|
+
setResponsive: (next) => {
|
|
1419
|
+
rules = next;
|
|
1420
|
+
syncResponsive(true);
|
|
1421
|
+
},
|
|
1422
|
+
matches: (name) => ctx.getState().activeRules.includes(name),
|
|
1423
|
+
activeRules: () => ctx.getState().activeRules,
|
|
912
1424
|
setFlow: (flow) => api.update({ flow }),
|
|
913
1425
|
setLayout: (layout) => api.update({ layout }),
|
|
914
1426
|
setSpread: (spread) => api.update({ spread }),
|
|
@@ -928,9 +1440,13 @@ function createStageCapability(ctx, config = {}) {
|
|
|
928
1440
|
setScrollBehavior: (behavior) => api.update({ scrollBehavior: behavior }),
|
|
929
1441
|
applyViewState: (view) => {
|
|
930
1442
|
cancelAnim();
|
|
1443
|
+
base = mergeSettings(base, pickSettings(view));
|
|
1444
|
+
const { effective, active } = resolveResponsive(base, rules, boxOf(vp()));
|
|
1445
|
+
lastEffective = effective;
|
|
1446
|
+
publishActive(active);
|
|
931
1447
|
ctx.dispatch({
|
|
932
1448
|
type: "PATCH",
|
|
933
|
-
patch:
|
|
1449
|
+
patch: effective
|
|
934
1450
|
});
|
|
935
1451
|
ctx.dispatch({
|
|
936
1452
|
type: "CURSOR",
|
|
@@ -946,97 +1462,92 @@ function createStageCapability(ctx, config = {}) {
|
|
|
946
1462
|
});
|
|
947
1463
|
},
|
|
948
1464
|
placeInitial: () => {
|
|
949
|
-
|
|
1465
|
+
placementStarted = true;
|
|
950
1466
|
const sorted = [...initialViewProviders].sort((a, b) => b.priority - a.priority);
|
|
951
1467
|
for (const p of sorted) {
|
|
952
1468
|
const view = p.fn();
|
|
953
1469
|
if (view) {
|
|
954
1470
|
api.applyViewState(view);
|
|
1471
|
+
ctx.dispatch({ type: "PLACED" });
|
|
955
1472
|
return;
|
|
956
1473
|
}
|
|
957
1474
|
}
|
|
958
1475
|
api.resetView();
|
|
1476
|
+
ctx.dispatch({ type: "PLACED" });
|
|
959
1477
|
},
|
|
960
1478
|
resetView: () => goToTarget(0, { behavior: "instant" })
|
|
961
1479
|
};
|
|
962
1480
|
return api;
|
|
963
1481
|
}
|
|
964
1482
|
//#endregion
|
|
965
|
-
//#region src/scroll-handler.ts
|
|
966
|
-
/**
|
|
967
|
-
* Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
|
|
968
|
-
* (the `scroll` tag) and — with `panFallback` — under every tool, where it then
|
|
969
|
-
* captures ONLY page-gap drags (on-page downs belong to the active tool, which
|
|
970
|
-
* claims them first at higher priority). It uses the sample's viewport delta, so
|
|
971
|
-
* it pans over the whole viewport (pages and gaps).
|
|
972
|
-
*
|
|
973
|
-
* Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
|
|
974
|
-
* hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
|
|
975
|
-
* from its `Tool.cursor`, so hovering it needs no claim here.
|
|
976
|
-
*/
|
|
977
|
-
function createScrollHandler(stage, interaction, options = {}) {
|
|
978
|
-
const panFallback = options.panFallback ?? true;
|
|
979
|
-
let last = {
|
|
980
|
-
x: 0,
|
|
981
|
-
y: 0
|
|
982
|
-
};
|
|
983
|
-
const isPanTool = () => interaction.activeTool().enables.has("scroll");
|
|
984
|
-
return {
|
|
985
|
-
id: "stage-scroll",
|
|
986
|
-
priority: 10,
|
|
987
|
-
enabledFor: (tool) => tool.enables.has("scroll") || panFallback,
|
|
988
|
-
onDown: (s) => {
|
|
989
|
-
if (!isPanTool() && s.page) return false;
|
|
990
|
-
last = s.viewport;
|
|
991
|
-
interaction.setCursor("stage-grab", "grabbing", 40);
|
|
992
|
-
return true;
|
|
993
|
-
},
|
|
994
|
-
onMove: (s) => {
|
|
995
|
-
stage.panBy(s.viewport.x - last.x, s.viewport.y - last.y);
|
|
996
|
-
last = s.viewport;
|
|
997
|
-
},
|
|
998
|
-
onUp: () => interaction.setCursor("stage-grab", null),
|
|
999
|
-
onHover: (s) => {
|
|
1000
|
-
if (panFallback && !isPanTool()) interaction.setCursor("stage-pan-fallback", s.page ? null : "grab", 5);
|
|
1001
|
-
}
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
//#endregion
|
|
1005
1483
|
//#region src/effects.ts
|
|
1006
1484
|
/**
|
|
1007
|
-
*
|
|
1008
|
-
* (rotate/move/delete bump `DocumentMeta.revision`), the page geometry under
|
|
1009
|
-
* this lens just changed, so re-fit — re-resolve the active zoom intent and
|
|
1010
|
-
* re-place against the new footprint.
|
|
1485
|
+
* Stage lens effects.
|
|
1011
1486
|
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
-
* only
|
|
1487
|
+
* 1. Re-fit on page-registry mutations: when rotate/move/delete bump
|
|
1488
|
+
* `DocumentMeta.revision`, the page geometry under this lens changed, so
|
|
1489
|
+
* re-resolve the active zoom intent and re-place against the new
|
|
1490
|
+
* footprint. Steady-state only — it reacts to revision CHANGES, never the
|
|
1491
|
+
* initial value, so it can't race the capability's level-triggered
|
|
1492
|
+
* initial placement.
|
|
1016
1493
|
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1494
|
+
* 2. The page-state feed (main lens only, actions plugin present): stage is
|
|
1495
|
+
* AUTHORITATIVE for what page the viewer is on; the action engine's
|
|
1496
|
+
* lifecycle coordinator owns WHEN page-lifecycle triggers fire. This feed
|
|
1497
|
+
* pushes `{ currentPon, visiblePons, placed, cause }` snapshots — pons,
|
|
1498
|
+
* not indexes (reorder-safe) — on placement and on every change; the
|
|
1499
|
+
* coordinator buffers them behind the document-open barrier and diffs
|
|
1500
|
+
* against its last-emitted state, so this side stays a dumb reporter.
|
|
1020
1501
|
*/
|
|
1021
|
-
function registerStageEffects(ctx, token) {
|
|
1502
|
+
function registerStageEffects(ctx, token, feedActions = false) {
|
|
1022
1503
|
ctx.watch(() => ctx.document()?.revision ?? 0, () => ctx.get(token).refit());
|
|
1504
|
+
if (!feedActions) return;
|
|
1505
|
+
const actions = ctx.tryGet(ActionsToken$1);
|
|
1506
|
+
if (!actions) return;
|
|
1507
|
+
const snapshot = () => {
|
|
1508
|
+
const state = ctx.getState();
|
|
1509
|
+
if (!state.placed) return null;
|
|
1510
|
+
const stage = ctx.get(token);
|
|
1511
|
+
return {
|
|
1512
|
+
currentPon: stage.pages()[state.cursor]?.pon ?? null,
|
|
1513
|
+
visiblePons: stage.visiblePages().map((page) => page.pon),
|
|
1514
|
+
cause: state.motionCause
|
|
1515
|
+
};
|
|
1516
|
+
};
|
|
1517
|
+
ctx.watch(() => {
|
|
1518
|
+
const s = snapshot();
|
|
1519
|
+
return s === null ? "unplaced" : `${s.currentPon ?? -1}|${[...s.visiblePons].sort((a, b) => a - b).join(",")}`;
|
|
1520
|
+
}, () => {
|
|
1521
|
+
const s = snapshot();
|
|
1522
|
+
if (s === null) return;
|
|
1523
|
+
actions.reportPageState({
|
|
1524
|
+
currentPon: s.currentPon,
|
|
1525
|
+
visiblePons: s.visiblePons,
|
|
1526
|
+
placed: true,
|
|
1527
|
+
cause: s.cause
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1023
1530
|
}
|
|
1024
1531
|
//#endregion
|
|
1025
1532
|
//#region src/reducer.ts
|
|
1026
1533
|
const initialStageState = (config) => {
|
|
1027
|
-
const { scheduler: _scheduler, ...overrides } = config;
|
|
1534
|
+
const { scheduler: _scheduler, responsive: _responsive, ...overrides } = config;
|
|
1028
1535
|
return {
|
|
1029
1536
|
camera: {
|
|
1030
1537
|
x: 0,
|
|
1031
1538
|
y: 0,
|
|
1032
1539
|
zoom: 1
|
|
1033
1540
|
},
|
|
1541
|
+
placed: false,
|
|
1542
|
+
cameraResting: true,
|
|
1034
1543
|
vp: {
|
|
1035
1544
|
width: 0,
|
|
1036
1545
|
height: 0
|
|
1037
1546
|
},
|
|
1038
1547
|
dpr: 1,
|
|
1039
1548
|
cursor: 0,
|
|
1549
|
+
motionCause: "user",
|
|
1550
|
+
activeRules: [],
|
|
1040
1551
|
...DEFAULT_SETTINGS,
|
|
1041
1552
|
...overrides
|
|
1042
1553
|
};
|
|
@@ -1062,6 +1573,14 @@ const stageReducer = (state, a) => {
|
|
|
1062
1573
|
...state,
|
|
1063
1574
|
camera: a.camera
|
|
1064
1575
|
};
|
|
1576
|
+
case "CAMERA_REST": return state.cameraResting === a.resting ? state : {
|
|
1577
|
+
...state,
|
|
1578
|
+
cameraResting: a.resting
|
|
1579
|
+
};
|
|
1580
|
+
case "PLACED": return state.placed ? state : {
|
|
1581
|
+
...state,
|
|
1582
|
+
placed: true
|
|
1583
|
+
};
|
|
1065
1584
|
case "VP": return {
|
|
1066
1585
|
...state,
|
|
1067
1586
|
vp: a.vp
|
|
@@ -1074,14 +1593,19 @@ const stageReducer = (state, a) => {
|
|
|
1074
1593
|
...state,
|
|
1075
1594
|
cursor: a.cursor
|
|
1076
1595
|
};
|
|
1596
|
+
case "MOTION_CAUSE": return {
|
|
1597
|
+
...state,
|
|
1598
|
+
motionCause: a.cause
|
|
1599
|
+
};
|
|
1077
1600
|
case "PATCH": return applyPatch(state, a.patch);
|
|
1601
|
+
case "RESPONSIVE": return {
|
|
1602
|
+
...state,
|
|
1603
|
+
activeRules: a.active
|
|
1604
|
+
};
|
|
1078
1605
|
default: return state;
|
|
1079
1606
|
}
|
|
1080
1607
|
};
|
|
1081
1608
|
//#endregion
|
|
1082
|
-
//#region src/types.ts
|
|
1083
|
-
const StageToken = createCapabilityToken("stage");
|
|
1084
|
-
//#endregion
|
|
1085
1609
|
//#region src/stage.plugin.ts
|
|
1086
1610
|
/**
|
|
1087
1611
|
* Wires the parts into a kernel plugin. This file is the "manifest": it says what
|
|
@@ -1089,178 +1613,111 @@ const StageToken = createCapabilityToken("stage");
|
|
|
1089
1613
|
* sibling files.
|
|
1090
1614
|
*/
|
|
1091
1615
|
const stagePlugin = (options = {}) => {
|
|
1092
|
-
const { id = "stage", token = StageToken,
|
|
1616
|
+
const { id = "stage", token = StageToken, ...config } = options;
|
|
1093
1617
|
return definePlugin({
|
|
1094
1618
|
id,
|
|
1095
1619
|
token,
|
|
1096
1620
|
scope: "document",
|
|
1097
|
-
optional:
|
|
1621
|
+
optional: [ActionsToken],
|
|
1098
1622
|
initialState: () => initialStageState(config),
|
|
1099
1623
|
reduce: stageReducer,
|
|
1100
1624
|
capability: (ctx) => createStageCapability(ctx, config),
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
if (
|
|
1104
|
-
|
|
1105
|
-
|
|
1625
|
+
effects: (ctx) => registerStageEffects(ctx, token, id === "stage"),
|
|
1626
|
+
init: (ctx) => {
|
|
1627
|
+
if (id !== "stage") return;
|
|
1628
|
+
const actions = ctx.tryGet(ActionsToken$1);
|
|
1629
|
+
if (!actions) return;
|
|
1630
|
+
ctx.cleanup(actions.registerExecutor("goto", (node) => {
|
|
1631
|
+
if (node.type !== "goto") return {
|
|
1632
|
+
status: "inert",
|
|
1633
|
+
reason: "not a goto node"
|
|
1634
|
+
};
|
|
1635
|
+
const stage = ctx.tryGet(token);
|
|
1636
|
+
const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === node.destination.pageObjectNumber);
|
|
1637
|
+
if (!stage || !layout) return {
|
|
1638
|
+
status: "failed",
|
|
1639
|
+
error: "no stage or destination page available"
|
|
1640
|
+
};
|
|
1641
|
+
const { pageIndex, options: reveal } = destinationToReveal(node.destination, layout);
|
|
1642
|
+
stage.reveal(pageIndex, {
|
|
1643
|
+
...reveal,
|
|
1644
|
+
behavior: "smooth"
|
|
1645
|
+
});
|
|
1646
|
+
return { status: "executed" };
|
|
1647
|
+
}));
|
|
1648
|
+
ctx.cleanup(actions.registerExecutor("named", (node) => {
|
|
1649
|
+
if (node.type !== "named") return {
|
|
1650
|
+
status: "inert",
|
|
1651
|
+
reason: "not a named node"
|
|
1652
|
+
};
|
|
1653
|
+
const stage = ctx.tryGet(token);
|
|
1654
|
+
if (!stage) return {
|
|
1655
|
+
status: "failed",
|
|
1656
|
+
error: "no stage available"
|
|
1657
|
+
};
|
|
1658
|
+
switch (node.name) {
|
|
1659
|
+
case "NextPage":
|
|
1660
|
+
stage.next({ behavior: "smooth" });
|
|
1661
|
+
return { status: "executed" };
|
|
1662
|
+
case "PrevPage":
|
|
1663
|
+
stage.prev({ behavior: "smooth" });
|
|
1664
|
+
return { status: "executed" };
|
|
1665
|
+
case "FirstPage":
|
|
1666
|
+
stage.goToPage(0, { behavior: "smooth" });
|
|
1667
|
+
return { status: "executed" };
|
|
1668
|
+
case "LastPage":
|
|
1669
|
+
stage.goToPage(Math.max(0, stage.pageCount() - 1), { behavior: "smooth" });
|
|
1670
|
+
return { status: "executed" };
|
|
1671
|
+
default: return {
|
|
1672
|
+
status: "inert",
|
|
1673
|
+
reason: `unknown named action '${node.name}'`
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
}));
|
|
1677
|
+
}
|
|
1106
1678
|
});
|
|
1107
1679
|
};
|
|
1108
1680
|
//#endregion
|
|
1109
|
-
//#region src/
|
|
1681
|
+
//#region src/scroll-handler.ts
|
|
1110
1682
|
/**
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
* code exists anywhere else.
|
|
1117
|
-
*
|
|
1118
|
-
* The whole protocol collapses onto three knobs:
|
|
1119
|
-
* rect — what to look at (a point for /XYZ, a rect for /FitR, the
|
|
1120
|
-
* page for /Fit, the content bounding box for /FitB*)
|
|
1121
|
-
* zoom — 'keep' | {level} | 'fit' | 'fit-width' | 'fit-height'
|
|
1122
|
-
* anchor — where it lands; 'keep' encodes the spec's null-means-retain
|
|
1683
|
+
* Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
|
|
1684
|
+
* (the `scroll` tag) and — with `panFallback` — under every tool, where it then
|
|
1685
|
+
* captures ONLY page-gap drags (on-page downs belong to the active tool, which
|
|
1686
|
+
* claims them first at higher priority). It uses the sample's viewport delta, so
|
|
1687
|
+
* it pans over the whole viewport (pages and gaps).
|
|
1123
1688
|
*
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
*/
|
|
1128
|
-
/**
|
|
1129
|
-
* Translate one explicit destination against its target page's layout.
|
|
1130
|
-
* `bbox` is the page's content BOUNDING box in CONTENT space, for the
|
|
1131
|
-
* `/FitB*` kinds — pass it when known; the crop box is the spec-tolerated
|
|
1132
|
-
* fallback until the engine exposes it.
|
|
1689
|
+
* Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
|
|
1690
|
+
* hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
|
|
1691
|
+
* from its `Tool.cursor`, so hovering it needs no claim here.
|
|
1133
1692
|
*/
|
|
1134
|
-
function
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
rotation: layout.rotation,
|
|
1138
|
-
userUnit: layout.userUnit
|
|
1139
|
-
}, 1);
|
|
1140
|
-
const crop = layout.boxes.crop;
|
|
1141
|
-
const toContent = (x, y) => applyPoint(geo.pdfToContent, {
|
|
1142
|
-
x,
|
|
1143
|
-
y
|
|
1144
|
-
});
|
|
1145
|
-
const page = {
|
|
1693
|
+
function createScrollHandler(stage, interaction, options = {}) {
|
|
1694
|
+
const panFallback = options.panFallback ?? true;
|
|
1695
|
+
let last = {
|
|
1146
1696
|
x: 0,
|
|
1147
|
-
y: 0
|
|
1148
|
-
width: layout.size.width,
|
|
1149
|
-
height: layout.size.height
|
|
1697
|
+
y: 0
|
|
1150
1698
|
};
|
|
1151
|
-
const
|
|
1152
|
-
const options = (() => {
|
|
1153
|
-
switch (dest.kind) {
|
|
1154
|
-
case "xyz": {
|
|
1155
|
-
const hasLeft = dest.left != null;
|
|
1156
|
-
const hasTop = dest.top != null;
|
|
1157
|
-
const p = toContent(dest.left ?? crop.left, dest.top ?? crop.top);
|
|
1158
|
-
return {
|
|
1159
|
-
rect: {
|
|
1160
|
-
x: p.x,
|
|
1161
|
-
y: p.y,
|
|
1162
|
-
width: 0,
|
|
1163
|
-
height: 0
|
|
1164
|
-
},
|
|
1165
|
-
anchor: {
|
|
1166
|
-
x: hasLeft ? "start" : "keep",
|
|
1167
|
-
y: hasTop ? "start" : "keep"
|
|
1168
|
-
},
|
|
1169
|
-
zoom: dest.zoom != null && dest.zoom !== 0 ? { level: dest.zoom } : "keep"
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
case "fit": return { zoom: "fit" };
|
|
1173
|
-
case "fitH": {
|
|
1174
|
-
const hasTop = dest.top != null;
|
|
1175
|
-
return {
|
|
1176
|
-
rect: {
|
|
1177
|
-
x: 0,
|
|
1178
|
-
y: hasTop ? toContent(crop.left, dest.top).y : 0,
|
|
1179
|
-
width: page.width,
|
|
1180
|
-
height: 0
|
|
1181
|
-
},
|
|
1182
|
-
zoom: "fit-width",
|
|
1183
|
-
anchor: { y: hasTop ? "start" : "keep" }
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
|
-
case "fitV": {
|
|
1187
|
-
const hasLeft = dest.left != null;
|
|
1188
|
-
return {
|
|
1189
|
-
rect: {
|
|
1190
|
-
x: hasLeft ? toContent(dest.left, crop.top).x : 0,
|
|
1191
|
-
y: 0,
|
|
1192
|
-
width: 0,
|
|
1193
|
-
height: page.height
|
|
1194
|
-
},
|
|
1195
|
-
zoom: "fit-height",
|
|
1196
|
-
anchor: { x: hasLeft ? "start" : "keep" }
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
case "fitR": {
|
|
1200
|
-
const tl = toContent(dest.left, dest.top);
|
|
1201
|
-
const br = toContent(dest.right, dest.bottom);
|
|
1202
|
-
return {
|
|
1203
|
-
rect: {
|
|
1204
|
-
x: Math.min(tl.x, br.x),
|
|
1205
|
-
y: Math.min(tl.y, br.y),
|
|
1206
|
-
width: Math.abs(br.x - tl.x),
|
|
1207
|
-
height: Math.abs(br.y - tl.y)
|
|
1208
|
-
},
|
|
1209
|
-
zoom: "fit"
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
case "fitB": return {
|
|
1213
|
-
rect: box,
|
|
1214
|
-
zoom: "fit"
|
|
1215
|
-
};
|
|
1216
|
-
case "fitBH": {
|
|
1217
|
-
const hasTop = dest.top != null;
|
|
1218
|
-
const y = hasTop ? toContent(crop.left, dest.top).y : box.y;
|
|
1219
|
-
return {
|
|
1220
|
-
rect: {
|
|
1221
|
-
x: box.x,
|
|
1222
|
-
y,
|
|
1223
|
-
width: box.width,
|
|
1224
|
-
height: 0
|
|
1225
|
-
},
|
|
1226
|
-
zoom: "fit-width",
|
|
1227
|
-
anchor: { y: hasTop ? "start" : "keep" }
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
case "fitBV": {
|
|
1231
|
-
const hasLeft = dest.left != null;
|
|
1232
|
-
return {
|
|
1233
|
-
rect: {
|
|
1234
|
-
x: hasLeft ? toContent(dest.left, crop.top).x : box.x,
|
|
1235
|
-
y: box.y,
|
|
1236
|
-
width: 0,
|
|
1237
|
-
height: box.height
|
|
1238
|
-
},
|
|
1239
|
-
zoom: "fit-height",
|
|
1240
|
-
anchor: { x: hasLeft ? "start" : "keep" }
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
})();
|
|
1699
|
+
const isPanTool = () => interaction.activeTool().enables.has("scroll");
|
|
1245
1700
|
return {
|
|
1246
|
-
|
|
1247
|
-
|
|
1701
|
+
id: "stage-scroll",
|
|
1702
|
+
priority: 10,
|
|
1703
|
+
enabledFor: (tool) => tool.enables.has("scroll") || panFallback,
|
|
1704
|
+
onDown: (s) => {
|
|
1705
|
+
if (!isPanTool() && s.page) return false;
|
|
1706
|
+
last = s.viewport;
|
|
1707
|
+
interaction.setCursor("stage-grab", "grabbing", 40);
|
|
1708
|
+
return true;
|
|
1709
|
+
},
|
|
1710
|
+
onMove: (s) => {
|
|
1711
|
+
stage.panBy(s.viewport.x - last.x, s.viewport.y - last.y);
|
|
1712
|
+
last = s.viewport;
|
|
1713
|
+
},
|
|
1714
|
+
onUp: () => interaction.setCursor("stage-grab", null),
|
|
1715
|
+
onHover: (s) => {
|
|
1716
|
+
if (panFallback && !isPanTool()) interaction.setCursor("stage-pan-fallback", s.page ? null : "grab", 5);
|
|
1717
|
+
}
|
|
1248
1718
|
};
|
|
1249
1719
|
}
|
|
1250
1720
|
//#endregion
|
|
1251
|
-
|
|
1252
|
-
/** The per-notch step for discrete wheels — the zoom buttons' ratio. */
|
|
1253
|
-
const NOTCH = 1.2;
|
|
1254
|
-
/** Zoom factor for a ctrl/cmd wheel event (the caller decides zoom vs pan). */
|
|
1255
|
-
function wheelZoomFactor(e) {
|
|
1256
|
-
if (e.deltaMode !== 0) return Math.pow(NOTCH, -Math.sign(e.deltaY));
|
|
1257
|
-
if (e.ctrlKey) {
|
|
1258
|
-
if (Math.abs(e.deltaY) >= 100) return Math.pow(NOTCH, -Math.sign(e.deltaY));
|
|
1259
|
-
return Math.exp(-e.deltaY / 100);
|
|
1260
|
-
}
|
|
1261
|
-
return Math.exp(-e.deltaY * .0015);
|
|
1262
|
-
}
|
|
1263
|
-
//#endregion
|
|
1264
|
-
export { DEFAULT_SETTINGS, StageToken, ZoomMode, destinationToReveal, settingsEqual, stagePlugin, wheelZoomFactor };
|
|
1721
|
+
export { DEFAULT_RESPONSIVE, DEFAULT_SETTINGS, StageToken, ZoomMode, boxOf, createScrollHandler, destinationToReveal, matchesQuery, resolveResponsive, settingsEqual, stagePlugin };
|
|
1265
1722
|
|
|
1266
1723
|
//# sourceMappingURL=index.js.map
|