@embedpdf/plugin-stage 3.0.0-next.10 → 3.0.0-next.12

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.js CHANGED
@@ -1,7 +1,11 @@
1
- import { createCapabilityToken, definePlugin } from "@embedpdf/core";
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";
2
6
  import * as S from "@embedpdf/core-stage";
3
- import { ZoomMode, ZoomMode as ZoomMode$1 } from "@embedpdf/core-stage";
4
- import { addRotations, applyPoint, applyRect, displaySize, intersectRects, pageGeometry, pageTransform, rotateScaleMatrix, snapToDevice } from "@embedpdf/core-geometry";
7
+ import { ZoomMode as ZoomMode$1 } from "@embedpdf/core-stage";
8
+ import { addRotations, applyPoint, applyRect, displaySize, intersectRects, pageTransform, rotateScaleMatrix, snapToDevice } from "@embedpdf/core-geometry";
5
9
  //#region src/motion.ts
6
10
  /**
7
11
  * The camera's MOTION vocabulary — every formula, pure.
@@ -831,7 +835,16 @@ function createStageCapability(ctx, config = {}) {
831
835
  if (isFitAll()) goToTarget(ctx.getState().cursor, { behavior: "instant" });
832
836
  else applyAnchor(anchor, align);
833
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
+ };
834
846
  const goToTarget = (pageIndex, opts) => {
847
+ markCause("programmatic");
835
848
  cancelAnim();
836
849
  const doc = ctx.document();
837
850
  if (!doc || doc.pageCount === 0) return;
@@ -1209,11 +1222,13 @@ function createStageCapability(ctx, config = {}) {
1209
1222
  });
1210
1223
  },
1211
1224
  setCamera: (c) => {
1225
+ markCause("user");
1212
1226
  cancelAnim();
1213
1227
  setCam(c);
1214
1228
  syncCursorFromCamera();
1215
1229
  },
1216
1230
  panBy: (dx, dy) => {
1231
+ markCause("user");
1217
1232
  cancelAnim();
1218
1233
  if (gestureDepth > 0 && gestureElastic) {
1219
1234
  gestureRaw = S.panByScreen(gestureRaw ?? unrubberize(cam()), dx, dy);
@@ -1222,6 +1237,7 @@ function createStageCapability(ctx, config = {}) {
1222
1237
  syncCursorFromCamera();
1223
1238
  },
1224
1239
  scrollTo: (opts) => {
1240
+ markCause("user");
1225
1241
  cancelAnim();
1226
1242
  if (!buildScene().itemCount) return;
1227
1243
  const next = S.cameraFromScroll(cam(), stayBounds(), vp(), pad(), opts);
@@ -1324,6 +1340,7 @@ function createStageCapability(ctx, config = {}) {
1324
1340
  },
1325
1341
  goToPage: (pageIndex, opts) => goToTarget(pageIndex, opts),
1326
1342
  reveal: (pageIndex, opts) => {
1343
+ markCause("programmatic");
1327
1344
  const doc = ctx.document();
1328
1345
  if (!doc || doc.pageCount === 0) return;
1329
1346
  const target = Math.max(0, Math.min(pageIndex, doc.pageCount - 1));
@@ -1465,22 +1482,51 @@ function createStageCapability(ctx, config = {}) {
1465
1482
  //#endregion
1466
1483
  //#region src/effects.ts
1467
1484
  /**
1468
- * The one effect a stage lens needs: when the document's page registry changes
1469
- * (rotate/move/delete bump `DocumentMeta.revision`), the page geometry under
1470
- * this lens just changed, so re-fit — re-resolve the active zoom intent and
1471
- * re-place against the new footprint.
1485
+ * Stage lens effects.
1472
1486
  *
1473
- * Without it, fit/pixel zoom modes stay anchored to the pre-mutation dimensions:
1474
- * `pageWidth: 110` renders wider after a 90° rotation (width↔height swap), and
1475
- * `fitPage` over- or under-fills. The scene already re-keys on the revision, so
1476
- * only the resolved `cam.zoom` was stale; `refit()` recomputes it.
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.
1477
1493
  *
1478
- * Steady-state only: this reacts to revision CHANGES, never the initial value,
1479
- * so it can't race the capability's level-triggered initial placement (the
1480
- * reason the rest of the plugin is effect-free).
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.
1481
1501
  */
