@embedpdf/core-annotation 3.0.0-next.1 → 3.0.0-next.10

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",
@@ -635,11 +635,51 @@ const interactive = (f) => !f.hidden && !f.noView && !f.readOnly;
635
635
  * (ISO 32000 — a ReadOnly form field must still be movable by a form designer;
636
636
  * the form-filling layer enforces field ReadOnly itself). */
637
637
  const annotInteractive = (a) => capsFor(a.subtype).ignoresReadOnly ? !a.flags.hidden && !a.flags.noView : interactive(a.flags);
638
- /** Geometry/style/delete mutations — `locked` freezes the OBJECT, not its
639
- * contents (that's `lockedContents`). */
640
- const annotTransformable = (a) => annotInteractive(a) && !a.flags.locked;
638
+ /**
639
+ * Geometry/style mutations — `locked` freezes the OBJECT, not its contents
640
+ * (that's `lockedContents`), AND the session must hold update authority over
641
+ * the record. One predicate for hit-test, chrome, and props alike, so a
642
+ * record you may not edit renders and behaves exactly like a locked one.
643
+ */
644
+ const annotTransformable = (a) => annotInteractive(a) && !a.flags.locked && (a.authority?.update ?? true);
645
+ /** Deletion — the delete half of the authority split (a narrowed grant can
646
+ * allow update but not delete, or vice versa). Flags gate like transforms. */
647
+ const annotDeletable = (a) => annotInteractive(a) && !a.flags.locked && (a.authority?.delete ?? true);
641
648
  /** `/Contents` text edits — the contents counterpart of `locked`. */
642
- const annotContentsEditable = (a) => annotInteractive(a) && !a.flags.lockedContents;
649
+ const annotContentsEditable = (a) => annotInteractive(a) && !a.flags.lockedContents && (a.authority?.update ?? true);
650
+ //#endregion
651
+ //#region src/plane.ts
652
+ const nonEmpty = (v) => typeof v === "string" && v !== "";
653
+ /** True when this annotation belongs to the conversation plane only —
654
+ * never painted, hit, marquee-selected, or counted into a page's
655
+ * appearance epoch. */
656
+ function isConversationOnly(a) {
657
+ if (a.irt !== void 0 && a.group === void 0) return true;
658
+ if (a.subtype === "text" && a.data?.subtype === "text") {
659
+ if (nonEmpty(a.data.state) || nonEmpty(a.data.stateModel)) return true;
660
+ }
661
+ return false;
662
+ }
663
+ /**
664
+ * A link CHILD attached to another annotation — a `/Link` grouped
665
+ * (`/IRT` + `/RT /Group`) under a non-link parent. Pure substrate: it is
666
+ * the parent's `link` PROPERTY while authoring (derived via `linkOf`) and
667
+ * the navigation plane's anchor while reading — never painted, never hit
668
+ * as itself. Its rect is RECONCILED from the parent's geometry, so direct
669
+ * manipulation would be overwritten anyway.
670
+ */
671
+ function isAttachedLink(a) {
672
+ return a.subtype === "link" && a.group !== void 0;
673
+ }
674
+ /**
675
+ * The ONE page-surface cull: everything that lives in the substrate but is
676
+ * not a page visual of its own — conversation members (replies, review
677
+ * states) and attached link children. Paint order, hit-testing, marquee
678
+ * and the appearance epoch all filter through THIS, never the parts.
679
+ */
680
+ function isSubstrateOnly(a) {
681
+ return isConversationOnly(a) || isAttachedLink(a);
682
+ }
643
683
  //#endregion
644
684
  //#region src/group.ts
645
685
  /**
@@ -650,7 +690,16 @@ const annotContentsEditable = (a) => annotInteractive(a) && !a.flags.lockedConte
650
690
  * carries `group = <primary's id>` (its `refKey`). The primary is NOT stamped —
651
691
  * it is simply the annotation whose `id` equals the group key, so membership is
652
692
  * the set of annotations pointing at it plus the primary itself.
693
+ *
694
+ * ATTACHED LINK CHILDREN are the exception: they ride the same `/RT /Group`
695
+ * wire mechanism but are NOT part of the composite visual — they're substrate
696
+ * plumbing (the parent's link property). They never count as members here, so
697
+ * a linked square still selects as a SINGLE annotation with handles, never
698
+ * shows Ungroup, and the ungroup verb can never strip an attached link's
699
+ * `/IRT` (which would orphan it into an unmanaged standalone document link).
653
700
  */
