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