@embedpdf/plugin-stage 3.0.0-next.10 → 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/index.cjs CHANGED
@@ -21,7 +21,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
+ const require_destination = require("./destination.cjs");
25
+ const require_contract = require("./contract-BU33R4oL.cjs");
24
26
  let _embedpdf_core = require("@embedpdf/core");
27
+ let _embedpdf_plugin_actions_contract = require("@embedpdf/plugin-actions/contract");
28
+ let _embedpdf_plugin_actions_contract_host = require("@embedpdf/plugin-actions/contract/host");
25
29
  let _embedpdf_core_stage = require("@embedpdf/core-stage");
26
30
  _embedpdf_core_stage = __toESM(_embedpdf_core_stage, 1);
27
31
  let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
@@ -854,7 +858,16 @@ function createStageCapability(ctx, config = {}) {
854
858
  if (isFitAll()) goToTarget(ctx.getState().cursor, { behavior: "instant" });
855
859
  else applyAnchor(anchor, align);
856
860
  };
861
+ /** Tag what drives the NEXT camera/cursor change — dispatched on flips
862
+ * only, read by the page-state feed (see StageState.motionCause). */
863
+ const markCause = (cause) => {
864
+ if (ctx.getState().motionCause !== cause) ctx.dispatch({
865
+ type: "MOTION_CAUSE",
866
+ cause
867
+ });
868
+ };
857
869
  const goToTarget = (pageIndex, opts) => {
870
+ markCause("programmatic");
858
871
  cancelAnim();
859
872
  const doc = ctx.document();
860
873
  if (!doc || doc.pageCount === 0) return;
@@ -1232,11 +1245,13 @@ function createStageCapability(ctx, config = {}) {
1232
1245
  });
1233
1246
  },
1234
1247
  setCamera: (c) => {
1248
+ markCause("user");
1235
1249
  cancelAnim();
1236
1250
  setCam(c);
1237
1251
  syncCursorFromCamera();
1238
1252
  },