701
+ /** A subordinate that counts toward a VISUAL group (not link plumbing). */
702
+ const visualMember = (a) => !!a && !isAttachedLink(a);
654
703
  function groupCaps(m, ids) {
655
704
  const members = ids.map((id) => m.byId[id]).filter((a) => !!a);
656
705
  if (members.length === 0) return {
@@ -674,8 +723,11 @@ function groupCaps(m, ids) {
674
723
  function groupKeyOf(m, id) {
675
724
  const a = m.byId[id];
676
725
  if (!a) return null;
677
- if (a.group) return a.group;
678
- for (const other of m.order) if (m.byId[other]?.group === id) return id;
726
+ if (a.group) return isAttachedLink(a) ? null : a.group;
727
+ for (const other of m.order) {
728
+ const sub = m.byId[other];
729
+ if (sub?.group === id && visualMember(sub)) return id;
730
+ }
679
731
  return null;
680
732
  }
681
733
  /**
@@ -688,7 +740,10 @@ function groupMembers(m, id) {
688
740
  if (key == null) return [id];
689
741
  const members = [];
690
742
  if (m.byId[key]) members.push(key);
691
- for (const other of m.order) if (other !== key && m.byId[other]?.group === key) members.push(other);
743
+ for (const other of m.order) {
744
+ const sub = m.byId[other];
745
+ if (other !== key && sub?.group === key && visualMember(sub)) members.push(other);
746
+ }
692
747
  return members;
693
748
  }
694
749
  /** Union of every id's full group — the selection seen as whole groups. */
@@ -1463,9 +1518,12 @@ const DEFAULT_CHROME_GEOM = {
1463
1518
  };
1464
1519
  /** Normalize degrees into `[0, 360)`. */
1465
1520
  const normalizeDeg = (d) => (d % 360 + 360) % 360;
1466
- /** A geom's applied rotation (deg), or 0 for the non-rotatable kinds. */
1521
+ /** A geom's applied rotation (deg), or 0 for the non-rotatable kinds. A
1522
+ * caret's `rot` is AUTHORING metadata (its text's baseline tilt): reported
1523
+ * here so the renderer and selection chrome follow it, while the caret's
1524
+ * caps (not movable/resizable) keep every rotate gesture away from it. */
1467
1525
  function geomRotation(g) {
1468
- if (g.t === "rect" || g.t === "line" || g.t === "poly" || g.t === "ink" || g.t === "text") return g.rot ?? 0;
1526
+ if (g.t === "rect" || g.t === "line" || g.t === "poly" || g.t === "ink" || g.t === "text" || g.t === "caret") return g.rot ?? 0;
1469
1527
  return 0;
1470
1528
  }
1471
1529
  const rectCenter = (r) => ({
@@ -1487,7 +1545,7 @@ function centroidOf(g) {
1487
1545
  x: (g.a.x + g.b.x) / 2,
1488
1546
  y: (g.a.y + g.b.y) / 2
1489
1547
  };
1490
- const pts = g.t === "poly" ? g.points : g.t === "ink" ? g.strokes.flat() : g.quads.flat();
1548
+ const pts = g.t === "poly" ? g.points : g.t === "ink" ? g.strokes.flat() : g.quads.flatMap(textQuadPoints);
1491
1549
  let sx = 0;
1492
1550
  let sy = 0;
1493
1551
  for (const p of pts) {
@@ -1500,9 +1558,12 @@ function centroidOf(g) {
1500
1558
  y: sy / n
1501
1559
  };
1502
1560
  }
1503
- /** Is this kind rotatable (the geometry carries a meaningful `rot`)? */
1561
+ /** Does the geometry carry a meaningful `rot` (an oriented local box exists)?
1562
+ * ORIENTATION is a geometry fact; whether the USER may rotate is the separate
1563
+ * `caps.rotatable` gate — a caret is oriented (it rides its text's tilt) yet
1564
+ * offers no rotate gesture. */
1504
1565
  function isRotatableGeom(g) {
1505
- return g.t === "rect" || g.t === "line" || g.t === "poly" || g.t === "ink" || g.t === "text" && !g.callout;
1566
+ return g.t === "rect" || g.t === "line" || g.t === "poly" || g.t === "ink" || g.t === "caret" || g.t === "text" && !g.callout;
1506
1567
  }
1507
1568
  /**
1508
1569
  * Rotate a geom by `deltaDeg` (clockwise) about `pivot`.
@@ -1511,7 +1572,10 @@ function isRotatableGeom(g) {
1511
1572
  * pivot IS the box centre this is a pure `rot += delta`.
1512
1573
  * - VERTEX (`line`/`poly`/`ink`): map every point through the rotation AND bump
1513
1574
  * the advisory `rot` (the points stay the authoritative visual).
1514
- * Non-rotatable kinds (caret/quads, callouts) are returned unchanged.
1575
+ * Kinds without a rotate VERB are returned unchanged: quads and callouts, and
1576
+ * also the caret — oriented (`isRotatableGeom`) but text-anchored, so its tilt
1577
+ * is authoring metadata that no gesture edits (`geomResetRotation` still
1578
+ * clears it).
1515
1579
  */
1516
1580
  function geomRotateAbout(g, pivot, deltaDeg) {
1517
1581
  if (deltaDeg === 0) return g;
@@ -1571,7 +1635,7 @@ function apFrameSize(g) {
1571
1635
  width: g.rect.width,
1572
1636
  height: g.rect.height
1573
1637
  };
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());
1638
+ 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
1639
  return {
1576
1640
  width: b.width,
1577
1641
  height: b.height
@@ -1680,7 +1744,7 @@ function fitStampBox(center, desired, page, rotCW) {
1680
1744
  function geomResetRotation(g) {
1681
1745
  const rot = geomRotation(g);
1682
1746
  if (!rot) return g;
1683
- if (g.t === "rect" || g.t === "text") return {
1747
+ if (g.t === "rect" || g.t === "text" || g.t === "caret") return {
1684
1748
  ...g,
1685
1749
  rot: 0
1686
1750
  };
@@ -1697,7 +1761,7 @@ function geomResetRotation(g) {
1697
1761
  function obbFromGeom(g, strokeWidth, border) {
1698
1762
  if (!isRotatableGeom(g)) return null;
1699
1763
  const rot = geomRotation(g);
1700
- if (g.t === "rect" || g.t === "text") {
1764
+ if (g.t === "rect" || g.t === "text" || g.t === "caret") {
1701
1765
  const c = rectCenter(g.rect);
1702
1766
  return {
1703
1767
  corners: rectCornerPoints(g.rect).map((p) => rotatePoint(p, c, rot)),
@@ -2002,6 +2066,77 @@ function caretRectFromTextEnd(lineRect) {
2002
2066
  height
2003
2067
  };
2004
2068
  }
2069
+ /**
2070
+ * Caret box for a text-edit anchor: half the glyph's ink height, centered on
2071
+ * the TRAILING baseline corner in READING direction (`advance` decides which
2072
+ * end — RTL carets land on the visual left), sitting on the baseline. The box
2073
+ * stays axis-aligned; use {@link caretGeomFromAnchor} for the tilt-carrying
2074
+ * geometry.
2075
+ */
2076
+ function caretRectFromAnchor(anchor) {
2077
+ const q = anchor.glyphQuad;
2078
+ const ink = Math.hypot(q.lowerStart.x - q.upperStart.x, q.lowerStart.y - q.upperStart.y);
2079
+ const size = Math.max(ink / 2, 1);
2080
+ const corner = anchor.advance > 0 ? q.lowerEnd : q.lowerStart;
2081
+ return {
2082
+ x: corner.x - size / 2,
2083
+ y: corner.y - size,
2084
+ width: size,
2085
+ height: size
2086
+ };
2087
+ }
2088
+ /** Rotations closer than ~0.05° to upright stay upright (float noise guard). */
2089
+ const CARET_ROT_EPSILON = .05;
2090
+ /**
2091
+ * Caret GEOMETRY for a text-edit anchor: the box-family pair — an UNROTATED
2092
+ * box whose centre sits half a caret-size ascent-ward of the trailing
2093
+ * baseline corner, plus `rot` = the text's baseline tilt (deg, CW in y-down
2094
+ * content space). Rotating the box about its centre by `rot` lands it
2095
+ * hugging the rotated baseline, symbol pointing at its text. For upright
2096
+ * anchors this degenerates EXACTLY to {@link caretRectFromAnchor} with no
2097
+ * `rot` key — the dominant case is byte-identical.
2098
+ */
2099
+ function caretGeomFromAnchor(anchor) {
2100
+ const q = anchor.glyphQuad;
2101
+ const ink = Math.hypot(q.lowerStart.x - q.upperStart.x, q.lowerStart.y - q.upperStart.y);
2102
+ const size = Math.max(ink / 2, 1);
2103
+ const corner = anchor.advance > 0 ? q.lowerEnd : q.lowerStart;
2104
+ const bx = q.lowerEnd.x - q.lowerStart.x;
2105
+ const by = q.lowerEnd.y - q.lowerStart.y;
2106
+ const rot = Math.hypot(bx, by) > 0 ? normalizeDeg(Math.atan2(by, bx) * 180 / Math.PI) : 0;
2107
+ if (rot < CARET_ROT_EPSILON || rot > 360 - CARET_ROT_EPSILON) return {
2108
+ t: "caret",
2109
+ rect: {
2110
+ x: corner.x - size / 2,
2111
+ y: corner.y - size,
2112
+ width: size,
2113
+ height: size
2114
+ }
2115
+ };
2116
+ const ux = (q.upperStart.x - q.lowerStart.x) / ink;
2117
+ const uy = (q.upperStart.y - q.lowerStart.y) / ink;
2118
+ const cx = corner.x + ux * size / 2;
2119
+ const cy = corner.y + uy * size / 2;
2120
+ return {
2121
+ t: "caret",
2122
+ rect: {
2123
+ x: cx - size / 2,
2124
+ y: cy - size / 2,
2125
+ width: size,
2126
+ height: size
2127
+ },
2128
+ rot
2129
+ };
2130
+ }
2131
+ /** Map a TextQuad's corners through a point function (names ride along). */
2132
+ function mapTextQuad(q, f) {
2133
+ return {
2134
+ upperStart: f(q.upperStart),
2135
+ upperEnd: f(q.upperEnd),
2136
+ lowerStart: f(q.lowerStart),
2137
+ lowerEnd: f(q.lowerEnd)
2138
+ };
2139
+ }
2005
2140
  /** Miter limit shared by the bounds math AND the SVG renderer (`stroke-miterlimit`),
2006
2141
  * so the computed box and the drawn stroke always agree on where a sharp join
2007
2142
  * bevels instead of spiking. 10 = the PDF default (also what the baked /AP uses). */
@@ -2150,7 +2285,7 @@ function geomVisualBounds(g, strokeWidth, border) {
2150
2285
  return expandRect(unionRect(all), strokeWidth / 2);
2151
2286
  }
2152
2287
  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);
2288
+ if (g.t === "quads") return expandRect(unionRect(g.quads.flatMap(textQuadPoints)), strokeWidth / 2);
2154
2289
  if (g.t === "ink") return expandRect(unionRect(g.strokes.flat()), strokeWidth / 2);
2155
2290
  const pts = strokeOutlinePoints(g.t === "line" ? [g.a, g.b] : g.points, g.t === "poly" && g.closed, strokeWidth);
2156
2291
  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 +2421,7 @@ function geomBounds(g) {
2286
2421
  if (g.t === "line") return rectFromPoints(g.a, g.b);
2287
2422
  if (g.t === "poly") return unionRect(g.points);
2288
2423
  if (g.t === "ink") return unionRect(g.strokes.flat());
2289
- return unionRect(g.quads.flat());
2424
+ return unionRect(g.quads.flatMap(textQuadPoints));
2290
2425
  }
2291
2426
  /**
2292
2427
  * Is the content point ON the annotation: within `margin` of the stroke, or
@@ -2299,7 +2434,7 @@ function geomBounds(g) {
2299
2434
  */
2300
2435
  function geomHit(g, p, margin, filled, strokeWidth) {
2301
2436
  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));
2437
+ 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
2438
  if (g.t === "caret") return rectContains(expandRect(g.rect, margin), p);
2304
2439
  if (g.t === "text") {
2305
2440
  const rot = g.callout ? g.rot ?? 0 : 0;
@@ -2374,7 +2509,7 @@ function geomHit(g, p, margin, filled, strokeWidth) {
2374
2509
  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
2510
  return false;
2376
2511
  }
2377
- return g.quads.some((q) => rectContains(unionRect(q), p));
2512
+ return g.quads.some((q) => pointInQuad(p, textQuadRing(q)));
2378
2513
  }
2379
2514
  function geomHandles(g) {
2380
2515
  if (g.t === "rect" || g.t === "text") {
@@ -2463,7 +2598,7 @@ function geomTranslate(g, d) {
2463
2598
  };
2464
2599
  return {
2465
2600
  ...g,
2466
- quads: g.quads.map((q) => q.map(mv))
2601
+ quads: g.quads.map((q) => mapTextQuad(q, mv))
2467
2602
  };
2468
2603
  }
2469
2604
  const OPPOSITE_HANDLE = {
@@ -2554,7 +2689,18 @@ function geomDragHandle(g, handle, to) {
2554
2689
  function geomScene(g, strokeWidth = 0, border) {
2555
2690
  if (g.t === "text") {
2556
2691
  if (!g.callout) return [];
2557
- const pts = calloutLinePoints(g);
2692
+ const pts = [...calloutLinePoints(g)];
2693
+ if (strokeWidth > 0 && pts.length >= 2) {
2694
+ const last = pts[pts.length - 1];
2695
+ const prev = pts[pts.length - 2];
2696
+ const dx = last.x - prev.x;
2697
+ const dy = last.y - prev.y;
2698
+ const len = Math.hypot(dx, dy);
2699
+ if (len > 0) pts[pts.length - 1] = {
2700
+ x: last.x + dx / len * (strokeWidth / 2),
2701
+ y: last.y + dy / len * (strokeWidth / 2)
2702
+ };
2703
+ }
2558
2704
  const nodes = [{
2559
2705
  kind: "poly",
2560
2706
  points: pts,
@@ -2563,15 +2709,16 @@ function geomScene(g, strokeWidth = 0, border) {
2563
2709
  const seg = calloutEndingSeg(pts, g.callout.ending);
2564
2710
  if (seg) nodes.push(...endingNodes(seg.tip, seg.angle, seg.ending, strokeWidth));
2565
2711
  if (strokeWidth > 0) {
2712
+ const r = insetRect(g.rect, strokeWidth / 2);
2566
2713
  const rot = g.rot ?? 0;
2567
2714
  const c = rectCenter(g.rect);
2568
2715
  nodes.push(rot ? {
2569
2716
  kind: "poly",
2570
- points: rectCornerPoints(g.rect).map((p) => rotatePoint(p, c, rot)),
2717
+ points: rectCornerPoints(r).map((p) => rotatePoint(p, c, rot)),
2571
2718
  closed: true
2572
2719
  } : {
2573
2720
  kind: "rect",
2574
- rect: g.rect
2721
+ rect: r
2575
2722
  });
2576
2723
  }
2577
2724
  return nodes;
@@ -2636,12 +2783,7 @@ function geomScene(g, strokeWidth = 0, border) {
2636
2783
  }));
2637
2784
  return g.quads.map((q) => ({
2638
2785
  kind: "poly",
2639
- points: [
2640
- q[0],
2641
- q[1],
2642
- q[3],
2643
- q[2]
2644
- ],
2786
+ points: textQuadRing(q),
2645
2787
  closed: true
2646
2788
  }));
2647
2789
  }
@@ -2841,7 +2983,9 @@ function anchoredBox(box, anchor, mode, view) {
2841
2983
  * (always beneath), then every other kind, each group preserving creation
2842
2984
  * z-order. The ONE z-order shared by rendering (`pageItems`) and hit-testing —
2843
2985
  * and the ONE visibility cull: what `/F` hides (hidden / un-engaged noView)
2844
- * neither paints nor hits, so an invisible annotation can never eat a click. */
2986
+ * neither paints nor hits, so an invisible annotation can never eat a click.
2987
+ * Conversation-plane annotations (replies, review-status states) are culled
2988
+ * here too — dialogue lives in the comments UI, never on the page. */
2845
2989
  function paintOrder(m, pon) {
2846
2990
  const markup = [];
2847
2991
  const other = [];
@@ -2849,6 +2993,7 @@ function paintOrder(m, pon) {
2849
2993
  const a = m.byId[id];
2850
2994
  if (!a || a.pon !== pon) continue;
2851
2995
  if (!viewable(a.flags, m.selected.includes(id))) continue;
2996
+ if (isSubstrateOnly(a)) continue;
2852
2997
  (isMarkup(a.subtype) ? markup : other).push(id);
2853
2998
  }
2854
2999
  return [...markup, ...other];
@@ -3029,6 +3174,24 @@ function cursorAt(m, pon, p, geom, strokeMargin, pageBox, inert, view) {
3029
3174
  return null;
3030
3175
  }
3031
3176
  //#endregion
3177
+ //#region src/links.ts
3178
+ /** Every attached link child of `parentId`, in z-order (multi-segment
3179
+ * markup parents carry several children with one shared target). */
3180
+ function linkChildrenOf(m, parentId) {
3181
+ const out = [];
3182
+ for (const id of m.order) {
3183
+ const a = m.byId[id];
3184
+ if (a && isAttachedLink(a) && a.group === parentId) out.push(a);
3185
+ }
3186
+ return out;
3187
+ }
3188
+ /** The parent's link target, derived from its first attached child — the
3189
+ * read side of the `syncLink` reconciler. Null when no child exists. */
3190
+ function linkOf(m, parentId) {
3191
+ const d = linkChildrenOf(m, parentId)[0]?.data;
3192
+ return d?.subtype === "link" ? d.target ?? null : null;
3193
+ }
3194
+ //#endregion
3032
3195
  //#region src/props.ts
3033
3196
  /**
3034
3197
  * The flat property vocabulary over the model: read and apply
@@ -3065,6 +3228,9 @@ const textStyleFromProps = (p) => ({
3065
3228
  fontColor: p.fontColor,
3066
3229
  textAlign: p.textAlign
3067
3230
  });
3231
+ /** Does this kind's prop table declare `link` (attachable link)? The link
3232
+ * KIND itself also declares it, but stores its own `/A` instead. */
3233
+ const kindTakesLink = (subtype) => propsFor(subtype).some((s) => s.key === "link");
3068
3234
  /** A geom that carries `/LE` endings: a line, or an OPEN poly (polyline). */
3069
3235
  const endingsGeom = (g) => g.t === "line" || g.t === "poly" && !g.closed;
3070
3236
  /**
@@ -3150,7 +3316,7 @@ function applyProps(a, patch) {
3150
3316
  ...next,
3151
3317
  icon: patch.icon
3152
3318
  };
3153
- if (patch.link !== void 0 && takes.has("link")) next = {
3319
+ if (patch.link !== void 0 && takes.has("link") && a.subtype === "link") next = {
3154
3320
  ...next,
3155
3321
  link: patch.link
3156
3322
  };
@@ -3664,10 +3830,10 @@ function update(m, msg) {
3664
3830
  case "createPointer": return createPointer(m, msg.phase, msg.subtype, msg.in, msg.preset, msg.intent, msg.deferInkCommit, msg.straightenInk, msg.clickCreate, msg.flags);
3665
3831
  case "finishInkDraft": return finishInkCreate(m);
3666
3832
  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);
3833
+ case "createCaret": return createCaret(m, msg.pon, msg.anchor, msg.flags);
3834
+ case "createReplaceText": return createReplaceText(m, msg.pon, msg.quads, msg.anchor, msg.preset);
3835
+ case "createMarkup": return createMarkup(m, msg.subtype, msg.pon, msg.quads, msg.preset, msg.flags);
3836
+ case "setMarkupPreview": return setMarkupPreview(m, msg.subtype, msg.quadsByPage, msg.preset);
3671
3837
  case "clearMarkupPreview": return m.preview ? [{
3672
3838
  ...m,
3673
3839
  preview: null
@@ -3712,6 +3878,7 @@ function update(m, msg) {
3712
3878
  draft: null
3713
3879
  }, []];
3714
3880
  case "loaded": return [mergeLoaded(m, msg.annots), []];
3881
+ case "hydrated": return [hydrateAnnots(m, msg.annots, msg.bumpAp ?? false), []];
3715
3882
  case "created": return [reconcile(m, msg.tempId, msg.id, msg.ref), []];
3716
3883
  case "createFailed": return [removeAnnots(m, [msg.tempId]), []];
3717
3884
  case "upsert": return [upsertAnnots(m, msg.annots, msg.bumpAp), []];
@@ -4468,33 +4635,22 @@ function finishPolyCreate(m) {
4468
4635
  id
4469
4636
  }]];
4470
4637
  }
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
- ]);
4638
+ /** Drop degenerate segment quads (zero-length baseline or ink extent). Area is
4639
+ * the cross product of the two edge vectors — orientation-safe. */
4640
+ const usableQuads = (quads) => quads.filter((q) => {
4641
+ const ux = q.upperEnd.x - q.upperStart.x;
4642
+ const uy = q.upperEnd.y - q.upperStart.y;
4643
+ const sx = q.lowerStart.x - q.upperStart.x;
4644
+ const sy = q.lowerStart.y - q.upperStart.y;
4645
+ return Math.abs(ux * sy - uy * sx) > 0;
4646
+ });
4491
4647
  /**
4492
4648
  * Build a text-markup annotation from the selection's per-line rects. The new
4493
4649
  * annotation is `vector` (rendered live by the overlay) and selected, mirroring
4494
4650
  * `createPointer`. One call per page the selection spans. Clears any live preview.
4495
4651
  */
4496
- function createMarkup(m, subtype, pon, rects, preset = subtype, flags) {
4497
- const quads = rectsToQuads(rects);
4652
+ function createMarkup(m, subtype, pon, segmentQuads, preset = subtype, flags) {
4653
+ const quads = usableQuads(segmentQuads);
4498
4654
  if (!quads.length) return [m, []];
4499
4655
  const id = `tmp:${m.seq + 1}`;
4500
4656
  const annot = {
@@ -4535,9 +4691,9 @@ function createMarkup(m, subtype, pon, rects, preset = subtype, flags) {
4535
4691
  * (`/IT /StrikeOutTextEdit`, `/IRT` caret, `/RT /Group`). Persistence performs
4536
4692
  * the two ordered writes and rolls the primary back if the subordinate fails.
4537
4693
  */
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, []];
4694
+ function createReplaceText(m, pon, segmentQuads, anchor, preset = "replace-text") {
4695
+ const quads = usableQuads(segmentQuads);
4696
+ if (!quads.length) return [m, []];
4541
4697
  const primaryId = `tmp:${m.seq + 1}`;
4542
4698
  const strikeoutId = `tmp:${m.seq + 2}`;
4543
4699
  const style = styleFromProps(defaultsFor(m, preset));
@@ -4547,10 +4703,7 @@ function createReplaceText(m, pon, rects, textEndRect, preset = "replace-text")
4547
4703
  pon,
4548
4704
  subtype: "caret",
4549
4705
  intent: "replace",
4550
- geom: {
4551
- t: "caret",
4552
- rect: caretRectFromTextEnd(textEndRect)
4553
- },
4706
+ geom: caretGeomFromAnchor(anchor),
4554
4707
  style,
4555
4708
  flags: DRAWN_FLAGS,
4556
4709
  source: "vector"
@@ -4593,20 +4746,17 @@ function createReplaceText(m, pon, rects, textEndRect, preset = "replace-text")
4593
4746
  members: [strikeoutId]
4594
4747
  }]];
4595
4748
  }
4596
- function createCaret(m, pon, textEndRect, flags) {
4597
- if (textEndRect.width <= 0 || textEndRect.height <= 0) return [m, []];
4749
+ function createCaret(m, pon, anchor, flags) {
4750
+ const caretGeom = caretGeomFromAnchor(anchor);
4751
+ if (caretGeom.rect.width <= 0 || caretGeom.rect.height <= 0) return [m, []];
4598
4752
  const id = `tmp:${m.seq + 1}`;
4599
- const def = defaultsFor(m, "caret");
4600
4753
  const annot = {
4601
4754
  id,
4602
4755
  ref: null,
4603
4756
  pon,
4604
4757
  subtype: "caret",
4605
- geom: {
4606
- t: "caret",
4607
- rect: caretRectFromTextEnd(textEndRect)
4608
- },
4609
- style: styleFromProps(def),
4758
+ geom: caretGeom,
4759
+ style: styleFromProps(defaultsFor(m, "caret")),
4610
4760
  flags: {
4611
4761
  ...DRAWN_FLAGS,
4612
4762
  ...flags
@@ -4629,11 +4779,11 @@ function createCaret(m, pon, textEndRect, flags) {
4629
4779
  id
4630
4780
  }]];
4631
4781
  }
4632
- /** Set / replace the live markup preview from the selection's per-page rects. */
4633
- function setMarkupPreview(m, subtype, rectsByPage, preset = subtype) {
4782
+ /** Set / replace the live markup preview from the selection's per-page quads. */
4783
+ function setMarkupPreview(m, subtype, quadsByPage, preset = subtype) {
4634
4784
  const byPage = {};
4635
- for (const k in rectsByPage) {
4636
- const quads = rectsToQuads(rectsByPage[k]);
4785
+ for (const k in quadsByPage) {
4786
+ const quads = usableQuads(quadsByPage[k]);
4637
4787
  if (quads.length) byPage[Number(k)] = quads;
4638
4788
  }
4639
4789
  return [{
@@ -4661,8 +4811,16 @@ function setProps(m, patch) {
4661
4811
  for (const id of m.selected) {
4662
4812
  const a = byId[id];
4663
4813
  if (!a) continue;
4814
+ const linkIntent = patch.link !== void 0 && a.subtype !== "link" && kindTakesLink(a.subtype) && annotTransformable(a);
4664
4815
  const next = applyProps(a, patch);
4665
- if (!next) continue;
4816
+ if (!next) {
4817
+ if (linkIntent) fx.push({
4818
+ fx: "syncLink",
4819
+ id,
4820
+ target: patch.link ?? null
4821
+ });
4822
+ continue;
4823
+ }
4666
4824
  const linkChanged = next.link !== a.link;
4667
4825
  const otherChanged = next.style !== a.style || next.geom !== a.geom || next.text !== a.text || next.icon !== a.icon;
4668
4826
  byId[id] = capsFor(a.subtype).opaqueBody || !otherChanged ? next : toVector(next);
@@ -4674,9 +4832,10 @@ function setProps(m, patch) {
4674
4832
  keys
4675
4833
  }
4676
4834
  });
4677
- if (linkChanged && a.subtype !== "link") fx.push({
4835
+ if (linkIntent) fx.push({
4678
4836
  fx: "syncLink",
4679
- id
4837
+ id,
4838
+ target: patch.link ?? null
4680
4839
  });
4681
4840
  }
4682
4841
  return fx.length ? [{
@@ -4803,28 +4962,26 @@ function resetRotation(m) {
4803
4962
  function deleteSelection(m) {
4804
4963
  const deletable = m.selected.filter((id) => {
4805
4964
  const a = m.byId[id];
4806
- return !!a && annotTransformable(a);
4965
+ return !!a && annotDeletable(a);
4807
4966
  });
4808
4967
  if (!deletable.length) return [m, []];
4968
+ const withChildren = [...deletable, ...deletable.flatMap((id) => linkChildrenOf(m, id).map((c) => c.id))];
4809
4969
  const fx = [];
4810
- for (const id of deletable) {
4970
+ for (const id of withChildren) {
4811
4971
  const a = m.byId[id];
4812
4972
  if (a?.ref) fx.push({
4813
4973
  fx: "delete",
4814
4974
  ref: a.ref
4815
4975
  });
4816
- for (const childRef of a?.linkRefs ?? []) fx.push({
4817
- fx: "delete",
4818
- ref: childRef
4819
- });
4820
4976
  }
4821
- return [removeAnnots(m, deletable), fx];
4977
+ return [removeAnnots(m, withChildren), fx];
4822
4978
  }
4823
4979
  function annotsInBox(m, pon, a, b, inert, view) {
4824
4980
  const box = rectFromPoints(a, b);
4825
4981
  return m.order.filter((id) => {
4826
4982
  const annot = m.byId[id];
4827
4983
  if (annot?.pon !== pon || inert?.has(id) || !isSelectable(m, id)) return false;
4984
+ if (isSubstrateOnly(annot)) return false;
4828
4985
  return quadIntersectsRect(selectionQuad(anchoredGeom(annot.geom, anchorModeOf(annot), view), annot.style.strokeWidth, annot.style.border), box);
4829
4986
  });
4830
4987
  }
@@ -4843,6 +5000,39 @@ function mergeLoaded(m, annots) {
4843
5000
  };
4844
5001
  }
4845
5002
  /**
5003
+ * Whole-document hydration ingest: the snapshot is the committed truth.
5004
+ * Overwrites by id via `upsertAnnots` (gesture protection included), then
5005
+ * reaps committed entries the snapshot no longer contains — deletions that
5006
+ * happened before we subscribed (initial load) or inside a desync gap.
5007
+ * Gentler than `removeAnnots`: `tmp:` drafts and gesture-locked ids
5008
+ * survive, and an in-progress draft is NOT cancelled — reaped ids can
5009
+ * never be part of it (locked ids are excluded from reaping).
5010
+ */
5011
+ function hydrateAnnots(m, annots, bumpApFlag) {
5012
+ const incoming = new Set(annots.map((a) => a.id));
5013
+ const locked = draftIds(m.draft);
5014
+ const reaped = m.order.filter((id) => {
5015
+ if (incoming.has(id) || locked.has(id)) return false;
5016
+ const a = m.byId[id];
5017
+ return a !== void 0 && a.ref !== null;
5018
+ });
5019
+ let next = m;
5020
+ if (reaped.length > 0) {
5021
+ const gone = new Set(reaped);
5022
+ const byId = { ...m.byId };
5023
+ for (const id of reaped) delete byId[id];
5024
+ next = {
5025
+ ...m,
5026
+ byId,
5027
+ order: m.order.filter((id) => !gone.has(id)),
5028
+ selected: m.selected.filter((id) => !gone.has(id)),
5029
+ hovered: m.hovered && gone.has(m.hovered) ? null : m.hovered,
5030
+ editing: m.editing && gone.has(m.editing) ? null : m.editing
5031
+ };
5032
+ }
5033
+ return upsertAnnots(next, annots, bumpApFlag);
5034
+ }
5035
+ /**
4846
5036
  * Add-or-replace by id. Unlike `mergeLoaded` (which skips ids it already has,
4847
5037
  * for the bulk page read), this OVERWRITES — it's how the data API re-syncs an
4848
5038
  * annotation from the authoritative engine DTO and how a remote edit lands.
@@ -4858,13 +5048,8 @@ function upsertAnnots(m, annots, bumpAp = false) {
4858
5048
  if (dragging.has(a.id)) continue;
4859
5049
  if (!byId[a.id]) order.push(a.id);
4860
5050
  const prev = byId[a.id];
4861
- const fold = a.linkRefs === void 0 && prev?.linkRefs?.length && a.subtype !== "link" ? {
4862
- link: a.link !== void 0 ? a.link : prev.link,
4863
- linkRefs: prev.linkRefs
4864
- } : {};
4865
5051
  byId[a.id] = {
4866
5052
  ...a,
4867
- ...fold,
4868
5053
  apVersion: (prev?.apVersion ?? 0) + (bumpAp ? 1 : 0)
4869
5054
  };
4870
5055
  }
@@ -4972,70 +5157,99 @@ function shapePaint(style, closed) {
4972
5157
  ...style.border.kind === "cloudy" ? { join: "round" } : {}
4973
5158
  };
4974
5159
  }
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) {
5160
+ /** A smooth squiggle (quadratic-bezier wave) along an ARBITRARY baseline,
5161
+ * generated in the (û, n̂) basis: one `Q` hump then reflected `T` segments
5162
+ * alternate across the run. Reflection is affine-invariant, so an upright
5163
+ * quad reproduces the old axis-aligned wave byte-for-byte. `n̂` points from
5164
+ * the baseline toward the ascent side; humps rise toward the text. */
5165
+ function squigglePath(start, u, n, w, amp) {
4978
5166
  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)}`;
5167
+ const at = (t, off) => ({
5168
+ x: start.x + u.x * t + n.x * off,
5169
+ y: start.y + u.y * t + n.y * off
5170
+ });
5171
+ const p = (v) => `${num(v.x)} ${num(v.y)}`;
5172
+ const hump = at(half / 2, amp);
5173
+ let d = `M ${p(at(0, 0))} Q ${p(hump)} ${p(at(half, 0))}`;
5174
+ for (let t = half; t + half <= w + .5; t += half) d += ` T ${p(at(t + half, 0))}`;
4981
5175
  return d;
4982
5176
  }
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). */
5177
+ /** Per-subtype markup nodes on the quads' own edges (corner-NAMED TextQuads:
5178
+ * upper = ascent side, lower = baseline side, start → end along the frame).
5179
+ * The colour is the markup `/C` (our model keeps stroke==fill). Rotated and
5180
+ * sheared cells draw along their true baselines; upright output is identical
5181
+ * to the old axis-aligned math. */
4985
5182
  function markupScene(subtype, quads, style) {
4986
5183
  const color = style.color;
4987
5184
  const opacity = style.opacity;
4988
5185
  const nodes = [];
4989
5186
  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;
5187
+ const down = {
5188
+ x: q.lowerStart.x - q.upperStart.x,
5189
+ y: q.lowerStart.y - q.upperStart.y
5190
+ };
5191
+ const h = Math.hypot(down.x, down.y);
5192
+ const wVec = {
5193
+ x: q.lowerEnd.x - q.lowerStart.x,
5194
+ y: q.lowerEnd.y - q.lowerStart.y
5195
+ };
5196
+ const w = Math.hypot(wVec.x, wVec.y);
4994
5197
  if (w <= 0 || h <= 0) continue;
5198
+ const n = {
5199
+ x: down.x / h,
5200
+ y: down.y / h
5201
+ };
4995
5202
  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") {
5203
+ if (subtype === "underline") nodes.push({
5204
+ kind: "line",
5205
+ a: {
5206
+ x: q.lowerStart.x - n.x * lw,
5207
+ y: q.lowerStart.y - n.y * lw
5208
+ },
5209
+ b: {
5210
+ x: q.lowerEnd.x - n.x * lw,
5211
+ y: q.lowerEnd.y - n.y * lw
5212
+ },
5213
+ paint: {
5214
+ stroke: color,
5215
+ width: lw,
5216
+ opacity,
5217
+ blend: blendFor(style)
5218
+ }
5219
+ });
5220
+ else if (subtype === "strikeout") nodes.push({
5221
+ kind: "line",
5222
+ a: {
5223
+ x: (q.upperStart.x + q.lowerStart.x) / 2,
5224
+ y: (q.upperStart.y + q.lowerStart.y) / 2
5225
+ },
5226
+ b: {
5227
+ x: (q.upperEnd.x + q.lowerEnd.x) / 2,
5228
+ y: (q.upperEnd.y + q.lowerEnd.y) / 2
5229
+ },
5230
+ paint: {
5231
+ stroke: color,
5232
+ width: lw,
5233
+ opacity,
5234
+ blend: blendFor(style)
5235
+ }
5236
+ });
5237
+ else if (subtype === "squiggly") {
5035
5238
  const amp = Math.min(2, Math.max(1, h * .08));
5239
+ const u = {
5240
+ x: wVec.x / w,
5241
+ y: wVec.y / w
5242
+ };
5243
+ const start = {
5244
+ x: q.lowerStart.x - n.x * amp,
5245
+ y: q.lowerStart.y - n.y * amp
5246
+ };
5036
5247
  nodes.push({
5037
5248
  kind: "path",
5038
- d: squigglePath(x, y + h - amp, w, amp),
5249
+ d: squigglePath(start, u, {
5250
+ x: -n.x,
5251
+ y: -n.y
5252
+ }, w, amp),
5039
5253
  paint: {
5040
5254
  stroke: color,
5041
5255
  width: lw,
@@ -5044,13 +5258,9 @@ function markupScene(subtype, quads, style) {
5044
5258
  }
5045
5259
  });
5046
5260
  } else nodes.push({
5047
- kind: "rect",
5048
- rect: {
5049
- x,
5050
- y,
5051
- width: w,
5052
- height: h
5053
- },
5261
+ kind: "poly",
5262
+ points: textQuadRing(q),
5263
+ closed: true,
5054
5264
  paint: {
5055
5265
  fill: color,
5056
5266
  opacity,
@@ -5060,23 +5270,40 @@ function markupScene(subtype, quads, style) {
5060
5270
  }
5061
5271
  return nodes;
5062
5272
  }
5063
- /** A redact mark's regions: per-quad boxes (text marks) or the rect (area). */
5273
+ const rectRing = (r) => [
5274
+ {
5275
+ x: r.x,
5276
+ y: r.y
5277
+ },
5278
+ {
5279
+ x: r.x + r.width,
5280
+ y: r.y
5281
+ },
5282
+ {
5283
+ x: r.x + r.width,
5284
+ y: r.y + r.height
5285
+ },
5286
+ {
5287
+ x: r.x,
5288
+ y: r.y + r.height
5289
+ }
5290
+ ];
5064
5291
  function redactRegions(geom) {
5065
5292
  if (geom.t === "quads") {
5066
5293
  const out = [];
5067
5294
  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
5295
+ const bounds = textQuadBounds(q);
5296
+ if (bounds.width > 0 && bounds.height > 0) out.push({
5297
+ ring: textQuadRing(q),
5298
+ bounds
5075
5299
  });
5076
5300
  }
5077
5301
  return out;
5078
5302
  }
5079
- if (geom.t === "rect") return [geom.rect];
5303
+ if (geom.t === "rect") return [{
5304
+ ring: rectRing(geom.rect),
5305
+ bounds: geom.rect
5306
+ }];
5080
5307
  return [];
5081
5308
  }
5082
5309
  /**
@@ -5142,9 +5369,10 @@ function redactScene(item) {
5142
5369
  width: item.style.strokeWidth || 1.5,
5143
5370
  opacity: item.style.opacity
5144
5371
  };
5145
- return regions.map((rect) => ({
5146
- kind: "rect",
5147
- rect,
5372
+ return regions.map((region) => ({
5373
+ kind: "poly",
5374
+ points: region.ring,
5375
+ closed: true,
5148
5376
  paint
5149
5377
  }));
5150
5378
  }
@@ -5154,13 +5382,14 @@ function redactScene(item) {
5154
5382
  fill: item.style.interiorColor,
5155
5383
  opacity: 1
5156
5384
  };
5157
- for (const rect of regions) nodes.push({
5158
- kind: "rect",
5159
- rect,
5385
+ for (const region of regions) nodes.push({
5386
+ kind: "poly",
5387
+ points: region.ring,
5388
+ closed: true,
5160
5389
  paint: fill
5161
5390
  });
5162
5391
  }
5163
- if (item.label) for (const region of regions) nodes.push(...layoutRedactLabel(region, item.label, item.text));
5392
+ if (item.label) for (const region of regions) nodes.push(...layoutRedactLabel(region.bounds, item.label, item.text));
5164
5393
  return nodes;
5165
5394
  }
5166
5395
  /** The full painted scene for one annotation. */
@@ -5284,6 +5513,7 @@ function effAp(m, id, view) {
5284
5513
  function effSource(m, id) {
5285
5514
  const a = m.byId[id];
5286
5515
  if (capsFor(a.subtype).opaqueBody) return a.source;
5516
+ if (m.editing === id && a.geom.t === "text" && a.geom.callout) return "vector";
5287
5517
  const d = m.draft;
5288
5518
  if (d?.g === "handle" && d.id === id) return "vector";
5289
5519
  if ((d?.g === "rotate" || d?.g === "group") && d.ids.includes(id)) return "vector";
@@ -5584,7 +5814,7 @@ function chrome(m, pon, pageBox, knobOffset = 24, view) {
5584
5814
  const style = effStyle(a, view);
5585
5815
  const caps = capsFor(a.subtype);
5586
5816
  const rot = geomRotation(g);
5587
- const obb = caps.rotatable ? obbFromGeom(g, style.strokeWidth, a.style.border) : null;
5817
+ const obb = obbFromGeom(g, style.strokeWidth, a.style.border);
5588
5818
  if (obb && rot !== 0) nodes.push({
5589
5819
  kind: "obb",
5590
5820
  corners: obb.corners,
@@ -5675,6 +5905,6 @@ function creationDraftAnchor(m) {
5675
5905
  };
5676
5906
  }
5677
5907
  //#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 };
5908
+ 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, annotDeletable, 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, isAttachedLink, isConversationOnly, isRotatableGeom, isSelectable, isSubstrateOnly, linkChildrenOf, linkOf, 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
5909
 
5680
5910
  //# sourceMappingURL=index.js.map