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

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];
@@ -2876,7 +3021,7 @@ const canMove = (m, id) => {
2876
3021
  * `locked` (and inert `/F` states) suppress them at runtime — a
2877
3022
  * screen-anchored body keeps its handles: `noZoom`/`noRotate` exempt it from
2878
3023
  * the display transform, they don't freeze its size or vertices. */
2879
- const hasHandles = (a) => {
3024
+ const hasHandles = (m, a) => {
2880
3025
  if (!annotTransformable(a) || textBound(a)) return false;
2881
3026
  const c = capsFor(a.subtype);
2882
3027
  return c.resizable || c.vertexEditable;
@@ -2943,7 +3088,7 @@ function hitTest(m, pon, p, geom, strokeMargin, pageBox, inert, view) {
2943
3088
  };
2944
3089
  }
2945
3090
  }
2946
- if (hasHandles(a)) {
3091
+ if (hasHandles(m, a)) {
2947
3092
  for (const h of geomHandles(hitGeomOf(a, view))) if (Math.abs(h.at.x - p.x) <= geom.handleTol && Math.abs(h.at.y - p.y) <= geom.handleTol) return {
2948
3093
  t: "handle",
2949
3094
  id: a.id,
@@ -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 ? [{
@@ -4694,6 +4853,14 @@ function setProps(m, patch) {
4694
4853
  * COMMITTED member; uncommitted drafts just merge (their create draft carries
4695
4854
  * the flags when it commits).
4696
4855
  */
4856
+ /**
4857
+ * The actions plane's session-visibility write (Hide actions, script
4858
+ * `annot.hidden`): merge per-id hidden overrides into the session overlay.
4859
+ * Pure session state — ZERO effects, no engine write, no authority. Hiding
4860
+ * clears transient engagement so no orphaned selection chrome or text editor
4861
+ * survives on an invisible annotation. Identity-preserving no-op when nothing
4862
+ * changes (plugin memo caches key on model identity).
4863
+ */
4697
4864
  function setFlags(m, patch, ids) {
4698
4865
  const targets = ids ?? m.selected;
4699
4866
  if (!targets.length) return [m, []];
@@ -4804,28 +4971,26 @@ function resetRotation(m) {
4804
4971
  function deleteSelection(m) {
4805
4972
  const deletable = m.selected.filter((id) => {
4806
4973
  const a = m.byId[id];
4807
- return !!a && annotTransformable(a);
4974
+ return !!a && annotDeletable(a);
4808
4975
  });
4809
4976
  if (!deletable.length) return [m, []];
4977
+ const withChildren = [...deletable, ...deletable.flatMap((id) => linkChildrenOf(m, id).map((c) => c.id))];
4810
4978
  const fx = [];
4811
- for (const id of deletable) {
4979
+ for (const id of withChildren) {
4812
4980
  const a = m.byId[id];
4813
4981
  if (a?.ref) fx.push({
4814
4982
  fx: "delete",
4815
4983
  ref: a.ref
4816
4984
  });
4817
- for (const childRef of a?.linkRefs ?? []) fx.push({
4818
- fx: "delete",
4819
- ref: childRef
4820
- });
4821
4985
  }
4822
- return [removeAnnots(m, deletable), fx];
4986
+ return [removeAnnots(m, withChildren), fx];
4823
4987
  }
4824
4988
  function annotsInBox(m, pon, a, b, inert, view) {
4825
4989
  const box = rectFromPoints(a, b);
4826
4990
  return m.order.filter((id) => {
4827
4991
  const annot = m.byId[id];
4828
4992
  if (annot?.pon !== pon || inert?.has(id) || !isSelectable(m, id)) return false;
4993
+ if (isSubstrateOnly(annot)) return false;
4829
4994
  return quadIntersectsRect(selectionQuad(anchoredGeom(annot.geom, anchorModeOf(annot), view), annot.style.strokeWidth, annot.style.border), box);
4830
4995
  });
4831
4996
  }
@@ -4844,6 +5009,39 @@ function mergeLoaded(m, annots) {
4844
5009
  };
4845
5010
  }
4846
5011
  /**
5012
+ * Whole-document hydration ingest: the snapshot is the committed truth.
5013
+ * Overwrites by id via `upsertAnnots` (gesture protection included), then
5014
+ * reaps committed entries the snapshot no longer contains — deletions that
5015
+ * happened before we subscribed (initial load) or inside a desync gap.
5016
+ * Gentler than `removeAnnots`: `tmp:` drafts and gesture-locked ids
5017
+ * survive, and an in-progress draft is NOT cancelled — reaped ids can
5018
+ * never be part of it (locked ids are excluded from reaping).
5019
+ */
5020
+ function hydrateAnnots(m, annots, bumpApFlag) {
5021
+ const incoming = new Set(annots.map((a) => a.id));
5022
+ const locked = draftIds(m.draft);
5023
+ const reaped = m.order.filter((id) => {
5024
+ if (incoming.has(id) || locked.has(id)) return false;
5025
+ const a = m.byId[id];
5026
+ return a !== void 0 && a.ref !== null;
5027
+ });
5028
+ let next = m;
5029
+ if (reaped.length > 0) {
5030
+ const gone = new Set(reaped);
5031
+ const byId = { ...m.byId };
5032
+ for (const id of reaped) delete byId[id];
5033
+ next = {
5034
+ ...m,
5035
+ byId,
5036
+ order: m.order.filter((id) => !gone.has(id)),
5037
+ selected: m.selected.filter((id) => !gone.has(id)),
5038
+ hovered: m.hovered && gone.has(m.hovered) ? null : m.hovered,
5039
+ editing: m.editing && gone.has(m.editing) ? null : m.editing
5040
+ };
5041
+ }
5042
+ return upsertAnnots(next, annots, bumpApFlag);
5043
+ }
5044
+ /**
4847
5045
  * Add-or-replace by id. Unlike `mergeLoaded` (which skips ids it already has,
4848
5046
  * for the bulk page read), this OVERWRITES — it's how the data API re-syncs an
4849
5047
  * annotation from the authoritative engine DTO and how a remote edit lands.
@@ -4859,13 +5057,8 @@ function upsertAnnots(m, annots, bumpAp = false) {
4859
5057
  if (dragging.has(a.id)) continue;
4860
5058
  if (!byId[a.id]) order.push(a.id);
4861
5059
  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
5060
  byId[a.id] = {
4867
5061
  ...a,
4868
- ...fold,
4869
5062
  apVersion: (prev?.apVersion ?? 0) + (bumpAp ? 1 : 0)
4870
5063
  };
4871
5064
  }
@@ -4973,70 +5166,99 @@ function shapePaint(style, closed) {
4973
5166
  ...style.border.kind === "cloudy" ? { join: "round" } : {}
4974
5167
  };
4975
5168
  }
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) {
5169
+ /** A smooth squiggle (quadratic-bezier wave) along an ARBITRARY baseline,
5170
+ * generated in the (û, n̂) basis: one `Q` hump then reflected `T` segments
5171
+ * alternate across the run. Reflection is affine-invariant, so an upright
5172
+ * quad reproduces the old axis-aligned wave byte-for-byte. `n̂` points from
5173
+ * the baseline toward the ascent side; humps rise toward the text. */
5174
+ function squigglePath(start, u, n, w, amp) {
4979
5175
  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)}`;
5176
+ const at = (t, off) => ({
5177
+ x: start.x + u.x * t + n.x * off,
5178
+ y: start.y + u.y * t + n.y * off
5179
+ });
5180
+ const p = (v) => `${num(v.x)} ${num(v.y)}`;
5181
+ const hump = at(half / 2, amp);
5182
+ let d = `M ${p(at(0, 0))} Q ${p(hump)} ${p(at(half, 0))}`;
5183
+ for (let t = half; t + half <= w + .5; t += half) d += ` T ${p(at(t + half, 0))}`;
4982
5184
  return d;
4983
5185
  }
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). */
5186
+ /** Per-subtype markup nodes on the quads' own edges (corner-NAMED TextQuads:
5187
+ * upper = ascent side, lower = baseline side, start → end along the frame).
5188
+ * The colour is the markup `/C` (our model keeps stroke==fill). Rotated and
5189
+ * sheared cells draw along their true baselines; upright output is identical
5190
+ * to the old axis-aligned math. */
4986
5191
  function markupScene(subtype, quads, style) {
4987
5192
  const color = style.color;
4988
5193
  const opacity = style.opacity;
4989
5194
  const nodes = [];
4990
5195
  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;
5196
+ const down = {
5197
+ x: q.lowerStart.x - q.upperStart.x,
5198
+ y: q.lowerStart.y - q.upperStart.y
5199
+ };
5200
+ const h = Math.hypot(down.x, down.y);
5201
+ const wVec = {
5202
+ x: q.lowerEnd.x - q.lowerStart.x,
5203
+ y: q.lowerEnd.y - q.lowerStart.y
5204
+ };
5205
+ const w = Math.hypot(wVec.x, wVec.y);
4995
5206
  if (w <= 0 || h <= 0) continue;
5207
+ const n = {
5208
+ x: down.x / h,
5209
+ y: down.y / h
5210
+ };
4996
5211
  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") {
5212
+ if (subtype === "underline") nodes.push({
5213
+ kind: "line",
5214
+ a: {
5215
+ x: q.lowerStart.x - n.x * lw,
5216
+ y: q.lowerStart.y - n.y * lw
5217
+ },
5218
+ b: {
5219
+ x: q.lowerEnd.x - n.x * lw,
5220
+ y: q.lowerEnd.y - n.y * lw
5221
+ },
5222
+ paint: {
5223
+ stroke: color,
5224
+ width: lw,
5225
+ opacity,
5226
+ blend: blendFor(style)
5227
+ }
5228
+ });
5229
+ else if (subtype === "strikeout") nodes.push({
5230
+ kind: "line",
5231
+ a: {
5232
+ x: (q.upperStart.x + q.lowerStart.x) / 2,
5233
+ y: (q.upperStart.y + q.lowerStart.y) / 2
5234
+ },
5235
+ b: {
5236
+ x: (q.upperEnd.x + q.lowerEnd.x) / 2,
5237
+ y: (q.upperEnd.y + q.lowerEnd.y) / 2
5238
+ },
5239
+ paint: {
5240
+ stroke: color,
5241
+ width: lw,
5242
+ opacity,
5243
+ blend: blendFor(style)
5244
+ }
5245
+ });
5246
+ else if (subtype === "squiggly") {
5036
5247
  const amp = Math.min(2, Math.max(1, h * .08));
5248
+ const u = {
5249
+ x: wVec.x / w,
5250
+ y: wVec.y / w
5251
+ };
5252
+ const start = {
5253
+ x: q.lowerStart.x - n.x * amp,
5254
+ y: q.lowerStart.y - n.y * amp
5255
+ };
5037
5256
  nodes.push({
5038
5257
  kind: "path",
5039
- d: squigglePath(x, y + h - amp, w, amp),
5258
+ d: squigglePath(start, u, {
5259
+ x: -n.x,
5260
+ y: -n.y
5261
+ }, w, amp),
5040
5262
  paint: {
5041
5263
  stroke: color,
5042
5264
  width: lw,
@@ -5045,13 +5267,9 @@ function markupScene(subtype, quads, style) {
5045
5267
  }
5046
5268
  });
5047
5269
  } else nodes.push({
5048
- kind: "rect",
5049
- rect: {
5050
- x,
5051
- y,
5052
- width: w,
5053
- height: h
5054
- },
5270
+ kind: "poly",
5271
+ points: (0, _embedpdf_core_geometry.textQuadRing)(q),
5272
+ closed: true,
5055
5273
  paint: {
5056
5274
  fill: color,
5057
5275
  opacity,
@@ -5061,23 +5279,40 @@ function markupScene(subtype, quads, style) {
5061
5279
  }
5062
5280
  return nodes;
5063
5281
  }
5064
- /** A redact mark's regions: per-quad boxes (text marks) or the rect (area). */
5282
+ const rectRing = (r) => [
5283
+ {
5284
+ x: r.x,
5285
+ y: r.y
5286
+ },
5287
+ {
5288
+ x: r.x + r.width,
5289
+ y: r.y
5290
+ },
5291
+ {
5292
+ x: r.x + r.width,
5293
+ y: r.y + r.height
5294
+ },
5295
+ {
5296
+ x: r.x,
5297
+ y: r.y + r.height
5298
+ }
5299
+ ];
5065
5300
  function redactRegions(geom) {
5066
5301
  if (geom.t === "quads") {
5067
5302
  const out = [];
5068
5303
  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
5304
+ const bounds = (0, _embedpdf_core_geometry.textQuadBounds)(q);
5305
+ if (bounds.width > 0 && bounds.height > 0) out.push({
5306
+ ring: (0, _embedpdf_core_geometry.textQuadRing)(q),
5307
+ bounds
5076
5308
  });
5077
5309
  }
5078
5310
  return out;
5079
5311
  }
5080
- if (geom.t === "rect") return [geom.rect];
5312
+ if (geom.t === "rect") return [{
5313
+ ring: rectRing(geom.rect),
5314
+ bounds: geom.rect
5315
+ }];
5081
5316
  return [];
5082
5317
  }
5083
5318
  /**
@@ -5143,9 +5378,10 @@ function redactScene(item) {
5143
5378
  width: item.style.strokeWidth || 1.5,
5144
5379
  opacity: item.style.opacity
5145
5380
  };
5146
- return regions.map((rect) => ({
5147
- kind: "rect",
5148
- rect,
5381
+ return regions.map((region) => ({
5382
+ kind: "poly",
5383
+ points: region.ring,
5384
+ closed: true,
5149
5385
  paint
5150
5386
  }));
5151
5387
  }
@@ -5155,13 +5391,14 @@ function redactScene(item) {
5155
5391
  fill: item.style.interiorColor,
5156
5392
  opacity: 1
5157
5393
  };
5158
- for (const rect of regions) nodes.push({
5159
- kind: "rect",
5160
- rect,
5394
+ for (const region of regions) nodes.push({
5395
+ kind: "poly",
5396
+ points: region.ring,
5397
+ closed: true,
5161
5398
  paint: fill
5162
5399
  });
5163
5400
  }
5164
- if (item.label) for (const region of regions) nodes.push(...layoutRedactLabel(region, item.label, item.text));
5401
+ if (item.label) for (const region of regions) nodes.push(...layoutRedactLabel(region.bounds, item.label, item.text));
5165
5402
  return nodes;
5166
5403
  }
5167
5404
  /** The full painted scene for one annotation. */
@@ -5285,6 +5522,7 @@ function effAp(m, id, view) {
5285
5522
  function effSource(m, id) {
5286
5523
  const a = m.byId[id];
5287
5524
  if (capsFor(a.subtype).opaqueBody) return a.source;
5525
+ if (m.editing === id && a.geom.t === "text" && a.geom.callout) return "vector";
5288
5526
  const d = m.draft;
5289
5527
  if (d?.g === "handle" && d.id === id) return "vector";
5290
5528
  if ((d?.g === "rotate" || d?.g === "group") && d.ids.includes(id)) return "vector";
@@ -5585,7 +5823,7 @@ function chrome(m, pon, pageBox, knobOffset = 24, view) {
5585
5823
  const style = effStyle(a, view);
5586
5824
  const caps = capsFor(a.subtype);
5587
5825
  const rot = geomRotation(g);
5588
- const obb = caps.rotatable ? obbFromGeom(g, style.strokeWidth, a.style.border) : null;
5826
+ const obb = obbFromGeom(g, style.strokeWidth, a.style.border);
5589
5827
  if (obb && rot !== 0) nodes.push({
5590
5828
  kind: "obb",
5591
5829
  corners: obb.corners,
@@ -5691,6 +5929,7 @@ exports.anchoredBox = anchoredBox;
5691
5929
  exports.anchoredGeom = anchoredGeom;
5692
5930
  exports.anchoredStrokeWidth = anchoredStrokeWidth;
5693
5931
  exports.annotContentsEditable = annotContentsEditable;
5932
+ exports.annotDeletable = annotDeletable;
5694
5933
  exports.annotInteractive = annotInteractive;
5695
5934
  exports.annotTransformable = annotTransformable;
5696
5935
  exports.applyProps = applyProps;
@@ -5698,6 +5937,8 @@ exports.calloutConnection = calloutConnection;
5698
5937
  exports.calloutLinePoints = calloutLinePoints;
5699
5938
  exports.canMove = canMove;
5700
5939
  exports.capsFor = capsFor;
5940
+ exports.caretGeomFromAnchor = caretGeomFromAnchor;
5941
+ exports.caretRectFromAnchor = caretRectFromAnchor;
5701
5942
  exports.caretRectFromTextEnd = caretRectFromTextEnd;
5702
5943
  exports.centroidOf = centroidOf;
5703
5944
  exports.chrome = chrome;
@@ -5737,8 +5978,13 @@ exports.initialModel = initialModel;
5737
5978
  exports.initialStyle = initialStyle;
5738
5979
  exports.initialTextStyle = initialTextStyle;
5739
5980
  exports.interactive = interactive;
5981
+ exports.isAttachedLink = isAttachedLink;
5982
+ exports.isConversationOnly = isConversationOnly;
5740
5983
  exports.isRotatableGeom = isRotatableGeom;
5741
5984
  exports.isSelectable = isSelectable;
5985
+ exports.isSubstrateOnly = isSubstrateOnly;
5986
+ exports.linkChildrenOf = linkChildrenOf;
5987
+ exports.linkOf = linkOf;
5742
5988
  exports.mergeFlags = mergeFlags;
5743
5989
  exports.normalizeDeg = normalizeDeg;
5744
5990
  exports.obbFromGeom = obbFromGeom;