1239
1253
  panBy: (dx, dy) => {
1254
+ markCause("user");
1240
1255
  cancelAnim();
1241
1256
  if (gestureDepth > 0 && gestureElastic) {
1242
1257
  gestureRaw = _embedpdf_core_stage.panByScreen(gestureRaw ?? unrubberize(cam()), dx, dy);
@@ -1245,6 +1260,7 @@ function createStageCapability(ctx, config = {}) {
1245
1260
  syncCursorFromCamera();
1246
1261
  },
1247
1262
  scrollTo: (opts) => {
1263
+ markCause("user");
1248
1264
  cancelAnim();
1249
1265
  if (!buildScene().itemCount) return;
1250
1266
  const next = _embedpdf_core_stage.cameraFromScroll(cam(), stayBounds(), vp(), pad(), opts);
@@ -1347,6 +1363,7 @@ function createStageCapability(ctx, config = {}) {
1347
1363
  },
1348
1364
  goToPage: (pageIndex, opts) => goToTarget(pageIndex, opts),
1349
1365
  reveal: (pageIndex, opts) => {
1366
+ markCause("programmatic");
1350
1367
  const doc = ctx.document();
1351
1368
  if (!doc || doc.pageCount === 0) return;
1352
1369
  const target = Math.max(0, Math.min(pageIndex, doc.pageCount - 1));
@@ -1488,22 +1505,51 @@ function createStageCapability(ctx, config = {}) {
1488
1505
  //#endregion
1489
1506
  //#region src/effects.ts
1490
1507
  /**
1491
- * The one effect a stage lens needs: when the document's page registry changes
1492
- * (rotate/move/delete bump `DocumentMeta.revision`), the page geometry under
1493
- * this lens just changed, so re-fit — re-resolve the active zoom intent and
1494
- * re-place against the new footprint.
1508
+ * Stage lens effects.
1495
1509
  *
1496
- * Without it, fit/pixel zoom modes stay anchored to the pre-mutation dimensions:
1497
- * `pageWidth: 110` renders wider after a 90° rotation (width↔height swap), and
1498
- * `fitPage` over- or under-fills. The scene already re-keys on the revision, so
1499
- * only the resolved `cam.zoom` was stale; `refit()` recomputes it.
1510
+ * 1. Re-fit on page-registry mutations: when rotate/move/delete bump
1511
+ * `DocumentMeta.revision`, the page geometry under this lens changed, so
1512
+ * re-resolve the active zoom intent and re-place against the new
1513
+ * footprint. Steady-state only — it reacts to revision CHANGES, never the
1514
+ * initial value, so it can't race the capability's level-triggered
1515
+ * initial placement.
1500
1516
  *
1501
- * Steady-state only: this reacts to revision CHANGES, never the initial value,
1502
- * so it can't race the capability's level-triggered initial placement (the
1503
- * reason the rest of the plugin is effect-free).
1517
+ * 2. The page-state feed (main lens only, actions plugin present): stage is
1518
+ * AUTHORITATIVE for what page the viewer is on; the action engine's
1519
+ * lifecycle coordinator owns WHEN page-lifecycle triggers fire. This feed
1520
+ * pushes `{ currentPon, visiblePons, placed, cause }` snapshots — pons,
1521
+ * not indexes (reorder-safe) — on placement and on every change; the
1522
+ * coordinator buffers them behind the document-open barrier and diffs
1523
+ * against its last-emitted state, so this side stays a dumb reporter.
1504
1524
  */
1505
- function registerStageEffects(ctx, token) {
1525
+ function registerStageEffects(ctx, token, feedActions = false) {
1506
1526
  ctx.watch(() => ctx.document()?.revision ?? 0, () => ctx.get(token).refit());
1527
+ if (!feedActions) return;
1528
+ const actions = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
1529
+ if (!actions) return;
1530
+ const snapshot = () => {
1531
+ const state = ctx.getState();
1532
+ if (!state.placed) return null;
1533
+ const stage = ctx.get(token);
1534
+ return {
1535
+ currentPon: stage.pages()[state.cursor]?.pon ?? null,
1536
+ visiblePons: stage.visiblePages().map((page) => page.pon),
1537
+ cause: state.motionCause
1538
+ };
1539
+ };
1540
+ ctx.watch(() => {
1541
+ const s = snapshot();
1542
+ return s === null ? "unplaced" : `${s.currentPon ?? -1}|${[...s.visiblePons].sort((a, b) => a - b).join(",")}`;
1543
+ }, () => {
1544
+ const s = snapshot();
1545
+ if (s === null) return;
1546
+ actions.reportPageState({
1547
+ currentPon: s.currentPon,
1548
+ visiblePons: s.visiblePons,
1549
+ placed: true,
1550
+ cause: s.cause
1551
+ });
1552
+ });
1507
1553
  }
1508
1554
  //#endregion
1509
1555
  //#region src/reducer.ts
@@ -1523,6 +1569,7 @@ const initialStageState = (config) => {
1523
1569
  },
1524
1570
  dpr: 1,
1525
1571
  cursor: 0,
1572
+ motionCause: "user",
1526
1573
  activeRules: [],
1527
1574
  ...DEFAULT_SETTINGS,
1528
1575
  ...overrides
@@ -1569,6 +1616,10 @@ const stageReducer = (state, a) => {
1569
1616
  ...state,
1570
1617
  cursor: a.cursor
1571
1618
  };
1619
+ case "MOTION_CAUSE": return {
1620
+ ...state,
1621
+ motionCause: a.cause
1622
+ };
1572
1623
  case "PATCH": return applyPatch(state, a.patch);
1573
1624
  case "RESPONSIVE": return {
1574
1625
  ...state,
@@ -1578,9 +1629,6 @@ const stageReducer = (state, a) => {
1578
1629
  }
1579
1630
  };
1580
1631
  //#endregion
1581
- //#region src/types.ts
1582
- const StageToken = (0, _embedpdf_core.createCapabilityToken)("stage");
1583
- //#endregion
1584
1632
  //#region src/stage.plugin.ts
1585
1633
  /**
1586
1634
  * Wires the parts into a kernel plugin. This file is the "manifest": it says what
@@ -1588,159 +1636,70 @@ const StageToken = (0, _embedpdf_core.createCapabilityToken)("stage");
1588
1636
  * sibling files.
1589
1637
  */
1590
1638
  const stagePlugin = (options = {}) => {
1591
- const { id = "stage", token = StageToken, ...config } = options;
1639
+ const { id = "stage", token = require_contract.StageToken, ...config } = options;
1592
1640
  return (0, _embedpdf_core.definePlugin)({
1593
1641
  id,
1594
1642
  token,
1595
1643
  scope: "document",
1644
+ optional: [_embedpdf_plugin_actions_contract.ActionsToken],
1596
1645
  initialState: () => initialStageState(config),
1597
1646
  reduce: stageReducer,
1598
1647
  capability: (ctx) => createStageCapability(ctx, config),
1599
- effects: (ctx) => registerStageEffects(ctx, token)
1600
- });
1601
- };
1602
- //#endregion
1603
- //#region src/destination.ts
1604
- /**
1605
- * PDF destination → reveal: the pure translator between the PDF protocol's
1606
- * navigation vocabulary (ISO 32000-1 §12.3.2.2) and the Stage's arrival
1607
- * primitive. Outline clicks, link annotations, and `/OpenAction` all
1608
- * resolve (engine-side) to an explicit `PdfDestination`; this maps it onto
1609
- * ONE `reveal(pageIndex, options)` call — no destination-specific camera
1610
- * code exists anywhere else.
1611
- *
1612
- * The whole protocol collapses onto three knobs:
1613
- * rect — what to look at (a point for /XYZ, a rect for /FitR, the
1614
- * page for /Fit, the content bounding box for /FitB*)
1615
- * zoom — 'keep' | {level} | 'fit' | 'fit-width' | 'fit-height'
1616
- * anchor — where it lands; 'keep' encodes the spec's null-means-retain
1617
- *
1618
- * Coordinates convert PDF user space (y-up, absolute) → content space
1619
- * (y-down, crop-relative) through the SAME `pdfToContent` matrix selection
1620
- * and search use, so a destination and an overlay can never disagree.
1621
- */
1622
- /**
1623
- * Translate one explicit destination against its target page's layout.
1624
- * `bbox` is the page's content BOUNDING box in CONTENT space, for the
1625
- * `/FitB*` kinds — pass it when known; the crop box is the spec-tolerated
1626
- * fallback until the engine exposes it.
1627
- */
1628
- function destinationToReveal(dest, layout, bbox) {
1629
- const geo = (0, _embedpdf_core_geometry.pageGeometry)({
1630
- crop: layout.boxes.crop,
1631
- rotation: layout.rotation,
1632
- userUnit: layout.userUnit
1633
- }, 1);
1634
- const crop = layout.boxes.crop;
1635
- const toContent = (x, y) => (0, _embedpdf_core_geometry.applyPoint)(geo.pdfToContent, {
1636
- x,
1637
- y
1638
- });
1639
- const page = {
1640
- x: 0,
1641
- y: 0,
1642
- width: layout.size.width,
1643
- height: layout.size.height
1644
- };
1645
- const box = bbox ?? page;
1646
- const options = (() => {
1647
- switch (dest.kind) {
1648
- case "xyz": {
1649
- const hasLeft = dest.left != null;
1650
- const hasTop = dest.top != null;
1651
- const p = toContent(dest.left ?? crop.left, dest.top ?? crop.top);
1652
- return {
1653
- rect: {
1654
- x: p.x,
1655
- y: p.y,
1656
- width: 0,
1657
- height: 0
1658
- },
1659
- anchor: {
1660
- x: hasLeft ? "start" : "keep",
1661
- y: hasTop ? "start" : "keep"
1662
- },
1663
- zoom: dest.zoom != null && dest.zoom !== 0 ? { level: dest.zoom } : "keep"
1648
+ effects: (ctx) => registerStageEffects(ctx, token, id === "stage"),
1649
+ init: (ctx) => {
1650
+ if (id !== "stage") return;
1651
+ const actions = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
1652
+ if (!actions) return;
1653
+ ctx.cleanup(actions.registerExecutor("goto", (node) => {
1654
+ if (node.type !== "goto") return {
1655
+ status: "inert",
1656
+ reason: "not a goto node"
1664
1657
  };
1665
- }
1666
- case "fit": return { zoom: "fit" };
1667
- case "fitH": {
1668
- const hasTop = dest.top != null;
1669
- return {
1670
- rect: {
1671
- x: 0,
1672
- y: hasTop ? toContent(crop.left, dest.top).y : 0,
1673
- width: page.width,
1674
- height: 0
1675
- },
1676
- zoom: "fit-width",
1677
- anchor: { y: hasTop ? "start" : "keep" }
1658
+ const stage = ctx.tryGet(token);
1659
+ const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === node.destination.pageObjectNumber);
1660
+ if (!stage || !layout) return {
1661
+ status: "failed",
1662
+ error: "no stage or destination page available"
1678
1663
  };
1679
- }
1680
- case "fitV": {
1681
- const hasLeft = dest.left != null;
1682
- return {
1683
- rect: {
1684
- x: hasLeft ? toContent(dest.left, crop.top).x : 0,
1685
- y: 0,
1686
- width: 0,
1687
- height: page.height
1688
- },
1689
- zoom: "fit-height",
1690
- anchor: { x: hasLeft ? "start" : "keep" }
1691
- };
1692
- }
1693
- case "fitR": {
1694
- const tl = toContent(dest.left, dest.top);
1695
- const br = toContent(dest.right, dest.bottom);
1696
- return {
1697
- rect: {
1698
- x: Math.min(tl.x, br.x),
1699
- y: Math.min(tl.y, br.y),
1700
- width: Math.abs(br.x - tl.x),
1701
- height: Math.abs(br.y - tl.y)
1702
- },
1703
- zoom: "fit"
1704
- };
1705
- }
1706
- case "fitB": return {
1707
- rect: box,
1708
- zoom: "fit"
1709
- };
1710
- case "fitBH": {
1711
- const hasTop = dest.top != null;
1712
- const y = hasTop ? toContent(crop.left, dest.top).y : box.y;
1713
- return {
1714
- rect: {
1715
- x: box.x,
1716
- y,
1717
- width: box.width,
1718
- height: 0
1719
- },
1720
- zoom: "fit-width",
1721
- anchor: { y: hasTop ? "start" : "keep" }
1664
+ const { pageIndex, options: reveal } = require_destination.destinationToReveal(node.destination, layout);
1665
+ stage.reveal(pageIndex, {
1666
+ ...reveal,
1667
+ behavior: "smooth"
1668
+ });
1669
+ return { status: "executed" };
1670
+ }));
1671
+ ctx.cleanup(actions.registerExecutor("named", (node) => {
1672
+ if (node.type !== "named") return {
1673
+ status: "inert",
1674
+ reason: "not a named node"
1722
1675
  };
1723
- }
1724
- case "fitBV": {
1725
- const hasLeft = dest.left != null;
1726
- return {
1727
- rect: {
1728
- x: hasLeft ? toContent(dest.left, crop.top).x : box.x,
1729
- y: box.y,
1730
- width: 0,
1731
- height: box.height
1732
- },
1733
- zoom: "fit-height",
1734
- anchor: { x: hasLeft ? "start" : "keep" }
1676
+ const stage = ctx.tryGet(token);
1677
+ if (!stage) return {
1678
+ status: "failed",
1679
+ error: "no stage available"
1735
1680
  };
1736
- }
1681
+ switch (node.name) {
1682
+ case "NextPage":
1683
+ stage.next({ behavior: "smooth" });
1684
+ return { status: "executed" };
1685
+ case "PrevPage":
1686
+ stage.prev({ behavior: "smooth" });
1687
+ return { status: "executed" };
1688
+ case "FirstPage":
1689
+ stage.goToPage(0, { behavior: "smooth" });
1690
+ return { status: "executed" };
1691
+ case "LastPage":
1692
+ stage.goToPage(Math.max(0, stage.pageCount() - 1), { behavior: "smooth" });
1693
+ return { status: "executed" };
1694
+ default: return {
1695
+ status: "inert",
1696
+ reason: `unknown named action '${node.name}'`
1697
+ };
1698
+ }
1699
+ }));
1737
1700
  }
1738
- })();
1739
- return {
1740
- pageIndex: layout.index,
1741
- options
1742
- };
1743
- }
1701
+ });
1702
+ };
1744
1703
  //#endregion
1745
1704
  //#region src/scroll-handler.ts
1746
1705
  /**
@@ -1784,7 +1743,7 @@ function createScrollHandler(stage, interaction, options = {}) {
1784
1743
  //#endregion
1785
1744
  exports.DEFAULT_RESPONSIVE = DEFAULT_RESPONSIVE;
1786
1745
  exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
1787
- exports.StageToken = StageToken;
1746
+ exports.StageToken = require_contract.StageToken;
1788
1747
  Object.defineProperty(exports, "ZoomMode", {
1789
1748
  enumerable: true,
1790
1749
  get: function() {
@@ -1793,7 +1752,7 @@ Object.defineProperty(exports, "ZoomMode", {
1793
1752
  });
1794
1753
  exports.boxOf = boxOf;
1795
1754
  exports.createScrollHandler = createScrollHandler;
1796
- exports.destinationToReveal = destinationToReveal;
1755
+ exports.destinationToReveal = require_destination.destinationToReveal;
1797
1756
  exports.matchesQuery = matchesQuery;
1798
1757
  exports.resolveResponsive = resolveResponsive;
1799
1758
  exports.settingsEqual = settingsEqual;