1482
- function registerStageEffects(ctx, token) {
1502
+ function registerStageEffects(ctx, token, feedActions = false) {
1483
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
+ });
1484
1530
  }
1485
1531
  //#endregion
1486
1532
  //#region src/reducer.ts
@@ -1500,6 +1546,7 @@ const initialStageState = (config) => {
1500
1546
  },
1501
1547
  dpr: 1,
1502
1548
  cursor: 0,
1549
+ motionCause: "user",
1503
1550
  activeRules: [],
1504
1551
  ...DEFAULT_SETTINGS,
1505
1552
  ...overrides
@@ -1546,6 +1593,10 @@ const stageReducer = (state, a) => {
1546
1593
  ...state,
1547
1594
  cursor: a.cursor
1548
1595
  };
1596
+ case "MOTION_CAUSE": return {
1597
+ ...state,
1598
+ motionCause: a.cause
1599
+ };
1549
1600
  case "PATCH": return applyPatch(state, a.patch);
1550
1601
  case "RESPONSIVE": return {
1551
1602
  ...state,
@@ -1555,9 +1606,6 @@ const stageReducer = (state, a) => {
1555
1606
  }
1556
1607
  };
1557
1608
  //#endregion
1558
- //#region src/types.ts
1559
- const StageToken = createCapabilityToken("stage");
1560
- //#endregion
1561
1609
  //#region src/stage.plugin.ts
