pdfjs-reader-core 0.5.5 → 0.5.7

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
@@ -9191,7 +9191,7 @@ var init_DocumentContainer = __esm({
9191
9191
  const containerRef = (0, import_react37.useRef)(null);
9192
9192
  const documentRef = (0, import_react37.useRef)(null);
9193
9193
  const baseScaleRef = (0, import_react37.useRef)(scale);
9194
- const isTouchDevice = useIsTouchDevice();
9194
+ const isTouchDevice2 = useIsTouchDevice();
9195
9195
  const documentLoadingState = useViewerStore((s) => s.documentLoadingState);
9196
9196
  const { selection, clearSelection, copySelection } = useTextSelection();
9197
9197
  const handlePinchZoom = (0, import_react37.useCallback)(
@@ -9222,7 +9222,7 @@ var init_DocumentContainer = __esm({
9222
9222
  onSwipeLeft: handleSwipeLeft,
9223
9223
  onSwipeRight: handleSwipeRight,
9224
9224
  onDoubleTap: handleDoubleTap,
9225
- enabled: enableTouchGestures && isTouchDevice,
9225
+ enabled: enableTouchGestures && isTouchDevice2,
9226
9226
  swipeThreshold: 50,
9227
9227
  doubleTapInterval: 300
9228
9228
  });
@@ -9466,7 +9466,7 @@ var init_VirtualizedDocumentContainer = __esm({
9466
9466
  const pageCache = (0, import_react38.useRef)(/* @__PURE__ */ new Map());
9467
9467
  const pageDimensionsCache = (0, import_react38.useRef)(/* @__PURE__ */ new Map());
9468
9468
  const baseScaleRef = (0, import_react38.useRef)(scale);
9469
- const isTouchDevice = useIsTouchDevice();
9469
+ const isTouchDevice2 = useIsTouchDevice();
9470
9470
  const [visiblePages, setVisiblePages] = (0, import_react38.useState)([1]);
9471
9471
  const [pageObjects, setPageObjects] = (0, import_react38.useState)(/* @__PURE__ */ new Map());
9472
9472
  const [totalHeight, setTotalHeight] = (0, import_react38.useState)(0);
@@ -9669,7 +9669,7 @@ var init_VirtualizedDocumentContainer = __esm({
9669
9669
  onPinchZoom: handlePinchZoom,
9670
9670
  onSwipeLeft: nextPage,
9671
9671
  onSwipeRight: previousPage,
9672
- enabled: enableTouchGestures && isTouchDevice
9672
+ enabled: enableTouchGestures && isTouchDevice2
9673
9673
  });
9674
9674
  const setContainerRef = (0, import_react38.useCallback)(
9675
9675
  (element) => {
@@ -9877,7 +9877,7 @@ var init_DualPageContainer = __esm({
9877
9877
  const containerRef = (0, import_react40.useRef)(null);
9878
9878
  const documentRef = (0, import_react40.useRef)(null);
9879
9879
  const baseScaleRef = (0, import_react40.useRef)(scale);
9880
- const isTouchDevice = useIsTouchDevice();
9880
+ const isTouchDevice2 = useIsTouchDevice();
9881
9881
  const [leftPage, setLeftPage] = (0, import_react40.useState)(null);
9882
9882
  const [rightPage, setRightPage] = (0, import_react40.useState)(null);
9883
9883
  const [isLoading, setIsLoading] = (0, import_react40.useState)(false);
@@ -10020,7 +10020,7 @@ var init_DualPageContainer = __esm({
10020
10020
  onPinchZoom: handlePinchZoom,
10021
10021
  onSwipeLeft: goToNextSpread,
10022
10022
  onSwipeRight: goToPreviousSpread,
10023
- enabled: enableTouchGestures && isTouchDevice
10023
+ enabled: enableTouchGestures && isTouchDevice2
10024
10024
  });
10025
10025
  const setContainerRef = (0, import_react40.useCallback)(
10026
10026
  (element) => {
@@ -13559,12 +13559,23 @@ function AnimatedUnderline({ bbox, action }) {
13559
13559
  const blotX = x2 + 4;
13560
13560
  const blotY = y;
13561
13561
  const strokeWeight = action.style === "wavy" ? 3 : 4;
13562
+ const xPad = 8;
13563
+ const yAbove = 8;
13564
+ const yBelow = 24;
13565
+ const svgX = x1 - xPad;
13566
+ const svgY = y - yAbove;
13567
+ const svgW = x2 - x1 + 2 * xPad;
13568
+ const svgH = yAbove + yBelow;
13562
13569
  return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
13563
13570
  "svg",
13564
13571
  {
13572
+ width: svgW,
13573
+ height: svgH,
13574
+ viewBox: `${svgX} ${svgY} ${svgW} ${svgH}`,
13565
13575
  style: {
13566
13576
  position: "absolute",
13567
- inset: 0,
13577
+ left: svgX,
13578
+ top: svgY,
13568
13579
  pointerEvents: "none",
13569
13580
  overflow: "visible"
13570
13581
  },
@@ -13627,10 +13638,9 @@ function AnimatedUnderline({ bbox, action }) {
13627
13638
  }
13628
13639
 
13629
13640
  // src/components/TutorMode/AnimatedHighlight.tsx
13630
- var import_react56 = require("react");
13631
13641
  var import_framer_motion4 = require("framer-motion");
13632
13642
  var import_jsx_runtime44 = require("react/jsx-runtime");
13633
- var WASH = "rgba(230, 180, 34, 0.22)";
13643
+ var WASH = "rgba(195, 145, 10, 0.32)";
13634
13644
  function AnimatedHighlight({ bbox, action }) {
13635
13645
  const [x1, y1, x2, y2] = bbox;
13636
13646
  const h = Math.max(1, y2 - y1);
@@ -13638,7 +13648,6 @@ function AnimatedHighlight({ bbox, action }) {
13638
13648
  const yTop = y1 - bleed;
13639
13649
  const yBot = y2 + bleed;
13640
13650
  const duration = action.draw_duration_ms / 1e3;
13641
- const filterId = (0, import_react56.useId)();
13642
13651
  const isDefaultColour = !action.color || action.color === "rgba(250, 204, 21, 0.35)" || action.color === "rgba(250,204,21,0.35)";
13643
13652
  const fill = isDefaultColour ? WASH : action.color;
13644
13653
  const taper = Math.min(6, h * 0.2);
@@ -13653,49 +13662,42 @@ function AnimatedHighlight({ bbox, action }) {
13653
13662
  L ${x1 - 2} ${yBot - taper}
13654
13663
  Z
13655
13664
  `;
13656
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
13665
+ const pad = 8;
13666
+ const svgX = x1 - pad;
13667
+ const svgY = yTop - pad;
13668
+ const svgW = x2 - x1 + 2 * pad;
13669
+ const svgH = yBot - yTop + 2 * pad;
13670
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13657
13671
  "svg",
13658
13672
  {
13673
+ width: svgW,
13674
+ height: svgH,
13675
+ viewBox: `${svgX} ${svgY} ${svgW} ${svgH}`,
13659
13676
  style: {
13660
13677
  position: "absolute",
13661
- inset: 0,
13678
+ left: svgX,
13679
+ top: svgY,
13662
13680
  pointerEvents: "none",
13663
- overflow: "visible",
13664
- mixBlendMode: "multiply"
13681
+ overflow: "visible"
13665
13682
  },
13666
13683
  "data-role": "highlight",
13667
- children: [
13668
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("filter", { id: filterId, children: [
13669
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13670
- "feTurbulence",
13671
- {
13672
- type: "fractalNoise",
13673
- baseFrequency: "1.8",
13674
- numOctaves: "1",
13675
- seed: 3,
13676
- result: "noise"
13677
- }
13678
- ),
13679
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1 })
13680
- ] }) }),
13681
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13682
- import_framer_motion4.motion.path,
13683
- {
13684
- d: pathD,
13685
- fill,
13686
- initial: { clipPath: `inset(0 100% 0 0)` },
13687
- animate: { clipPath: `inset(0 0% 0 0)` },
13688
- exit: { opacity: 0 },
13689
- filter: `url(#${filterId})`,
13690
- transition: { duration, ease: EASE_OUT_EXPO }
13691
- }
13692
- )
13693
- ]
13684
+ children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13685
+ import_framer_motion4.motion.path,
13686
+ {
13687
+ d: pathD,
13688
+ fill,
13689
+ initial: { clipPath: `inset(0 100% 0 0)` },
13690
+ animate: { clipPath: `inset(0 0% 0 0)` },
13691
+ exit: { opacity: 0 },
13692
+ transition: { duration, ease: EASE_OUT_EXPO }
13693
+ }
13694
+ )
13694
13695
  }
13695
13696
  );
13696
13697
  }
13697
13698
 
13698
13699
  // src/components/TutorMode/PulseOverlay.tsx
13700
+ var import_react56 = require("react");
13699
13701
  var import_framer_motion5 = require("framer-motion");
13700
13702
  var import_jsx_runtime45 = require("react/jsx-runtime");
13701
13703
  var INTENSITY = {
@@ -13704,6 +13706,7 @@ var INTENSITY = {
13704
13706
  strong: { bracketLen: 26, strokeWeight: 3, coreOpacity: 1, ringScale: 1.22 }
13705
13707
  };
13706
13708
  function PulseOverlay({ bbox, action }) {
13709
+ const pulseId = (0, import_react56.useId)();
13707
13710
  const [x1, y1, x2, y2] = bbox;
13708
13711
  const w = Math.max(1, x2 - x1);
13709
13712
  const h = Math.max(1, y2 - y1);
@@ -13712,29 +13715,48 @@ function PulseOverlay({ bbox, action }) {
13712
13715
  const spec = INTENSITY[action.intensity] ?? INTENSITY.normal;
13713
13716
  const L = Math.min(spec.bracketLen, Math.min(w, h) / 2.5);
13714
13717
  const PAD = 6;
13718
+ const bracketExtent = PAD + L + 8;
13719
+ const glowExtent = (w / 2 + 24) * spec.ringScale - w / 2;
13720
+ const ringExtent = (w / 2 + PAD) * spec.ringScale - w / 2;
13721
+ const glowExtentV = (h / 2 + 24) * spec.ringScale - h / 2;
13722
+ const ringExtentV = (h / 2 + PAD) * spec.ringScale - h / 2;
13723
+ const svgPadX = Math.ceil(Math.max(bracketExtent, glowExtent, ringExtent) + 4);
13724
+ const svgPadY = Math.ceil(Math.max(bracketExtent, glowExtentV, ringExtentV) + 4);
13725
+ const svgX = x1 - svgPadX;
13726
+ const svgY = y1 - svgPadY;
13727
+ const svgW = w + 2 * svgPadX;
13728
+ const svgH = h + 2 * svgPadY;
13715
13729
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
13716
13730
  "svg",
13717
13731
  {
13732
+ width: svgW,
13733
+ height: svgH,
13734
+ viewBox: `${svgX} ${svgY} ${svgW} ${svgH}`,
13718
13735
  style: {
13719
13736
  position: "absolute",
13720
- inset: 0,
13737
+ left: svgX,
13738
+ top: svgY,
13721
13739
  pointerEvents: "none",
13722
13740
  overflow: "visible"
13723
13741
  },
13724
13742
  "data-role": "pulse",
13725
13743
  children: [
13744
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("radialGradient", { id: `${pulseId}-glow`, cx: "50%", cy: "50%", r: "50%", children: [
13745
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("stop", { offset: "0%", stopColor: ACCENT_GLOW, stopOpacity: 1 }),
13746
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("stop", { offset: "60%", stopColor: ACCENT_GLOW, stopOpacity: 0.45 }),
13747
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("stop", { offset: "100%", stopColor: ACCENT_GLOW, stopOpacity: 0 })
13748
+ ] }) }),
13726
13749
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13727
13750
  import_framer_motion5.motion.ellipse,
13728
13751
  {
13729
13752
  cx,
13730
13753
  cy,
13731
- rx: w / 2 + 10,
13732
- ry: h / 2 + 10,
13733
- fill: ACCENT_GLOW,
13754
+ rx: w / 2 + 24,
13755
+ ry: h / 2 + 24,
13756
+ fill: `url(#${pulseId}-glow)`,
13734
13757
  style: {
13735
13758
  transformOrigin: `${cx}px ${cy}px`,
13736
- transformBox: "fill-box",
13737
- filter: "blur(16px)"
13759
+ transformBox: "fill-box"
13738
13760
  },
13739
13761
  initial: { opacity: 0, scale: 0.95 },
13740
13762
  animate: {
@@ -13907,12 +13929,27 @@ function CalloutArrow({ fromBbox, toBbox, action }) {
13907
13929
  const glowId = `${markerId}-glow`;
13908
13930
  const { from, to } = edgePoints(fromBbox, toBbox);
13909
13931
  const d = arrowPath(from, to, action.curve);
13932
+ const rawMinX = Math.min(from.x, to.x);
13933
+ const rawMinY = Math.min(from.y, to.y);
13934
+ const rawMaxX = Math.max(from.x, to.x);
13935
+ const rawMaxY = Math.max(from.y, to.y);
13936
+ const dist = Math.hypot(to.x - from.x, to.y - from.y);
13937
+ const arcDev = action.curve === "curved" ? dist * 0.12 : 0;
13938
+ const basePad = 16;
13939
+ const svgX = rawMinX - basePad - arcDev;
13940
+ const svgY = rawMinY - basePad - arcDev;
13941
+ const svgW = rawMaxX - rawMinX + 2 * (basePad + arcDev);
13942
+ const svgH = rawMaxY - rawMinY + 2 * (basePad + arcDev);
13910
13943
  return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
13911
13944
  "svg",
13912
13945
  {
13946
+ width: svgW,
13947
+ height: svgH,
13948
+ viewBox: `${svgX} ${svgY} ${svgW} ${svgH}`,
13913
13949
  style: {
13914
13950
  position: "absolute",
13915
- inset: 0,
13951
+ left: svgX,
13952
+ top: svgY,
13916
13953
  pointerEvents: "none",
13917
13954
  overflow: "visible"
13918
13955
  },
@@ -14061,8 +14098,9 @@ function CinemaLayer({
14061
14098
  page,
14062
14099
  index,
14063
14100
  overlays,
14064
- scale
14101
+ coordScale
14065
14102
  }) {
14103
+ if (overlays.length === 0) return null;
14066
14104
  return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
14067
14105
  "div",
14068
14106
  {
@@ -14071,7 +14109,7 @@ function CinemaLayer({
14071
14109
  position: "absolute",
14072
14110
  inset: 0,
14073
14111
  transformOrigin: "0 0",
14074
- transform: `scale(${scale})`,
14112
+ transform: `scale(${coordScale})`,
14075
14113
  width: page.page_dimensions.width,
14076
14114
  height: page.page_dimensions.height,
14077
14115
  pointerEvents: "none",
@@ -14661,7 +14699,8 @@ function LabelOverlay({
14661
14699
  index,
14662
14700
  currentPage,
14663
14701
  camera,
14664
- viewport
14702
+ viewport,
14703
+ coordScale
14665
14704
  }) {
14666
14705
  const labels = overlays.filter((o) => o.kind === "label");
14667
14706
  const page = index.byPage.get(currentPage);
@@ -14688,7 +14727,8 @@ function LabelOverlay({
14688
14727
  a.position,
14689
14728
  page,
14690
14729
  camera,
14691
- viewport
14730
+ viewport,
14731
+ coordScale
14692
14732
  );
14693
14733
  return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
14694
14734
  StickyLabel,
@@ -14702,7 +14742,7 @@ function LabelOverlay({
14702
14742
  }
14703
14743
  );
14704
14744
  }
14705
- function computeScreenAnchor(bbox, where, page, camera, viewport) {
14745
+ function computeScreenAnchor(bbox, where, page, camera, viewport, coordScale) {
14706
14746
  const [x1, y1, x2, y2] = bbox;
14707
14747
  const pageCX = page.page_dimensions.width / 2;
14708
14748
  const pageCY = page.page_dimensions.height / 2;
@@ -14728,8 +14768,8 @@ function computeScreenAnchor(bbox, where, page, camera, viewport) {
14728
14768
  px = (x1 + x2) / 2;
14729
14769
  py = y1;
14730
14770
  }
14731
- const screenX = viewport.width / 2 + camera.x + (px - pageCX) * camera.scale;
14732
- const screenY = viewport.height / 2 + camera.y + (py - pageCY) * camera.scale;
14771
+ const screenX = viewport.width / 2 + camera.x + (px - pageCX) * coordScale * camera.scale;
14772
+ const screenY = viewport.height / 2 + camera.y + (py - pageCY) * coordScale * camera.scale;
14733
14773
  return { x: screenX, y: screenY };
14734
14774
  }
14735
14775
 
@@ -14741,7 +14781,8 @@ function CalloutLabelOverlay({
14741
14781
  index,
14742
14782
  currentPage,
14743
14783
  camera,
14744
- viewport
14784
+ viewport,
14785
+ coordScale
14745
14786
  }) {
14746
14787
  const callouts = overlays.filter(
14747
14788
  (o) => o.kind === "callout" && o.action.label
@@ -14770,7 +14811,8 @@ function CalloutLabelOverlay({
14770
14811
  toHit.block.bbox,
14771
14812
  page,
14772
14813
  camera,
14773
- viewport
14814
+ viewport,
14815
+ coordScale
14774
14816
  );
14775
14817
  return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
14776
14818
  CalloutLabelPill,
@@ -14785,7 +14827,7 @@ function CalloutLabelOverlay({
14785
14827
  }
14786
14828
  );
14787
14829
  }
14788
- function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14830
+ function computePillAnchor(fromBbox, toBbox, page, camera, viewport, coordScale) {
14789
14831
  const aCX = (fromBbox[0] + fromBbox[2]) / 2;
14790
14832
  const aCY = (fromBbox[1] + fromBbox[3]) / 2;
14791
14833
  const bCX = (toBbox[0] + toBbox[2]) / 2;
@@ -14802,8 +14844,8 @@ function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14802
14844
  const toY = bCY - uy * bOff;
14803
14845
  const pageCX = page.page_dimensions.width / 2;
14804
14846
  const pageCY = page.page_dimensions.height / 2;
14805
- const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * camera.scale;
14806
- const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * camera.scale;
14847
+ const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * coordScale * camera.scale;
14848
+ const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * coordScale * camera.scale;
14807
14849
  const isVertical = Math.abs(dy) >= Math.abs(dx);
14808
14850
  const OFFSET = resolvePillOffset(viewport.width);
14809
14851
  const MAX_PILL_W = resolveMaxPillW(viewport.width);
@@ -14946,6 +14988,41 @@ function SubtitleBar({ text }) {
14946
14988
  ) : null });
14947
14989
  }
14948
14990
 
14991
+ // src/utils/render-scale.ts
14992
+ function computeFitScale(input) {
14993
+ const { pageWidthPt, pageHeightPt, viewport, paddingFactor = 0.95 } = input;
14994
+ if (!Number.isFinite(pageWidthPt) || !Number.isFinite(pageHeightPt) || pageWidthPt <= 0 || pageHeightPt <= 0) {
14995
+ return 1;
14996
+ }
14997
+ const w = Math.max(1, viewport.width);
14998
+ const h = Math.max(1, viewport.height);
14999
+ const fit = Math.min(w / pageWidthPt, h / pageHeightPt) * paddingFactor;
15000
+ return Number.isFinite(fit) && fit > 0 ? fit : 1;
15001
+ }
15002
+ function computeCoordScale(input) {
15003
+ const { renderScale, dpi } = input;
15004
+ if (!Number.isFinite(renderScale) || renderScale <= 0) return 0;
15005
+ if (!Number.isFinite(dpi) || dpi <= 0) return 1;
15006
+ return renderScale * 72 / dpi;
15007
+ }
15008
+ function isTouchDevice() {
15009
+ if (typeof window === "undefined") return false;
15010
+ if (typeof window.matchMedia !== "function") return false;
15011
+ try {
15012
+ return window.matchMedia("(pointer: coarse)").matches;
15013
+ } catch {
15014
+ return false;
15015
+ }
15016
+ }
15017
+ function resolveRenderScale(input) {
15018
+ const { renderScaleProp, scaleProp, defaultRenderScale } = input;
15019
+ if (Number.isFinite(renderScaleProp) && renderScaleProp > 0) {
15020
+ return renderScaleProp;
15021
+ }
15022
+ const multiplier = Number.isFinite(scaleProp) && scaleProp > 0 ? scaleProp : 1;
15023
+ return defaultRenderScale * multiplier;
15024
+ }
15025
+
14949
15026
  // src/director/storyboard-engine.ts
14950
15027
  init_narration_store();
14951
15028
  init_camera_math();
@@ -15036,11 +15113,32 @@ var StoryboardEngine = class {
15036
15113
  this.pendingStepTimers.clear();
15037
15114
  this.deps.narrationStore.getState().setEngineStatus("idle");
15038
15115
  }
15039
- /** Cancel every removal timer (used by resetVisuals only). */
15116
+ /** Cancel every removal timer (used by resetVisuals and destroy). */
15040
15117
  cancelAllRemovalTimers() {
15041
15118
  for (const t of this.overlayRemovalTimers.values()) clearTimeout(t);
15042
15119
  this.overlayRemovalTimers.clear();
15043
15120
  }
15121
+ /**
15122
+ * Full destructor — cancels both pending step timers AND overlay removal
15123
+ * timers. Use this in component unmount/cleanup. The per-storyboard
15124
+ * `cancelPending()` deliberately leaves overlay removal timers alone so a
15125
+ * mid-flight overlay doesn't get stranded (see `overlayRemovalTimers` doc),
15126
+ * but at destruction time we must release every timer or their closures
15127
+ * keep `deps` (narrationStore, the full bboxIndex) alive beyond the
15128
+ * lifetime of this engine. On iOS Safari with many engine recreations
15129
+ * this causes cumulative memory pressure and eventual tab reload.
15130
+ */
15131
+ destroy() {
15132
+ this.cancelPending();
15133
+ this.cancelAllRemovalTimers();
15134
+ }
15135
+ /** Test-only — exposes internal queue sizes for regression tests. */
15136
+ _queueSizesForTest() {
15137
+ return {
15138
+ pending: this.pendingStepTimers.size,
15139
+ removals: this.overlayRemovalTimers.size
15140
+ };
15141
+ }
15044
15142
  /** Reset visuals: clear overlays, cancel every removal timer, fit camera. */
15045
15143
  resetVisuals() {
15046
15144
  this.cancelPending();
@@ -16181,6 +16279,7 @@ function TutorModeContainer({
16181
16279
  loadingComponent,
16182
16280
  onPageChange,
16183
16281
  storyboardProvider,
16282
+ renderScale,
16184
16283
  className
16185
16284
  }) {
16186
16285
  const containerRef = (0, import_react58.useRef)(null);
@@ -16195,6 +16294,27 @@ function TutorModeContainer({
16195
16294
  const [viewport, setViewport] = (0, import_react58.useState)({ width: 800, height: 1e3 });
16196
16295
  const camera = (0, import_zustand2.useStore)(narrationStore, (s) => s.camera);
16197
16296
  const activeOverlays = (0, import_zustand2.useStore)(narrationStore, (s) => s.activeOverlays);
16297
+ const page = index.byPage.get(pageNumber);
16298
+ const pagePointsW = page ? page.page_dimensions.width * 72 / page.page_dimensions.dpi : 0;
16299
+ const pagePointsH = page ? page.page_dimensions.height * 72 / page.page_dimensions.dpi : 0;
16300
+ const touch = isTouchDevice();
16301
+ const defaultRenderScale = page ? touch ? computeFitScale({
16302
+ pageWidthPt: pagePointsW,
16303
+ pageHeightPt: pagePointsH,
16304
+ viewport
16305
+ }) : page.page_dimensions.dpi / 72 : 1;
16306
+ const effectiveRenderScale = resolveRenderScale({
16307
+ renderScaleProp: renderScale,
16308
+ scaleProp: scale,
16309
+ defaultRenderScale
16310
+ });
16311
+ const coordScale = page ? computeCoordScale({
16312
+ renderScale: effectiveRenderScale,
16313
+ dpi: page.page_dimensions.dpi
16314
+ }) : 1;
16315
+ const rasterScale = effectiveRenderScale;
16316
+ const baseW = pagePointsW * effectiveRenderScale;
16317
+ const baseH = pagePointsH * effectiveRenderScale;
16198
16318
  (0, import_react58.useEffect)(() => {
16199
16319
  if (numPages <= 0) return;
16200
16320
  if (pageNumber < 1 || pageNumber > numPages) return;
@@ -16210,7 +16330,13 @@ function TutorModeContainer({
16210
16330
  (0, import_react58.useEffect)(() => {
16211
16331
  if (!containerRef.current) return;
16212
16332
  const el = containerRef.current;
16213
- const update = () => setViewport({ width: el.clientWidth, height: el.clientHeight });
16333
+ const update = () => {
16334
+ const w = el.clientWidth;
16335
+ const h = el.clientHeight;
16336
+ setViewport(
16337
+ (prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h }
16338
+ );
16339
+ };
16214
16340
  update();
16215
16341
  const ro = new ResizeObserver(update);
16216
16342
  ro.observe(el);
@@ -16238,23 +16364,29 @@ function TutorModeContainer({
16238
16364
  const page2 = index.byPage.get(pageNumber);
16239
16365
  if (!page2) return;
16240
16366
  if (viewport.width === 0 || viewport.height === 0) return;
16367
+ if (baseW === 0 || baseH === 0) return;
16241
16368
  if (narrationStore.getState().activeOverlays.length > 0) return;
16242
- const fit = Math.min(
16243
- viewport.width / page2.page_dimensions.width,
16244
- viewport.height / page2.page_dimensions.height
16245
- ) * 0.95;
16369
+ const fit = Math.min(viewport.width / baseW, viewport.height / baseH) * 0.95;
16246
16370
  narrationStore.getState().setCamera({ scale: fit, x: 0, y: 0 });
16247
- }, [pageNumber, viewport, index, narrationStore]);
16371
+ }, [pageNumber, viewport, index, narrationStore, baseW, baseH]);
16372
+ const viewportRef = (0, import_react58.useRef)(viewport);
16373
+ (0, import_react58.useEffect)(() => {
16374
+ viewportRef.current = viewport;
16375
+ }, [viewport]);
16248
16376
  const engineRef = (0, import_react58.useRef)(null);
16249
16377
  (0, import_react58.useEffect)(() => {
16250
- engineRef.current = new StoryboardEngine({
16378
+ const engine = new StoryboardEngine({
16251
16379
  narrationStore,
16252
16380
  bboxIndex: index,
16253
- getViewport: () => viewport,
16381
+ getViewport: () => viewportRef.current,
16254
16382
  minOverlayDurationMs
16255
16383
  });
16256
- return () => engineRef.current?.cancelPending();
16257
- }, [narrationStore, index, viewport, minOverlayDurationMs]);
16384
+ engineRef.current = engine;
16385
+ return () => {
16386
+ engine.destroy();
16387
+ if (engineRef.current === engine) engineRef.current = null;
16388
+ };
16389
+ }, [narrationStore, index, minOverlayDurationMs]);
16258
16390
  const abortRef = (0, import_react58.useRef)(null);
16259
16391
  const debounceRef = (0, import_react58.useRef)(null);
16260
16392
  const lastChunkRef = (0, import_react58.useRef)(null);
@@ -16435,11 +16567,6 @@ function TutorModeContainer({
16435
16567
  }, idleTimeoutMs + 100);
16436
16568
  return () => clearTimeout(t);
16437
16569
  }, [currentChunk, idleTimeoutMs, narrationStore]);
16438
- const page = index.byPage.get(pageNumber);
16439
- const dpiScale = page ? page.page_dimensions.dpi / 72 : 1;
16440
- const rasterScale = dpiScale * (scale || 1);
16441
- const baseW = page ? page.page_dimensions.width * (scale || 1) : 0;
16442
- const baseH = page ? page.page_dimensions.height * (scale || 1) : 0;
16443
16570
  const isReady = !!page && !!pageProxy;
16444
16571
  return /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(
16445
16572
  "div",
@@ -16518,7 +16645,7 @@ function TutorModeContainer({
16518
16645
  page,
16519
16646
  index,
16520
16647
  overlays: activeOverlays,
16521
- scale: scale || 1
16648
+ coordScale
16522
16649
  }
16523
16650
  )
16524
16651
  ]
@@ -16531,7 +16658,8 @@ function TutorModeContainer({
16531
16658
  index,
16532
16659
  currentPage: pageNumber,
16533
16660
  camera,
16534
- viewport
16661
+ viewport,
16662
+ coordScale
16535
16663
  }
16536
16664
  ),
16537
16665
  /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
@@ -16541,7 +16669,8 @@ function TutorModeContainer({
16541
16669
  index,
16542
16670
  currentPage: pageNumber,
16543
16671
  camera,
16544
- viewport
16672
+ viewport,
16673
+ coordScale
16545
16674
  }
16546
16675
  ),
16547
16676
  /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(GhostReferenceOverlay, { overlays: activeOverlays, index })