@embedpdf/core-annotation 3.0.0-next.3 → 3.0.0-next.5

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,5 +1,5 @@
1
1
  import { NO_ANNOTATION_FLAGS } from "@embedpdf/engine-core/runtime";
2
- import { applyPoint, applyRect, invert, isQuarterTurn, pdfToContentMatrix, rotateAbout } from "@embedpdf/core-geometry";
2
+ import { applyPoint, applyRect, invert, isQuarterTurn, pdfToContentMatrix, rotateAbout, textQuadBounds, textQuadPoints, textQuadRing } from "@embedpdf/core-geometry";
3
3
  //#region src/kinds.ts
4
4
  const OPACITY = {
5
5
  key: "opacity",
@@ -1463,9 +1463,12 @@ const DEFAULT_CHROME_GEOM = {
1463
1463
  };
1464
1464
  /** Normalize degrees into `[0, 360)`. */
1465
1465
  const normalizeDeg = (d) => (d % 360 + 360) % 360;
1466
- /** A geom's applied rotation (deg), or 0 for the non-rotatable kinds. */
1466
+ /** A geom's applied rotation (deg), or 0 for the non-rotatable kinds. A
1467
+ * caret's `rot` is AUTHORING metadata (its text's baseline tilt): reported
1468
+ * here so the renderer and selection chrome follow it, while the caret's
1469
+ * caps (not movable/resizable) keep every rotate gesture away from it. */
1467
1470
  function geomRotation(g) {
1468
- if (g.t === "rect" || g.t === "line" || g.t === "poly" || g.t === "ink" || g.t === "text") return g.rot ?? 0;
1471
+ if (g.t === "rect" || g.t === "line" || g.t === "poly" || g.t === "ink" || g.t === "text" || g.t === "caret") return g.rot ?? 0;
1469
1472
  return 0;
1470
1473
  }
1471
1474
  const rectCenter = (r) => ({
@@ -1487,7 +1490,7 @@ function centroidOf(g) {
1487
1490
  x: (g.a.x + g.b.x) / 2,
1488
1491
  y: (g.a.y + g.b.y) / 2
1489
1492
  };
1490
- const pts = g.t === "poly" ? g.points : g.t === "ink" ? g.strokes.flat() : g.quads.flat();
1493
+ const pts = g.t === "poly" ? g.points : g.t === "ink" ? g.strokes.flat() : g.quads.flatMap(textQuadPoints);
1491
1494
  let sx = 0;
1492
1495
  let sy = 0;
1493
1496
  for (const p of pts) {
@@ -1500,9 +1503,12 @@ function centroidOf(g) {
1500
1503
  y: sy / n
1501
1504
  };
1502
1505
  }
1503
- /** Is this kind rotatable (the geometry carries a meaningful `rot`)? */
1506
+ /** Does the geometry carry a meaningful `rot` (an oriented local box exists)?
1507
+ * ORIENTATION is a geometry fact; whether the USER may rotate is the separate
1508
+ * `caps.rotatable` gate — a caret is oriented (it rides its text's tilt) yet
1509
+ * offers no rotate gesture. */
1504
1510
  function isRotatableGeom(g) {
1505
- return g.t === "rect" || g.t === "line" || g.t === "poly" || g.t === "ink" || g.t === "text" && !g.callout;
1511
+ return g.t === "rect" || g.t === "line" || g.t === "poly" || g.t === "ink" || g.t === "caret" || g.t === "text" && !g.callout;
1506
1512
  }
1507
1513
  /**
1508
1514
  * Rotate a geom by `deltaDeg` (clockwise) about `pivot`.
@@ -1511,7 +1517,10 @@ function isRotatableGeom(g) {
1511
1517
  * pivot IS the box centre this is a pure `rot += delta`.
1512
1518
  * - VERTEX (`line`/`poly`/`ink`): map every point through the rotation AND bump
1513
1519
  * the advisory `rot` (the points stay the authoritative visual).
1514
- * Non-rotatable kinds (caret/quads, callouts) are returned unchanged.
1520
+ * Kinds without a rotate VERB are returned unchanged: quads and callouts, and
1521
+ * also the caret — oriented (`isRotatableGeom`) but text-anchored, so its tilt
1522
+ * is authoring metadata that no gesture edits (`geomResetRotation` still
1523
+ * clears it).
1515
1524
  */
1516
1525
  function geomRotateAbout(g, pivot, deltaDeg) {
1517
1526
  if (deltaDeg === 0) return g;
@@ -1571,7 +1580,7 @@ function apFrameSize(g) {
1571
1580
  width: g.rect.width,
1572
1581
  height: g.rect.height
1573
1582
  };
1574
- const b = unionRect(g.t === "line" ? [g.a, g.b] : g.t === "poly" ? g.points : g.t === "ink" ? g.strokes.flat() : g.quads.flat());
1583
+ const b = unionRect(g.t === "line" ? [g.a, g.b] : g.t === "poly" ? g.points : g.t === "ink" ? g.strokes.flat() : g.quads.flatMap(textQuadPoints));
1575
1584
  return {
1576
1585
  width: b.width,
1577
1586
  height: b.height
@@ -1680,7 +1689,7 @@ function fitStampBox(center, desired, page, rotCW) {
1680
1689
  function geomResetRotation(g) {
1681
1690
  const rot = geomRotation(g);
1682
1691
  if (!rot) return g;
1683
- if (g.t === "rect" || g.t === "text") return {
1692
+ if (g.t === "rect" || g.t === "text" || g.t === "caret") return {
1684
1693
  ...g,
1685
1694
  rot: 0
1686
1695
  };
@@ -1697,7 +1706,7 @@ function geomResetRotation(g) {
1697
1706
  function obbFromGeom(g, strokeWidth, border) {
1698
1707
  if (!isRotatableGeom(g)) return null;
1699
1708
  const rot = geomRotation(g);
1700
- if (g.t === "rect" || g.t === "text") {
1709
+ if (g.t === "rect" || g.t === "text" || g.t === "caret") {
1701
1710
  const c = rectCenter(g.rect);
1702
1711
  return {
1703
1712
  corners: rectCornerPoints(g.rect).map((p) => rotatePoint(p, c, rot)),
@@ -2002,6 +2011,77 @@ function caretRectFromTextEnd(lineRect) {
2002
2011
  height
2003
2012
  };
2004
2013
  }
2014
+ /**
2015
+ * Caret box for a text-edit anchor: half the glyph's ink height, centered on
2016
+ * the TRAILING baseline corner in READING direction (`advance` decides which
2017
+ * end — RTL carets land on the visual left), sitting on the baseline. The box
2018
+ * stays axis-aligned; use {@link caretGeomFromAnchor} for the tilt-carrying
2019
+ * geometry.
2020
+ */
2021
+ function caretRectFromAnchor(anchor) {
2022
+ const q = anchor.glyphQuad;
2023
+ const ink = Math.hypot(q.lowerStart.x - q.upperStart.x, q.lowerStart.y - q.upperStart.y);
2024
+ const size = Math.max(ink / 2, 1);
2025
+ const corner = anchor.advance > 0 ? q.lowerEnd : q.lowerStart;
2026
+ return {
2027
+ x: corner.x - size / 2,
2028
+ y: corner.y - size,
2029
+ width: size,
2030
+ height: size
2031
+ };
2032
+ }
2033
+ /** Rotations closer than ~0.05° to upright stay upright (float noise guard). */
2034
+ const CARET_ROT_EPSILON = .05;
2035
+ /**
2036
+ * Caret GEOMETRY for a text-edit anchor: the box-family pair — an UNROTATED
2037
+ * box whose centre sits half a caret-size ascent-ward of the trailing
2038
+ * baseline corner, plus `rot` = the text's baseline tilt (deg, CW in y-down
2039
+ * content space). Rotating the box about its centre by `rot` lands it
2040
+ * hugging the rotated baseline, symbol pointing at its text. For upright
2041
+ * anchors this degenerates EXACTLY to {@link caretRectFromAnchor} with no
2042
+ * `rot` key — the dominant case is byte-identical.
2043
+ */
2044
+ function caretGeomFromAnchor(anchor) {
2045
+ const q = anchor.glyphQuad;
2046
+ const ink = Math.hypot(q.lowerStart.x - q.upperStart.x, q.lowerStart.y - q.upperStart.y);
2047
+ const size = Math.max(ink / 2, 1);
2048
+ const corner = anchor.advance > 0 ? q.lowerEnd : q.lowerStart;
2049
+ const bx = q.lowerEnd.x - q.lowerStart.x;
2050
+ const by = q.lowerEnd.y - q.lowerStart.y;
2051
+ const rot = Math.hypot(bx, by) > 0 ? normalizeDeg(Math.atan2(by, bx) * 180 / Math.PI) : 0;
2052
+ if (rot < CARET_ROT_EPSILON || rot > 360 - CARET_ROT_EPSILON) return {
2053
+ t: "caret",
2054
+ rect: {
2055
+ x: corner.x - size / 2,
2056
+ y: corner.y - size,
2057
+ width: size,
2058
+ height: size
2059
+ }
2060
+ };
2061
+ const ux = (q.upperStart.x - q.lowerStart.x) / ink;
2062
+ const uy = (q.upperStart.y - q.lowerStart.y) / ink;
2063
+ const cx = corner.x + ux * size / 2;
2064
+ const cy = corner.y + uy * size / 2;
2065
+ return {
2066
+ t: "caret",
2067
+ rect: {
2068
+ x: cx - size / 2,
2069
+ y: cy - size / 2,
2070
+ width: size,
2071
+ height: size
2072
+ },
2073
+ rot
2074
+ };
2075
+ }
2076
+ /** Map a TextQuad's corners through a point function (names ride along). */
2077
+ function mapTextQuad(q, f) {
2078
+ return {
2079
+ upperStart: f(q.upperStart),
2080
+ upperEnd: f(q.upperEnd),
2081
+ lowerStart: f(q.lowerStart),
2082
+ lowerEnd: f(q.lowerEnd)
2083
+ };
2084
+ }
2005
2085
  /** Miter limit shared by the bounds math AND the SVG renderer (`stroke-miterlimit`),
2006
2086
  * so the computed box and the drawn stroke always agree on where a sharp join
2007
2087
  * bevels instead of spiking. 10 = the PDF default (also what the baked /AP uses). */
@@ -2150,7 +2230,7 @@ function geomVisualBounds(g, strokeWidth, border) {
2150
2230
  return expandRect(unionRect(all), strokeWidth / 2);
2151
2231
  }
2152
2232
  if (g.t === "rect" || g.t === "text" || g.t === "caret") return g.rect;
2153
- if (g.t === "quads") return expandRect(unionRect(g.quads.flat()), strokeWidth / 2);
2233
+ if (g.t === "quads") return expandRect(unionRect(g.quads.flatMap(textQuadPoints)), strokeWidth / 2);
2154
2234
  if (g.t === "ink") return expandRect(unionRect(g.strokes.flat()), strokeWidth / 2);
2155
2235
  const pts = strokeOutlinePoints(g.t === "line" ? [g.a, g.b] : g.points, g.t === "poly" && g.closed, strokeWidth);
2156
2236
  for (const seg of endingSegs(g)) for (const node of endingNodes(seg.tip, seg.angle, seg.ending, strokeWidth)) if (node.kind === "poly") pts.push(...strokeOutlinePoints(node.points, node.closed, strokeWidth));
@@ -2286,7 +2366,7 @@ function geomBounds(g) {
2286
2366
  if (g.t === "line") return rectFromPoints(g.a, g.b);
2287
2367
  if (g.t === "poly") return unionRect(g.points);
2288
2368
  if (g.t === "ink") return unionRect(g.strokes.flat());
2289
- return unionRect(g.quads.flat());
2369
+ return unionRect(g.quads.flatMap(textQuadPoints));
2290
2370
  }
2291
2371
  /**
2292
2372
  * Is the content point ON the annotation: within `margin` of the stroke, or
@@ -2299,7 +2379,7 @@ function geomBounds(g) {
2299
2379
  */
2300
2380
  function geomHit(g, p, margin, filled, strokeWidth) {
2301
2381
  const tol = margin + strokeWidth / 2;
2302
- if ((g.t === "rect" || g.t === "text" && !g.callout) && (g.rot ?? 0) !== 0) p = rotatePoint(p, rectCenter(g.rect), -(g.rot ?? 0));
2382
+ if ((g.t === "rect" || g.t === "caret" || g.t === "text" && !g.callout) && (g.rot ?? 0) !== 0) p = rotatePoint(p, rectCenter(g.rect), -(g.rot ?? 0));
2303
2383
  if (g.t === "caret") return rectContains(expandRect(g.rect, margin), p);
2304
2384
  if (g.t === "text") {
2305
2385
  const rot = g.callout ? g.rot ?? 0 : 0;
@@ -2374,7 +2454,7 @@ function geomHit(g, p, margin, filled, strokeWidth) {
2374
2454
  for (const stroke of g.strokes) for (let i = 0; i < stroke.length - 1; i++) if (segDist(p, stroke[i], stroke[i + 1]) <= tol) return true;
2375
2455
  return false;
2376
2456
  }
2377
- return g.quads.some((q) => rectContains(unionRect(q), p));
2457
+ return g.quads.some((q) => pointInQuad(p, textQuadRing(q)));
2378
2458
  }
2379
2459
  function geomHandles(g) {
2380
2460
  if (g.t === "rect" || g.t === "text") {
@@ -2463,7 +2543,7 @@ function geomTranslate(g, d) {
2463
2543
  };
2464
2544
  return {
2465
2545
  ...g,
2466
- quads: g.quads.map((q) => q.map(mv))
2546
+ quads: g.quads.map((q) => mapTextQuad(q, mv))
2467
2547
  };
2468
2548
  }
2469
2549
  const OPPOSITE_HANDLE = {
@@ -2636,12 +2716,7 @@ function geomScene(g, strokeWidth = 0, border) {
2636
2716
  }));
2637
2717
  return g.quads.map((q) => ({
2638
2718
  kind: "poly",
2639
- points: [
2640
- q[0],
2641
- q[1],
2642
- q[3],
2643
- q[2]
2644
- ],
2719
+ points: textQuadRing(q),
2645
2720
  closed: true
2646
2721
  }));
2647
2722
  }
@@ -3664,10 +3739,10 @@ function update(m, msg) {
3664
3739
  case "createPointer": return createPointer(m, msg.phase, msg.subtype, msg.in, msg.preset, msg.intent, msg.deferInkCommit, msg.straightenInk, msg.clickCreate, msg.flags);
3665
3740
  case "finishInkDraft": return finishInkCreate(m);
3666
3741
  case "finishCreationDraft": return finishPolyCreate(m);
3667
- case "createCaret": return createCaret(m, msg.pon, msg.rect, msg.flags);
3668
- case "createReplaceText": return createReplaceText(m, msg.pon, msg.rects, msg.endRect, msg.preset);
3669
- case "createMarkup": return createMarkup(m, msg.subtype, msg.pon, msg.rects, msg.preset, msg.flags);
3670
- case "setMarkupPreview": return setMarkupPreview(m, msg.subtype, msg.rectsByPage, msg.preset);
3742
+ case "createCaret": return createCaret(m, msg.pon, msg.anchor, msg.flags);
3743
+ case "createReplaceText": return createReplaceText(m, msg.pon, msg.quads, msg.anchor, msg.preset);
3744
+ case "createMarkup": return createMarkup(m, msg.subtype, msg.pon, msg.quads, msg.preset, msg.flags);
3745
+ case "setMarkupPreview": return setMarkupPreview(m, msg.subtype, msg.quadsByPage, msg.preset);
3671
3746
  case "clearMarkupPreview": return m.preview ? [{
3672
3747
  ...m,
3673
3748
  preview: null
@@ -4468,33 +4543,22 @@ function finishPolyCreate(m) {
4468
4543
  id
4469
4544
  }]];
4470
4545
  }
4471
- /** Per-line selection rects → /QuadPoints quads (content space, y-down: UL, UR,
4472
- * LL, LR). Shared by markup creation and the live preview. */
4473
- const rectsToQuads = (rects) => rects.filter((r) => r.width > 0 && r.height > 0).map((r) => [
4474
- {
4475
- x: r.x,
4476
- y: r.y
4477
- },
4478
- {
4479
- x: r.x + r.width,
4480
- y: r.y
4481
- },
4482
- {
4483
- x: r.x,
4484
- y: r.y + r.height
4485
- },
4486
- {
4487
- x: r.x + r.width,
4488
- y: r.y + r.height
4489
- }
4490
- ]);
4546
+ /** Drop degenerate segment quads (zero-length baseline or ink extent). Area is
4547
+ * the cross product of the two edge vectors — orientation-safe. */
4548
+ const usableQuads = (quads) => quads.filter((q) => {
4549
+ const ux = q.upperEnd.x - q.upperStart.x;
4550
+ const uy = q.upperEnd.y - q.upperStart.y;
4551
+ const sx = q.lowerStart.x - q.upperStart.x;
4552
+ const sy = q.lowerStart.y - q.upperStart.y;
4553
+ return Math.abs(ux * sy - uy * sx) > 0;
4554
+ });
4491
4555
  /**
4492
4556
  * Build a text-markup annotation from the selection's per-line rects. The new
4493
4557
  * annotation is `vector` (rendered live by the overlay) and selected, mirroring
4494
4558
  * `createPointer`. One call per page the selection spans. Clears any live preview.
4495
4559
  */
4496
- function createMarkup(m, subtype, pon, rects, preset = subtype, flags) {
4497
- const quads = rectsToQuads(rects);
4560
+ function createMarkup(m, subtype, pon, segmentQuads, preset = subtype, flags) {
4561
+ const quads = usableQuads(segmentQuads);
4498
4562
  if (!quads.length) return [m, []];
4499
4563
  const id = `tmp:${m.seq + 1}`;
4500
4564
  const annot = {
@@ -4535,9 +4599,9 @@ function createMarkup(m, subtype, pon, rects, preset = subtype, flags) {
4535
4599
  * (`/IT /StrikeOutTextEdit`, `/IRT` caret, `/RT /Group`). Persistence performs
4536
4600
  * the two ordered writes and rolls the primary back if the subordinate fails.
4537
4601
  */
4538
- function createReplaceText(m, pon, rects, textEndRect, preset = "replace-text") {
4539
- const quads = rectsToQuads(rects);
4540
- if (!quads.length || textEndRect.width <= 0 || textEndRect.height <= 0) return [m, []];
4602
+ function createReplaceText(m, pon, segmentQuads, anchor, preset = "replace-text") {
4603
+ const quads = usableQuads(segmentQuads);
4604
+ if (!quads.length) return [m, []];
4541
4605
  const primaryId = `tmp:${m.seq + 1}`;
4542
4606
  const strikeoutId = `tmp:${m.seq + 2}`;
4543
4607
  const style = styleFromProps(defaultsFor(m, preset));
@@ -4547,10 +4611,7 @@ function createReplaceText(m, pon, rects, textEndRect, preset = "replace-text")
4547
4611
  pon,
4548
4612
  subtype: "caret",
4549
4613
  intent: "replace",
4550
- geom: {
4551
- t: "caret",
4552
- rect: caretRectFromTextEnd(textEndRect)
4553
- },
4614
+ geom: caretGeomFromAnchor(anchor),
4554
4615
  style,
4555
4616
  flags: DRAWN_FLAGS,
4556
4617
  source: "vector"
@@ -4593,20 +4654,17 @@ function createReplaceText(m, pon, rects, textEndRect, preset = "replace-text")
4593
4654
  members: [strikeoutId]
4594
4655
  }]];
4595
4656
  }
4596
- function createCaret(m, pon, textEndRect, flags) {
4597
- if (textEndRect.width <= 0 || textEndRect.height <= 0) return [m, []];
4657
+ function createCaret(m, pon, anchor, flags) {
4658
+ const caretGeom = caretGeomFromAnchor(anchor);
4659
+ if (caretGeom.rect.width <= 0 || caretGeom.rect.height <= 0) return [m, []];
4598
4660
  const id = `tmp:${m.seq + 1}`;
4599
- const def = defaultsFor(m, "caret");
4600
4661
  const annot = {
4601
4662
  id,
4602
4663
  ref: null,
4603
4664
  pon,
4604
4665
  subtype: "caret",
4605
- geom: {
4606
- t: "caret",
4607
- rect: caretRectFromTextEnd(textEndRect)
4608
- },
4609
- style: styleFromProps(def),
4666
+ geom: caretGeom,
4667
+ style: styleFromProps(defaultsFor(m, "caret")),
4610
4668
  flags: {
4611
4669
  ...DRAWN_FLAGS,
4612
4670
  ...flags
@@ -4629,11 +4687,11 @@ function createCaret(m, pon, textEndRect, flags) {
4629
4687
  id
4630
4688
  }]];
4631
4689
  }
4632
- /** Set / replace the live markup preview from the selection's per-page rects. */
4633
- function setMarkupPreview(m, subtype, rectsByPage, preset = subtype) {
4690
+ /** Set / replace the live markup preview from the selection's per-page quads. */
4691
+ function setMarkupPreview(m, subtype, quadsByPage, preset = subtype) {
4634
4692
  const byPage = {};
4635
- for (const k in rectsByPage) {
4636
- const quads = rectsToQuads(rectsByPage[k]);
4693
+ for (const k in quadsByPage) {
4694
+ const quads = usableQuads(quadsByPage[k]);
4637
4695
  if (quads.length) byPage[Number(k)] = quads;
4638
4696
  }
4639
4697
  return [{
@@ -4972,70 +5030,99 @@ function shapePaint(style, closed) {
4972
5030
  ...style.border.kind === "cloudy" ? { join: "round" } : {}
4973
5031
  };
4974
5032
  }
4975
- /** A smooth squiggle (quadratic-bezier wave) along a baseline, adapted from v2's
4976
- * tile: one `Q` hump then reflected `T` segments alternate up/down across the run. */
4977
- function squigglePath(x, y, w, amp) {
5033
+ /** A smooth squiggle (quadratic-bezier wave) along an ARBITRARY baseline,
5034
+ * generated in the (û, n̂) basis: one `Q` hump then reflected `T` segments
5035
+ * alternate across the run. Reflection is affine-invariant, so an upright
5036
+ * quad reproduces the old axis-aligned wave byte-for-byte. `n̂` points from
5037
+ * the baseline toward the ascent side; humps rise toward the text. */
5038
+ function squigglePath(start, u, n, w, amp) {
4978
5039
  const half = Math.max(2, amp * 1.5);
4979
- let d = `M ${num(x)} ${num(y)} Q ${num(x + half / 2)} ${num(y - amp)} ${num(x + half)} ${num(y)}`;
4980
- for (let px = x + half; px + half <= x + w + .5; px += half) d += ` T ${num(px + half)} ${num(y)}`;
5040
+ const at = (t, off) => ({
5041
+ x: start.x + u.x * t + n.x * off,
5042
+ y: start.y + u.y * t + n.y * off
5043
+ });
5044
+ const p = (v) => `${num(v.x)} ${num(v.y)}`;
5045
+ const hump = at(half / 2, amp);
5046
+ let d = `M ${p(at(0, 0))} Q ${p(hump)} ${p(at(half, 0))}`;
5047
+ for (let t = half; t + half <= w + .5; t += half) d += ` T ${p(at(t + half, 0))}`;
4981
5048
  return d;
4982
5049
  }
4983
- /** Per-subtype markup nodes. Quads are axis-aligned per-line rects (UL,UR,LL,LR);
4984
- * the colour is the markup `/C` (our model keeps stroke==fill). */
5050
+ /** Per-subtype markup nodes on the quads' own edges (corner-NAMED TextQuads:
5051
+ * upper = ascent side, lower = baseline side, start → end along the frame).
5052
+ * The colour is the markup `/C` (our model keeps stroke==fill). Rotated and
5053
+ * sheared cells draw along their true baselines; upright output is identical
5054
+ * to the old axis-aligned math. */
4985
5055
  function markupScene(subtype, quads, style) {
4986
5056
  const color = style.color;
4987
5057
  const opacity = style.opacity;
4988
5058
  const nodes = [];
4989
5059
  for (const q of quads) {
4990
- const x = q[0].x;
4991
- const y = q[0].y;
4992
- const w = q[1].x - q[0].x;
4993
- const h = q[2].y - q[0].y;
5060
+ const down = {
5061
+ x: q.lowerStart.x - q.upperStart.x,
5062
+ y: q.lowerStart.y - q.upperStart.y
5063
+ };
5064
+ const h = Math.hypot(down.x, down.y);
5065
+ const wVec = {
5066
+ x: q.lowerEnd.x - q.lowerStart.x,
5067
+ y: q.lowerEnd.y - q.lowerStart.y
5068
+ };
5069
+ const w = Math.hypot(wVec.x, wVec.y);
4994
5070
  if (w <= 0 || h <= 0) continue;
5071
+ const n = {
5072
+ x: down.x / h,
5073
+ y: down.y / h
5074
+ };
4995
5075
  const lw = Math.min(2.5, Math.max(.75, h * .06));
4996
- if (subtype === "underline") {
4997
- const yy = y + h - lw;
4998
- nodes.push({
4999
- kind: "line",
5000
- a: {
5001
- x,
5002
- y: yy
5003
- },
5004
- b: {
5005
- x: x + w,
5006
- y: yy
5007
- },
5008
- paint: {
5009
- stroke: color,
5010
- width: lw,
5011
- opacity,
5012
- blend: blendFor(style)
5013
- }
5014
- });
5015
- } else if (subtype === "strikeout") {
5016
- const yy = y + h / 2;
5017
- nodes.push({
5018
- kind: "line",
5019
- a: {
5020
- x,
5021
- y: yy
5022
- },
5023
- b: {
5024
- x: x + w,
5025
- y: yy
5026
- },
5027
- paint: {
5028
- stroke: color,
5029
- width: lw,
5030
- opacity,
5031
- blend: blendFor(style)
5032
- }
5033
- });
5034
- } else if (subtype === "squiggly") {
5076
+ if (subtype === "underline") nodes.push({
5077
+ kind: "line",
5078
+ a: {
5079
+ x: q.lowerStart.x - n.x * lw,
5080
+ y: q.lowerStart.y - n.y * lw
5081
+ },
5082
+ b: {
5083
+ x: q.lowerEnd.x - n.x * lw,
5084
+ y: q.lowerEnd.y - n.y * lw
5085
+ },
5086
+ paint: {
5087
+ stroke: color,
5088
+ width: lw,
5089
+ opacity,
5090
+ blend: blendFor(style)
5091
+ }
5092
+ });
5093
+ else if (subtype === "strikeout") nodes.push({
5094
+ kind: "line",
5095
+ a: {
5096
+ x: (q.upperStart.x + q.lowerStart.x) / 2,
5097
+ y: (q.upperStart.y + q.lowerStart.y) / 2
5098
+ },
5099
+ b: {
5100
+ x: (q.upperEnd.x + q.lowerEnd.x) / 2,
5101
+ y: (q.upperEnd.y + q.lowerEnd.y) / 2
5102
+ },
5103
+ paint: {
5104
+ stroke: color,
5105
+ width: lw,
5106
+ opacity,
5107
+ blend: blendFor(style)
5108
+ }
5109
+ });
5110
+ else if (subtype === "squiggly") {
5035
5111
  const amp = Math.min(2, Math.max(1, h * .08));
5112
+ const u = {
5113
+ x: wVec.x / w,
5114
+ y: wVec.y / w
5115
+ };
5116
+ const start = {
5117
+ x: q.lowerStart.x - n.x * amp,
5118
+ y: q.lowerStart.y - n.y * amp
5119
+ };
5036
5120
  nodes.push({
5037
5121
  kind: "path",
5038
- d: squigglePath(x, y + h - amp, w, amp),
5122
+ d: squigglePath(start, u, {
5123
+ x: -n.x,
5124
+ y: -n.y
5125
+ }, w, amp),
5039
5126
  paint: {
5040
5127
  stroke: color,
5041
5128
  width: lw,
@@ -5044,13 +5131,9 @@ function markupScene(subtype, quads, style) {
5044
5131
  }
5045
5132
  });
5046
5133
  } else nodes.push({
5047
- kind: "rect",
5048
- rect: {
5049
- x,
5050
- y,
5051
- width: w,
5052
- height: h
5053
- },
5134
+ kind: "poly",
5135
+ points: textQuadRing(q),
5136
+ closed: true,
5054
5137
  paint: {
5055
5138
  fill: color,
5056
5139
  opacity,
@@ -5060,23 +5143,40 @@ function markupScene(subtype, quads, style) {
5060
5143
  }
5061
5144
  return nodes;
5062
5145
  }
5063
- /** A redact mark's regions: per-quad boxes (text marks) or the rect (area). */
5146
+ const rectRing = (r) => [
5147
+ {
5148
+ x: r.x,
5149
+ y: r.y
5150
+ },
5151
+ {
5152
+ x: r.x + r.width,
5153
+ y: r.y
5154
+ },
5155
+ {
5156
+ x: r.x + r.width,
5157
+ y: r.y + r.height
5158
+ },
5159
+ {
5160
+ x: r.x,
5161
+ y: r.y + r.height
5162
+ }
5163
+ ];
5064
5164
  function redactRegions(geom) {
5065
5165
  if (geom.t === "quads") {
5066
5166
  const out = [];
5067
5167
  for (const q of geom.quads) {
5068
- const w = q[1].x - q[0].x;
5069
- const h = q[2].y - q[0].y;
5070
- if (w > 0 && h > 0) out.push({
5071
- x: q[0].x,
5072
- y: q[0].y,
5073
- width: w,
5074
- height: h
5168
+ const bounds = textQuadBounds(q);
5169
+ if (bounds.width > 0 && bounds.height > 0) out.push({
5170
+ ring: textQuadRing(q),
5171
+ bounds
5075
5172
  });
5076
5173
  }
5077
5174
  return out;
5078
5175
  }
5079
- if (geom.t === "rect") return [geom.rect];
5176
+ if (geom.t === "rect") return [{
5177
+ ring: rectRing(geom.rect),
5178
+ bounds: geom.rect
5179
+ }];
5080
5180
  return [];
5081
5181
  }
5082
5182
  /**
@@ -5142,9 +5242,10 @@ function redactScene(item) {
5142
5242
  width: item.style.strokeWidth || 1.5,
5143
5243
  opacity: item.style.opacity
5144
5244
  };
5145
- return regions.map((rect) => ({
5146
- kind: "rect",
5147
- rect,
5245
+ return regions.map((region) => ({
5246
+ kind: "poly",
5247
+ points: region.ring,
5248
+ closed: true,
5148
5249
  paint
5149
5250
  }));
5150
5251
  }
@@ -5154,13 +5255,14 @@ function redactScene(item) {
5154
5255
  fill: item.style.interiorColor,
5155
5256
  opacity: 1
5156
5257
  };
5157
- for (const rect of regions) nodes.push({
5158
- kind: "rect",
5159
- rect,
5258
+ for (const region of regions) nodes.push({
5259
+ kind: "poly",
5260
+ points: region.ring,
5261
+ closed: true,
5160
5262
  paint: fill
5161
5263
  });
5162
5264
  }
5163
- if (item.label) for (const region of regions) nodes.push(...layoutRedactLabel(region, item.label, item.text));
5265
+ if (item.label) for (const region of regions) nodes.push(...layoutRedactLabel(region.bounds, item.label, item.text));
5164
5266
  return nodes;
5165
5267
  }
5166
5268
  /** The full painted scene for one annotation. */
@@ -5584,7 +5686,7 @@ function chrome(m, pon, pageBox, knobOffset = 24, view) {
5584
5686
  const style = effStyle(a, view);
5585
5687
  const caps = capsFor(a.subtype);
5586
5688
  const rot = geomRotation(g);
5587
- const obb = caps.rotatable ? obbFromGeom(g, style.strokeWidth, a.style.border) : null;
5689
+ const obb = obbFromGeom(g, style.strokeWidth, a.style.border);
5588
5690
  if (obb && rot !== 0) nodes.push({
5589
5691
  kind: "obb",
5590
5692
  corners: obb.corners,
@@ -5675,6 +5777,6 @@ function creationDraftAnchor(m) {
5675
5777
  };
5676
5778
  }
5677
5779
  //#endregion
5678
- export { DEFAULT_CHROME_GEOM, DRAWN_FLAGS, FLAG_KEYS, KINDS, MIN_DRAG, MITER_LIMIT, NO_ANNOTATION_FLAGS, RECT_HANDLES, ROTATE_KNOB_OFFSET, anchorModeOf, anchorOf, anchoredBox, anchoredGeom, anchoredStrokeWidth, annotContentsEditable, annotInteractive, annotTransformable, applyProps, calloutConnection, calloutLinePoints, canMove, capsFor, caretRectFromTextEnd, centroidOf, chrome, clampRectToBox, clickCreateGeom, cloudyBorderExtent, cloudyPath, computeMoveSnap, contentToPdfPoint, contentToPdfRect, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, flagsEqual, geomBounds, geomDragHandle, geomHandles, geomHit, geomPdfBounds, geomResetRotation, geomRotateAbout, geomRotation, geomScaleAbout, geomScene, geomTranslate, geomVisualBounds, groupCaps, groupKeyOf, groupMembers, groupResizeAnchor, groupResizeBox, groupResizeFactors, hitTest, initialModel, initialStyle, initialTextStyle, interactive, isRotatableGeom, isSelectable, mergeFlags, normalizeDeg, obbFromGeom, pageItems, pdfToContentPoint, pdfToContentRect, placeRotateKnob, pointInQuad, propsFor, quadIntersectsRect, readProp, rectFromPoints, resolveClickPlacement, rotateDraftDelta, rotateKnob, rotatedAabb, rotatedHandleCursor, scene, selectedItems, selectionAnchor, selectionBounds, selectionBoundsOnPage, selectionCenter, selectionKnob, selectionQuad, shapeRectFor, sharedProps, straightenInkStroke, styleFromProps, textBoxes, textStyleFromProps, transposedAboutCenter, unanchoredGeom, unionRect, update, uprightAnchoredRect, uprightRotation, viewable };
5780
+ export { DEFAULT_CHROME_GEOM, DRAWN_FLAGS, FLAG_KEYS, KINDS, MIN_DRAG, MITER_LIMIT, NO_ANNOTATION_FLAGS, RECT_HANDLES, ROTATE_KNOB_OFFSET, anchorModeOf, anchorOf, anchoredBox, anchoredGeom, anchoredStrokeWidth, annotContentsEditable, annotInteractive, annotTransformable, applyProps, calloutConnection, calloutLinePoints, canMove, capsFor, caretGeomFromAnchor, caretRectFromAnchor, caretRectFromTextEnd, centroidOf, chrome, clampRectToBox, clickCreateGeom, cloudyBorderExtent, cloudyPath, computeMoveSnap, contentToPdfPoint, contentToPdfRect, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, flagsEqual, geomBounds, geomDragHandle, geomHandles, geomHit, geomPdfBounds, geomResetRotation, geomRotateAbout, geomRotation, geomScaleAbout, geomScene, geomTranslate, geomVisualBounds, groupCaps, groupKeyOf, groupMembers, groupResizeAnchor, groupResizeBox, groupResizeFactors, hitTest, initialModel, initialStyle, initialTextStyle, interactive, isRotatableGeom, isSelectable, mergeFlags, normalizeDeg, obbFromGeom, pageItems, pdfToContentPoint, pdfToContentRect, placeRotateKnob, pointInQuad, propsFor, quadIntersectsRect, readProp, rectFromPoints, resolveClickPlacement, rotateDraftDelta, rotateKnob, rotatedAabb, rotatedHandleCursor, scene, selectedItems, selectionAnchor, selectionBounds, selectionBoundsOnPage, selectionCenter, selectionKnob, selectionQuad, shapeRectFor, sharedProps, straightenInkStroke, styleFromProps, textBoxes, textStyleFromProps, transposedAboutCenter, unanchoredGeom, unionRect, update, uprightAnchoredRect, uprightRotation, viewable };
5679
5781
 
5680
5782
  //# sourceMappingURL=index.js.map