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.js CHANGED
@@ -9154,7 +9154,7 @@ var init_DocumentContainer = __esm({
9154
9154
  const containerRef = useRef16(null);
9155
9155
  const documentRef = useRef16(null);
9156
9156
  const baseScaleRef = useRef16(scale);
9157
- const isTouchDevice = useIsTouchDevice();
9157
+ const isTouchDevice2 = useIsTouchDevice();
9158
9158
  const documentLoadingState = useViewerStore((s) => s.documentLoadingState);
9159
9159
  const { selection, clearSelection, copySelection } = useTextSelection();
9160
9160
  const handlePinchZoom = useCallback29(
@@ -9185,7 +9185,7 @@ var init_DocumentContainer = __esm({
9185
9185
  onSwipeLeft: handleSwipeLeft,
9186
9186
  onSwipeRight: handleSwipeRight,
9187
9187
  onDoubleTap: handleDoubleTap,
9188
- enabled: enableTouchGestures && isTouchDevice,
9188
+ enabled: enableTouchGestures && isTouchDevice2,
9189
9189
  swipeThreshold: 50,
9190
9190
  doubleTapInterval: 300
9191
9191
  });
@@ -9429,7 +9429,7 @@ var init_VirtualizedDocumentContainer = __esm({
9429
9429
  const pageCache = useRef17(/* @__PURE__ */ new Map());
9430
9430
  const pageDimensionsCache = useRef17(/* @__PURE__ */ new Map());
9431
9431
  const baseScaleRef = useRef17(scale);
9432
- const isTouchDevice = useIsTouchDevice();
9432
+ const isTouchDevice2 = useIsTouchDevice();
9433
9433
  const [visiblePages, setVisiblePages] = useState19([1]);
9434
9434
  const [pageObjects, setPageObjects] = useState19(/* @__PURE__ */ new Map());
9435
9435
  const [totalHeight, setTotalHeight] = useState19(0);
@@ -9632,7 +9632,7 @@ var init_VirtualizedDocumentContainer = __esm({
9632
9632
  onPinchZoom: handlePinchZoom,
9633
9633
  onSwipeLeft: nextPage,
9634
9634
  onSwipeRight: previousPage,
9635
- enabled: enableTouchGestures && isTouchDevice
9635
+ enabled: enableTouchGestures && isTouchDevice2
9636
9636
  });
9637
9637
  const setContainerRef = useCallback30(
9638
9638
  (element) => {
@@ -9840,7 +9840,7 @@ var init_DualPageContainer = __esm({
9840
9840
  const containerRef = useRef18(null);
9841
9841
  const documentRef = useRef18(null);
9842
9842
  const baseScaleRef = useRef18(scale);
9843
- const isTouchDevice = useIsTouchDevice();
9843
+ const isTouchDevice2 = useIsTouchDevice();
9844
9844
  const [leftPage, setLeftPage] = useState20(null);
9845
9845
  const [rightPage, setRightPage] = useState20(null);
9846
9846
  const [isLoading, setIsLoading] = useState20(false);
@@ -9983,7 +9983,7 @@ var init_DualPageContainer = __esm({
9983
9983
  onPinchZoom: handlePinchZoom,
9984
9984
  onSwipeLeft: goToNextSpread,
9985
9985
  onSwipeRight: goToPreviousSpread,
9986
- enabled: enableTouchGestures && isTouchDevice
9986
+ enabled: enableTouchGestures && isTouchDevice2
9987
9987
  });
9988
9988
  const setContainerRef = useCallback31(
9989
9989
  (element) => {
@@ -13374,12 +13374,23 @@ function AnimatedUnderline({ bbox, action }) {
13374
13374
  const blotX = x2 + 4;
13375
13375
  const blotY = y;
13376
13376
  const strokeWeight = action.style === "wavy" ? 3 : 4;
13377
+ const xPad = 8;
13378
+ const yAbove = 8;
13379
+ const yBelow = 24;
13380
+ const svgX = x1 - xPad;
13381
+ const svgY = y - yAbove;
13382
+ const svgW = x2 - x1 + 2 * xPad;
13383
+ const svgH = yAbove + yBelow;
13377
13384
  return /* @__PURE__ */ jsxs36(
13378
13385
  "svg",
13379
13386
  {
13387
+ width: svgW,
13388
+ height: svgH,
13389
+ viewBox: `${svgX} ${svgY} ${svgW} ${svgH}`,
13380
13390
  style: {
13381
13391
  position: "absolute",
13382
- inset: 0,
13392
+ left: svgX,
13393
+ top: svgY,
13383
13394
  pointerEvents: "none",
13384
13395
  overflow: "visible"
13385
13396
  },
@@ -13442,10 +13453,9 @@ function AnimatedUnderline({ bbox, action }) {
13442
13453
  }
13443
13454
 
13444
13455
  // src/components/TutorMode/AnimatedHighlight.tsx
13445
- import { useId as useId2 } from "react";
13446
13456
  import { motion as motion4 } from "framer-motion";
13447
- import { jsx as jsx44, jsxs as jsxs37 } from "react/jsx-runtime";
13448
- var WASH = "rgba(230, 180, 34, 0.22)";
13457
+ import { jsx as jsx44 } from "react/jsx-runtime";
13458
+ var WASH = "rgba(195, 145, 10, 0.32)";
13449
13459
  function AnimatedHighlight({ bbox, action }) {
13450
13460
  const [x1, y1, x2, y2] = bbox;
13451
13461
  const h = Math.max(1, y2 - y1);
@@ -13453,7 +13463,6 @@ function AnimatedHighlight({ bbox, action }) {
13453
13463
  const yTop = y1 - bleed;
13454
13464
  const yBot = y2 + bleed;
13455
13465
  const duration = action.draw_duration_ms / 1e3;
13456
- const filterId = useId2();
13457
13466
  const isDefaultColour = !action.color || action.color === "rgba(250, 204, 21, 0.35)" || action.color === "rgba(250,204,21,0.35)";
13458
13467
  const fill = isDefaultColour ? WASH : action.color;
13459
13468
  const taper = Math.min(6, h * 0.2);
@@ -13468,57 +13477,51 @@ function AnimatedHighlight({ bbox, action }) {
13468
13477
  L ${x1 - 2} ${yBot - taper}
13469
13478
  Z
13470
13479
  `;
13471
- return /* @__PURE__ */ jsxs37(
13480
+ const pad = 8;
13481
+ const svgX = x1 - pad;
13482
+ const svgY = yTop - pad;
13483
+ const svgW = x2 - x1 + 2 * pad;
13484
+ const svgH = yBot - yTop + 2 * pad;
13485
+ return /* @__PURE__ */ jsx44(
13472
13486
  "svg",
13473
13487
  {
13488
+ width: svgW,
13489
+ height: svgH,
13490
+ viewBox: `${svgX} ${svgY} ${svgW} ${svgH}`,
13474
13491
  style: {
13475
13492
  position: "absolute",
13476
- inset: 0,
13493
+ left: svgX,
13494
+ top: svgY,
13477
13495
  pointerEvents: "none",
13478
- overflow: "visible",
13479
- mixBlendMode: "multiply"
13496
+ overflow: "visible"
13480
13497
  },
13481
13498
  "data-role": "highlight",
13482
- children: [
13483
- /* @__PURE__ */ jsx44("defs", { children: /* @__PURE__ */ jsxs37("filter", { id: filterId, children: [
13484
- /* @__PURE__ */ jsx44(
13485
- "feTurbulence",
13486
- {
13487
- type: "fractalNoise",
13488
- baseFrequency: "1.8",
13489
- numOctaves: "1",
13490
- seed: 3,
13491
- result: "noise"
13492
- }
13493
- ),
13494
- /* @__PURE__ */ jsx44("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1 })
13495
- ] }) }),
13496
- /* @__PURE__ */ jsx44(
13497
- motion4.path,
13498
- {
13499
- d: pathD,
13500
- fill,
13501
- initial: { clipPath: `inset(0 100% 0 0)` },
13502
- animate: { clipPath: `inset(0 0% 0 0)` },
13503
- exit: { opacity: 0 },
13504
- filter: `url(#${filterId})`,
13505
- transition: { duration, ease: EASE_OUT_EXPO }
13506
- }
13507
- )
13508
- ]
13499
+ children: /* @__PURE__ */ jsx44(
13500
+ motion4.path,
13501
+ {
13502
+ d: pathD,
13503
+ fill,
13504
+ initial: { clipPath: `inset(0 100% 0 0)` },
13505
+ animate: { clipPath: `inset(0 0% 0 0)` },
13506
+ exit: { opacity: 0 },
13507
+ transition: { duration, ease: EASE_OUT_EXPO }
13508
+ }
13509
+ )
13509
13510
  }
13510
13511
  );
13511
13512
  }
13512
13513
 
13513
13514
  // src/components/TutorMode/PulseOverlay.tsx
13515
+ import { useId as useId2 } from "react";
13514
13516
  import { motion as motion5 } from "framer-motion";
13515
- import { jsx as jsx45, jsxs as jsxs38 } from "react/jsx-runtime";
13517
+ import { jsx as jsx45, jsxs as jsxs37 } from "react/jsx-runtime";
13516
13518
  var INTENSITY = {
13517
13519
  subtle: { bracketLen: 14, strokeWeight: 2, coreOpacity: 0.5, ringScale: 1.08 },
13518
13520
  normal: { bracketLen: 20, strokeWeight: 2.5, coreOpacity: 0.75, ringScale: 1.14 },
13519
13521
  strong: { bracketLen: 26, strokeWeight: 3, coreOpacity: 1, ringScale: 1.22 }
13520
13522
  };
13521
13523
  function PulseOverlay({ bbox, action }) {
13524
+ const pulseId = useId2();
13522
13525
  const [x1, y1, x2, y2] = bbox;
13523
13526
  const w = Math.max(1, x2 - x1);
13524
13527
  const h = Math.max(1, y2 - y1);
@@ -13527,29 +13530,48 @@ function PulseOverlay({ bbox, action }) {
13527
13530
  const spec = INTENSITY[action.intensity] ?? INTENSITY.normal;
13528
13531
  const L = Math.min(spec.bracketLen, Math.min(w, h) / 2.5);
13529
13532
  const PAD = 6;
13530
- return /* @__PURE__ */ jsxs38(
13533
+ const bracketExtent = PAD + L + 8;
13534
+ const glowExtent = (w / 2 + 24) * spec.ringScale - w / 2;
13535
+ const ringExtent = (w / 2 + PAD) * spec.ringScale - w / 2;
13536
+ const glowExtentV = (h / 2 + 24) * spec.ringScale - h / 2;
13537
+ const ringExtentV = (h / 2 + PAD) * spec.ringScale - h / 2;
13538
+ const svgPadX = Math.ceil(Math.max(bracketExtent, glowExtent, ringExtent) + 4);
13539
+ const svgPadY = Math.ceil(Math.max(bracketExtent, glowExtentV, ringExtentV) + 4);
13540
+ const svgX = x1 - svgPadX;
13541
+ const svgY = y1 - svgPadY;
13542
+ const svgW = w + 2 * svgPadX;
13543
+ const svgH = h + 2 * svgPadY;
13544
+ return /* @__PURE__ */ jsxs37(
13531
13545
  "svg",
13532
13546
  {
13547
+ width: svgW,
13548
+ height: svgH,
13549
+ viewBox: `${svgX} ${svgY} ${svgW} ${svgH}`,
13533
13550
  style: {
13534
13551
  position: "absolute",
13535
- inset: 0,
13552
+ left: svgX,
13553
+ top: svgY,
13536
13554
  pointerEvents: "none",
13537
13555
  overflow: "visible"
13538
13556
  },
13539
13557
  "data-role": "pulse",
13540
13558
  children: [
13559
+ /* @__PURE__ */ jsx45("defs", { children: /* @__PURE__ */ jsxs37("radialGradient", { id: `${pulseId}-glow`, cx: "50%", cy: "50%", r: "50%", children: [
13560
+ /* @__PURE__ */ jsx45("stop", { offset: "0%", stopColor: ACCENT_GLOW, stopOpacity: 1 }),
13561
+ /* @__PURE__ */ jsx45("stop", { offset: "60%", stopColor: ACCENT_GLOW, stopOpacity: 0.45 }),
13562
+ /* @__PURE__ */ jsx45("stop", { offset: "100%", stopColor: ACCENT_GLOW, stopOpacity: 0 })
13563
+ ] }) }),
13541
13564
  /* @__PURE__ */ jsx45(
13542
13565
  motion5.ellipse,
13543
13566
  {
13544
13567
  cx,
13545
13568
  cy,
13546
- rx: w / 2 + 10,
13547
- ry: h / 2 + 10,
13548
- fill: ACCENT_GLOW,
13569
+ rx: w / 2 + 24,
13570
+ ry: h / 2 + 24,
13571
+ fill: `url(#${pulseId}-glow)`,
13549
13572
  style: {
13550
13573
  transformOrigin: `${cx}px ${cy}px`,
13551
- transformBox: "fill-box",
13552
- filter: "blur(16px)"
13574
+ transformBox: "fill-box"
13553
13575
  },
13554
13576
  initial: { opacity: 0, scale: 0.95 },
13555
13577
  animate: {
@@ -13682,7 +13704,7 @@ function Bracket({
13682
13704
  // src/components/TutorMode/CalloutArrow.tsx
13683
13705
  import { useId as useId3 } from "react";
13684
13706
  import { motion as motion6 } from "framer-motion";
13685
- import { jsx as jsx46, jsxs as jsxs39 } from "react/jsx-runtime";
13707
+ import { jsx as jsx46, jsxs as jsxs38 } from "react/jsx-runtime";
13686
13708
  function centerOf(b) {
13687
13709
  return { x: (b[0] + b[2]) / 2, y: (b[1] + b[3]) / 2 };
13688
13710
  }
@@ -13722,18 +13744,33 @@ function CalloutArrow({ fromBbox, toBbox, action }) {
13722
13744
  const glowId = `${markerId}-glow`;
13723
13745
  const { from, to } = edgePoints(fromBbox, toBbox);
13724
13746
  const d = arrowPath(from, to, action.curve);
13725
- return /* @__PURE__ */ jsxs39(
13747
+ const rawMinX = Math.min(from.x, to.x);
13748
+ const rawMinY = Math.min(from.y, to.y);
13749
+ const rawMaxX = Math.max(from.x, to.x);
13750
+ const rawMaxY = Math.max(from.y, to.y);
13751
+ const dist = Math.hypot(to.x - from.x, to.y - from.y);
13752
+ const arcDev = action.curve === "curved" ? dist * 0.12 : 0;
13753
+ const basePad = 16;
13754
+ const svgX = rawMinX - basePad - arcDev;
13755
+ const svgY = rawMinY - basePad - arcDev;
13756
+ const svgW = rawMaxX - rawMinX + 2 * (basePad + arcDev);
13757
+ const svgH = rawMaxY - rawMinY + 2 * (basePad + arcDev);
13758
+ return /* @__PURE__ */ jsxs38(
13726
13759
  "svg",
13727
13760
  {
13761
+ width: svgW,
13762
+ height: svgH,
13763
+ viewBox: `${svgX} ${svgY} ${svgW} ${svgH}`,
13728
13764
  style: {
13729
13765
  position: "absolute",
13730
- inset: 0,
13766
+ left: svgX,
13767
+ top: svgY,
13731
13768
  pointerEvents: "none",
13732
13769
  overflow: "visible"
13733
13770
  },
13734
13771
  "data-role": "callout",
13735
13772
  children: [
13736
- /* @__PURE__ */ jsxs39("defs", { children: [
13773
+ /* @__PURE__ */ jsxs38("defs", { children: [
13737
13774
  /* @__PURE__ */ jsx46(
13738
13775
  "marker",
13739
13776
  {
@@ -13876,8 +13913,9 @@ function CinemaLayer({
13876
13913
  page,
13877
13914
  index,
13878
13915
  overlays,
13879
- scale
13916
+ coordScale
13880
13917
  }) {
13918
+ if (overlays.length === 0) return null;
13881
13919
  return /* @__PURE__ */ jsx48(
13882
13920
  "div",
13883
13921
  {
@@ -13886,7 +13924,7 @@ function CinemaLayer({
13886
13924
  position: "absolute",
13887
13925
  inset: 0,
13888
13926
  transformOrigin: "0 0",
13889
- transform: `scale(${scale})`,
13927
+ transform: `scale(${coordScale})`,
13890
13928
  width: page.page_dimensions.width,
13891
13929
  height: page.page_dimensions.height,
13892
13930
  pointerEvents: "none",
@@ -13971,7 +14009,7 @@ import { AnimatePresence as AnimatePresence2 } from "framer-motion";
13971
14009
 
13972
14010
  // src/components/TutorMode/GhostReference.tsx
13973
14011
  import { motion as motion8 } from "framer-motion";
13974
- import { jsx as jsx49, jsxs as jsxs40 } from "react/jsx-runtime";
14012
+ import { jsx as jsx49, jsxs as jsxs39 } from "react/jsx-runtime";
13975
14013
  var POSITIONS = {
13976
14014
  "top-right": {
13977
14015
  top: "clamp(12px, 4vw, 40px)",
@@ -14004,7 +14042,7 @@ function GhostReference({
14004
14042
  }) {
14005
14043
  const [x1, y1, x2, y2] = sourceBbox;
14006
14044
  const text = sourceBlockText ?? "(figure)";
14007
- return /* @__PURE__ */ jsxs40(
14045
+ return /* @__PURE__ */ jsxs39(
14008
14046
  motion8.div,
14009
14047
  {
14010
14048
  initial: { opacity: 0, y: 24, scale: 0.97 },
@@ -14067,7 +14105,7 @@ function GhostReference({
14067
14105
  }
14068
14106
  }
14069
14107
  ),
14070
- /* @__PURE__ */ jsxs40(
14108
+ /* @__PURE__ */ jsxs39(
14071
14109
  "div",
14072
14110
  {
14073
14111
  style: {
@@ -14097,7 +14135,7 @@ function GhostReference({
14097
14135
  overflow: "hidden",
14098
14136
  boxShadow: "inset 0 0 0 1px rgba(42, 36, 32, 0.12), 0 1px 3px rgba(42, 36, 32, 0.10)"
14099
14137
  },
14100
- children: /* @__PURE__ */ jsxs40(
14138
+ children: /* @__PURE__ */ jsxs39(
14101
14139
  "svg",
14102
14140
  {
14103
14141
  width: "100%",
@@ -14144,7 +14182,7 @@ function GhostReference({
14144
14182
  )
14145
14183
  }
14146
14184
  ),
14147
- /* @__PURE__ */ jsxs40(
14185
+ /* @__PURE__ */ jsxs39(
14148
14186
  motion8.div,
14149
14187
  {
14150
14188
  initial: { opacity: 0, y: 6 },
@@ -14286,12 +14324,12 @@ import { AnimatePresence as AnimatePresence3 } from "framer-motion";
14286
14324
 
14287
14325
  // src/components/TutorMode/StickyLabel.tsx
14288
14326
  import { motion as motion9 } from "framer-motion";
14289
- import { jsx as jsx51, jsxs as jsxs41 } from "react/jsx-runtime";
14327
+ import { jsx as jsx51, jsxs as jsxs40 } from "react/jsx-runtime";
14290
14328
  var STEM = 18;
14291
14329
  function StickyLabel({ screenAnchor, action }) {
14292
14330
  const { x, y } = screenAnchor;
14293
14331
  const layout = LAYOUTS[action.position] ?? LAYOUTS.top;
14294
- return /* @__PURE__ */ jsxs41(
14332
+ return /* @__PURE__ */ jsxs40(
14295
14333
  motion9.div,
14296
14334
  {
14297
14335
  initial: { opacity: 0, scale: 0.88 },
@@ -14476,7 +14514,8 @@ function LabelOverlay({
14476
14514
  index,
14477
14515
  currentPage,
14478
14516
  camera,
14479
- viewport
14517
+ viewport,
14518
+ coordScale
14480
14519
  }) {
14481
14520
  const labels = overlays.filter((o) => o.kind === "label");
14482
14521
  const page = index.byPage.get(currentPage);
@@ -14503,7 +14542,8 @@ function LabelOverlay({
14503
14542
  a.position,
14504
14543
  page,
14505
14544
  camera,
14506
- viewport
14545
+ viewport,
14546
+ coordScale
14507
14547
  );
14508
14548
  return /* @__PURE__ */ jsx52(
14509
14549
  StickyLabel,
@@ -14517,7 +14557,7 @@ function LabelOverlay({
14517
14557
  }
14518
14558
  );
14519
14559
  }
14520
- function computeScreenAnchor(bbox, where, page, camera, viewport) {
14560
+ function computeScreenAnchor(bbox, where, page, camera, viewport, coordScale) {
14521
14561
  const [x1, y1, x2, y2] = bbox;
14522
14562
  const pageCX = page.page_dimensions.width / 2;
14523
14563
  const pageCY = page.page_dimensions.height / 2;
@@ -14543,8 +14583,8 @@ function computeScreenAnchor(bbox, where, page, camera, viewport) {
14543
14583
  px = (x1 + x2) / 2;
14544
14584
  py = y1;
14545
14585
  }
14546
- const screenX = viewport.width / 2 + camera.x + (px - pageCX) * camera.scale;
14547
- const screenY = viewport.height / 2 + camera.y + (py - pageCY) * camera.scale;
14586
+ const screenX = viewport.width / 2 + camera.x + (px - pageCX) * coordScale * camera.scale;
14587
+ const screenY = viewport.height / 2 + camera.y + (py - pageCY) * coordScale * camera.scale;
14548
14588
  return { x: screenX, y: screenY };
14549
14589
  }
14550
14590
 
@@ -14556,7 +14596,8 @@ function CalloutLabelOverlay({
14556
14596
  index,
14557
14597
  currentPage,
14558
14598
  camera,
14559
- viewport
14599
+ viewport,
14600
+ coordScale
14560
14601
  }) {
14561
14602
  const callouts = overlays.filter(
14562
14603
  (o) => o.kind === "callout" && o.action.label
@@ -14585,7 +14626,8 @@ function CalloutLabelOverlay({
14585
14626
  toHit.block.bbox,
14586
14627
  page,
14587
14628
  camera,
14588
- viewport
14629
+ viewport,
14630
+ coordScale
14589
14631
  );
14590
14632
  return /* @__PURE__ */ jsx53(
14591
14633
  CalloutLabelPill,
@@ -14600,7 +14642,7 @@ function CalloutLabelOverlay({
14600
14642
  }
14601
14643
  );
14602
14644
  }
14603
- function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14645
+ function computePillAnchor(fromBbox, toBbox, page, camera, viewport, coordScale) {
14604
14646
  const aCX = (fromBbox[0] + fromBbox[2]) / 2;
14605
14647
  const aCY = (fromBbox[1] + fromBbox[3]) / 2;
14606
14648
  const bCX = (toBbox[0] + toBbox[2]) / 2;
@@ -14617,8 +14659,8 @@ function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14617
14659
  const toY = bCY - uy * bOff;
14618
14660
  const pageCX = page.page_dimensions.width / 2;
14619
14661
  const pageCY = page.page_dimensions.height / 2;
14620
- const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * camera.scale;
14621
- const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * camera.scale;
14662
+ const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * coordScale * camera.scale;
14663
+ const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * coordScale * camera.scale;
14622
14664
  const isVertical = Math.abs(dy) >= Math.abs(dx);
14623
14665
  const OFFSET = resolvePillOffset(viewport.width);
14624
14666
  const MAX_PILL_W = resolveMaxPillW(viewport.width);
@@ -14761,6 +14803,41 @@ function SubtitleBar({ text }) {
14761
14803
  ) : null });
14762
14804
  }
14763
14805
 
14806
+ // src/utils/render-scale.ts
14807
+ function computeFitScale(input) {
14808
+ const { pageWidthPt, pageHeightPt, viewport, paddingFactor = 0.95 } = input;
14809
+ if (!Number.isFinite(pageWidthPt) || !Number.isFinite(pageHeightPt) || pageWidthPt <= 0 || pageHeightPt <= 0) {
14810
+ return 1;
14811
+ }
14812
+ const w = Math.max(1, viewport.width);
14813
+ const h = Math.max(1, viewport.height);
14814
+ const fit = Math.min(w / pageWidthPt, h / pageHeightPt) * paddingFactor;
14815
+ return Number.isFinite(fit) && fit > 0 ? fit : 1;
14816
+ }
14817
+ function computeCoordScale(input) {
14818
+ const { renderScale, dpi } = input;
14819
+ if (!Number.isFinite(renderScale) || renderScale <= 0) return 0;
14820
+ if (!Number.isFinite(dpi) || dpi <= 0) return 1;
14821
+ return renderScale * 72 / dpi;
14822
+ }
14823
+ function isTouchDevice() {
14824
+ if (typeof window === "undefined") return false;
14825
+ if (typeof window.matchMedia !== "function") return false;
14826
+ try {
14827
+ return window.matchMedia("(pointer: coarse)").matches;
14828
+ } catch {
14829
+ return false;
14830
+ }
14831
+ }
14832
+ function resolveRenderScale(input) {
14833
+ const { renderScaleProp, scaleProp, defaultRenderScale } = input;
14834
+ if (Number.isFinite(renderScaleProp) && renderScaleProp > 0) {
14835
+ return renderScaleProp;
14836
+ }
14837
+ const multiplier = Number.isFinite(scaleProp) && scaleProp > 0 ? scaleProp : 1;
14838
+ return defaultRenderScale * multiplier;
14839
+ }
14840
+
14764
14841
  // src/director/storyboard-engine.ts
14765
14842
  init_narration_store();
14766
14843
  init_camera_math();
@@ -14851,11 +14928,32 @@ var StoryboardEngine = class {
14851
14928
  this.pendingStepTimers.clear();
14852
14929
  this.deps.narrationStore.getState().setEngineStatus("idle");
14853
14930
  }
14854
- /** Cancel every removal timer (used by resetVisuals only). */
14931
+ /** Cancel every removal timer (used by resetVisuals and destroy). */
14855
14932
  cancelAllRemovalTimers() {
14856
14933
  for (const t of this.overlayRemovalTimers.values()) clearTimeout(t);
14857
14934
  this.overlayRemovalTimers.clear();
14858
14935
  }
14936
+ /**
14937
+ * Full destructor — cancels both pending step timers AND overlay removal
14938
+ * timers. Use this in component unmount/cleanup. The per-storyboard
14939
+ * `cancelPending()` deliberately leaves overlay removal timers alone so a
14940
+ * mid-flight overlay doesn't get stranded (see `overlayRemovalTimers` doc),
14941
+ * but at destruction time we must release every timer or their closures
14942
+ * keep `deps` (narrationStore, the full bboxIndex) alive beyond the
14943
+ * lifetime of this engine. On iOS Safari with many engine recreations
14944
+ * this causes cumulative memory pressure and eventual tab reload.
14945
+ */
14946
+ destroy() {
14947
+ this.cancelPending();
14948
+ this.cancelAllRemovalTimers();
14949
+ }
14950
+ /** Test-only — exposes internal queue sizes for regression tests. */
14951
+ _queueSizesForTest() {
14952
+ return {
14953
+ pending: this.pendingStepTimers.size,
14954
+ removals: this.overlayRemovalTimers.size
14955
+ };
14956
+ }
14859
14957
  /** Reset visuals: clear overlays, cancel every removal timer, fit camera. */
14860
14958
  resetVisuals() {
14861
14959
  this.cancelPending();
@@ -15956,7 +16054,7 @@ function storyboardFromMatch(match, page) {
15956
16054
  }
15957
16055
 
15958
16056
  // src/components/TutorMode/TutorModeContainer.tsx
15959
- import { Fragment as Fragment4, jsx as jsx55, jsxs as jsxs42 } from "react/jsx-runtime";
16057
+ import { Fragment as Fragment4, jsx as jsx55, jsxs as jsxs41 } from "react/jsx-runtime";
15960
16058
  function buildBBoxIndex(bboxData) {
15961
16059
  const byPage = /* @__PURE__ */ new Map();
15962
16060
  const blockById = /* @__PURE__ */ new Map();
@@ -15996,6 +16094,7 @@ function TutorModeContainer({
15996
16094
  loadingComponent,
15997
16095
  onPageChange,
15998
16096
  storyboardProvider,
16097
+ renderScale,
15999
16098
  className
16000
16099
  }) {
16001
16100
  const containerRef = useRef27(null);
@@ -16010,6 +16109,27 @@ function TutorModeContainer({
16010
16109
  const [viewport, setViewport] = useState30({ width: 800, height: 1e3 });
16011
16110
  const camera = useStore2(narrationStore, (s) => s.camera);
16012
16111
  const activeOverlays = useStore2(narrationStore, (s) => s.activeOverlays);
16112
+ const page = index.byPage.get(pageNumber);
16113
+ const pagePointsW = page ? page.page_dimensions.width * 72 / page.page_dimensions.dpi : 0;
16114
+ const pagePointsH = page ? page.page_dimensions.height * 72 / page.page_dimensions.dpi : 0;
16115
+ const touch = isTouchDevice();
16116
+ const defaultRenderScale = page ? touch ? computeFitScale({
16117
+ pageWidthPt: pagePointsW,
16118
+ pageHeightPt: pagePointsH,
16119
+ viewport
16120
+ }) : page.page_dimensions.dpi / 72 : 1;
16121
+ const effectiveRenderScale = resolveRenderScale({
16122
+ renderScaleProp: renderScale,
16123
+ scaleProp: scale,
16124
+ defaultRenderScale
16125
+ });
16126
+ const coordScale = page ? computeCoordScale({
16127
+ renderScale: effectiveRenderScale,
16128
+ dpi: page.page_dimensions.dpi
16129
+ }) : 1;
16130
+ const rasterScale = effectiveRenderScale;
16131
+ const baseW = pagePointsW * effectiveRenderScale;
16132
+ const baseH = pagePointsH * effectiveRenderScale;
16013
16133
  useEffect28(() => {
16014
16134
  if (numPages <= 0) return;
16015
16135
  if (pageNumber < 1 || pageNumber > numPages) return;
@@ -16025,7 +16145,13 @@ function TutorModeContainer({
16025
16145
  useEffect28(() => {
16026
16146
  if (!containerRef.current) return;
16027
16147
  const el = containerRef.current;
16028
- const update = () => setViewport({ width: el.clientWidth, height: el.clientHeight });
16148
+ const update = () => {
16149
+ const w = el.clientWidth;
16150
+ const h = el.clientHeight;
16151
+ setViewport(
16152
+ (prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h }
16153
+ );
16154
+ };
16029
16155
  update();
16030
16156
  const ro = new ResizeObserver(update);
16031
16157
  ro.observe(el);
@@ -16053,23 +16179,29 @@ function TutorModeContainer({
16053
16179
  const page2 = index.byPage.get(pageNumber);
16054
16180
  if (!page2) return;
16055
16181
  if (viewport.width === 0 || viewport.height === 0) return;
16182
+ if (baseW === 0 || baseH === 0) return;
16056
16183
  if (narrationStore.getState().activeOverlays.length > 0) return;
16057
- const fit = Math.min(
16058
- viewport.width / page2.page_dimensions.width,
16059
- viewport.height / page2.page_dimensions.height
16060
- ) * 0.95;
16184
+ const fit = Math.min(viewport.width / baseW, viewport.height / baseH) * 0.95;
16061
16185
  narrationStore.getState().setCamera({ scale: fit, x: 0, y: 0 });
16062
- }, [pageNumber, viewport, index, narrationStore]);
16186
+ }, [pageNumber, viewport, index, narrationStore, baseW, baseH]);
16187
+ const viewportRef = useRef27(viewport);
16188
+ useEffect28(() => {
16189
+ viewportRef.current = viewport;
16190
+ }, [viewport]);
16063
16191
  const engineRef = useRef27(null);
16064
16192
  useEffect28(() => {
16065
- engineRef.current = new StoryboardEngine({
16193
+ const engine = new StoryboardEngine({
16066
16194
  narrationStore,
16067
16195
  bboxIndex: index,
16068
- getViewport: () => viewport,
16196
+ getViewport: () => viewportRef.current,
16069
16197
  minOverlayDurationMs
16070
16198
  });
16071
- return () => engineRef.current?.cancelPending();
16072
- }, [narrationStore, index, viewport, minOverlayDurationMs]);
16199
+ engineRef.current = engine;
16200
+ return () => {
16201
+ engine.destroy();
16202
+ if (engineRef.current === engine) engineRef.current = null;
16203
+ };
16204
+ }, [narrationStore, index, minOverlayDurationMs]);
16073
16205
  const abortRef = useRef27(null);
16074
16206
  const debounceRef = useRef27(null);
16075
16207
  const lastChunkRef = useRef27(null);
@@ -16250,13 +16382,8 @@ function TutorModeContainer({
16250
16382
  }, idleTimeoutMs + 100);
16251
16383
  return () => clearTimeout(t);
16252
16384
  }, [currentChunk, idleTimeoutMs, narrationStore]);
16253
- const page = index.byPage.get(pageNumber);
16254
- const dpiScale = page ? page.page_dimensions.dpi / 72 : 1;
16255
- const rasterScale = dpiScale * (scale || 1);
16256
- const baseW = page ? page.page_dimensions.width * (scale || 1) : 0;
16257
- const baseH = page ? page.page_dimensions.height * (scale || 1) : 0;
16258
16385
  const isReady = !!page && !!pageProxy;
16259
- return /* @__PURE__ */ jsxs42(
16386
+ return /* @__PURE__ */ jsxs41(
16260
16387
  "div",
16261
16388
  {
16262
16389
  ref: containerRef,
@@ -16302,8 +16429,8 @@ function TutorModeContainer({
16302
16429
  children: "Reset view"
16303
16430
  }
16304
16431
  ) : null,
16305
- isReady ? /* @__PURE__ */ jsxs42(Fragment4, { children: [
16306
- /* @__PURE__ */ jsx55(CameraView, { camera, children: /* @__PURE__ */ jsxs42(
16432
+ isReady ? /* @__PURE__ */ jsxs41(Fragment4, { children: [
16433
+ /* @__PURE__ */ jsx55(CameraView, { camera, children: /* @__PURE__ */ jsxs41(
16307
16434
  "div",
16308
16435
  {
16309
16436
  style: {
@@ -16333,7 +16460,7 @@ function TutorModeContainer({
16333
16460
  page,
16334
16461
  index,
16335
16462
  overlays: activeOverlays,
16336
- scale: scale || 1
16463
+ coordScale
16337
16464
  }
16338
16465
  )
16339
16466
  ]
@@ -16346,7 +16473,8 @@ function TutorModeContainer({
16346
16473
  index,
16347
16474
  currentPage: pageNumber,
16348
16475
  camera,
16349
- viewport
16476
+ viewport,
16477
+ coordScale
16350
16478
  }
16351
16479
  ),
16352
16480
  /* @__PURE__ */ jsx55(
@@ -16356,7 +16484,8 @@ function TutorModeContainer({
16356
16484
  index,
16357
16485
  currentPage: pageNumber,
16358
16486
  camera,
16359
- viewport
16487
+ viewport,
16488
+ coordScale
16360
16489
  }
16361
16490
  ),
16362
16491
  /* @__PURE__ */ jsx55(GhostReferenceOverlay, { overlays: activeOverlays, index })
@@ -16385,7 +16514,7 @@ function TutorLoadingState({
16385
16514
  }
16386
16515
  );
16387
16516
  }
16388
- return /* @__PURE__ */ jsxs42(
16517
+ return /* @__PURE__ */ jsxs41(
16389
16518
  "div",
16390
16519
  {
16391
16520
  style: {