1562
1610
  /**
1563
1611
  * Wires the parts into a kernel plugin. This file is the "manifest": it says what
@@ -1570,154 +1618,65 @@ const stagePlugin = (options = {}) => {
1570
1618
  id,
1571
1619
  token,
1572
1620
  scope: "document",
1621
+ optional: [ActionsToken],
1573
1622
  initialState: () => initialStageState(config),
1574
1623
  reduce: stageReducer,
1575
1624
  capability: (ctx) => createStageCapability(ctx, config),
1576
- effects: (ctx) => registerStageEffects(ctx, token)
1577
- });
1578
- };
1579
- //#endregion
1580
- //#region src/destination.ts
1581
- /**
1582
- * PDF destination → reveal: the pure translator between the PDF protocol's
1583
- * navigation vocabulary (ISO 32000-1 §12.3.2.2) and the Stage's arrival
1584
- * primitive. Outline clicks, link annotations, and `/OpenAction` all
1585
- * resolve (engine-side) to an explicit `PdfDestination`; this maps it onto
1586
- * ONE `reveal(pageIndex, options)` call — no destination-specific camera
1587
- * code exists anywhere else.
1588
- *
1589
- * The whole protocol collapses onto three knobs:
1590
- * rect — what to look at (a point for /XYZ, a rect for /FitR, the
1591
- * page for /Fit, the content bounding box for /FitB*)
1592
- * zoom — 'keep' | {level} | 'fit' | 'fit-width' | 'fit-height'
1593
- * anchor — where it lands; 'keep' encodes the spec's null-means-retain
1594
- *
1595
- * Coordinates convert PDF user space (y-up, absolute) → content space
1596
- * (y-down, crop-relative) through the SAME `pdfToContent` matrix selection
1597
- * and search use, so a destination and an overlay can never disagree.
1598
- */
1599
- /**
1600
- * Translate one explicit destination against its target page's layout.
1601
- * `bbox` is the page's content BOUNDING box in CONTENT space, for the
1602
- * `/FitB*` kinds — pass it when known; the crop box is the spec-tolerated
1603
- * fallback until the engine exposes it.
1604
- */
1605
- function destinationToReveal(dest, layout, bbox) {
1606
- const geo = pageGeometry({
1607
- crop: layout.boxes.crop,
1608
- rotation: layout.rotation,
1609
- userUnit: layout.userUnit
1610
- }, 1);
1611
- const crop = layout.boxes.crop;
1612
- const toContent = (x, y) => applyPoint(geo.pdfToContent, {
1613
- x,
1614
- y
1615
- });
1616
- const page = {
1617
- x: 0,
1618
- y: 0,
1619
- width: layout.size.width,
1620
- height: layout.size.height
1621
- };
1622
- const box = bbox ?? page;
1623
- const options = (() => {
1624
- switch (dest.kind) {
1625
- case "xyz": {
1626
- const hasLeft = dest.left != null;
1627
- const hasTop = dest.top != null;
1628
- const p = toContent(dest.left ?? crop.left, dest.top ?? crop.top);
1629
- return {
1630
- rect: {
1631
- x: p.x,
1632
- y: p.y,
1633
- width: 0,
1634
- height: 0
1635
- },
1636
- anchor: {
1637
- x: hasLeft ? "start" : "keep",
1638
- y: hasTop ? "start" : "keep"
1639
- },
1640
- zoom: dest.zoom != null && dest.zoom !== 0 ? { level: dest.zoom } : "keep"
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"
1641
1634
  };
1642
- }
1643
- case "fit": return { zoom: "fit" };
1644
- case "fitH": {
1645
- const hasTop = dest.top != null;
1646
- return {
1647
- rect: {
1648
- x: 0,
1649
- y: hasTop ? toContent(crop.left, dest.top).y : 0,
1650
- width: page.width,
1651
- height: 0
1652
- },
1653
- zoom: "fit-width",
1654
- anchor: { y: hasTop ? "start" : "keep" }
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"
1655
1640
  };
1656
- }
1657
- case "fitV": {
1658
- const hasLeft = dest.left != null;
1659
- return {
1660
- rect: {
1661
- x: hasLeft ? toContent(dest.left, crop.top).x : 0,
1662
- y: 0,
1663
- width: 0,
1664
- height: page.height
1665
- },
1666
- zoom: "fit-height",
1667
- anchor: { x: hasLeft ? "start" : "keep" }
1668
- };
1669
- }
1670
- case "fitR": {
1671
- const tl = toContent(dest.left, dest.top);
1672
- const br = toContent(dest.right, dest.bottom);
1673
- return {
1674
- rect: {
1675
- x: Math.min(tl.x, br.x),
1676
- y: Math.min(tl.y, br.y),
1677
- width: Math.abs(br.x - tl.x),
1678
- height: Math.abs(br.y - tl.y)
1679
- },
1680
- zoom: "fit"
1681
- };
1682
- }
1683
- case "fitB": return {
1684
- rect: box,
1685
- zoom: "fit"
1686
- };
1687
- case "fitBH": {
1688
- const hasTop = dest.top != null;
1689
- const y = hasTop ? toContent(crop.left, dest.top).y : box.y;
1690
- return {
1691
- rect: {
1692
- x: box.x,
1693
- y,
1694
- width: box.width,
1695
- height: 0
1696
- },
1697
- zoom: "fit-width",
1698
- anchor: { y: hasTop ? "start" : "keep" }
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"
1699
1652
  };
1700
- }
1701
- case "fitBV": {
1702
- const hasLeft = dest.left != null;
1703
- return {
1704
- rect: {
1705
- x: hasLeft ? toContent(dest.left, crop.top).x : box.x,
1706
- y: box.y,
1707
- width: 0,
1708
- height: box.height
1709
- },
1710
- zoom: "fit-height",
1711
- anchor: { x: hasLeft ? "start" : "keep" }
1653
+ const stage = ctx.tryGet(token);
1654
+ if (!stage) return {
1655
+ status: "failed",
1656
+ error: "no stage available"
1712
1657
  };
1713
- }
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
+ }));
1714
1677
  }
1715
- })();
1716
- return {
1717
- pageIndex: layout.index,
1718
- options
1719
- };
1720
- }
1678
+ });
1679
+ };
1721
1680
  //#endregion
1722
1681
  //#region src/scroll-handler.ts
1723
1682
  /**