@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 +405 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -27
- package/dist/index.d.ts +134 -27
- package/dist/index.js +399 -169
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
/**
|
|
640
|
-
*
|
|
641
|
-
|
|
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)
|
|
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)
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
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) =>
|
|
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
|
|
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(
|
|
2718
|
+
points: rectCornerPoints(r).map((p) => rotatePoint(p, c, rot)),
|
|
2572
2719
|
closed: true
|
|
2573
2720
|
} : {
|
|
2574
2721
|
kind: "rect",
|
|
2575
|
-
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.
|
|
3669
|
-
case "createReplaceText": return createReplaceText(m, msg.pon, msg.
|
|
3670
|
-
case "createMarkup": return createMarkup(m, msg.subtype, msg.pon, msg.
|
|
3671
|
-
case "setMarkupPreview": return setMarkupPreview(m, msg.subtype, msg.
|
|
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
|
-
/**
|
|
4473
|
-
*
|
|
4474
|
-
const
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
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,
|
|
4498
|
-
const quads =
|
|
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,
|
|
4540
|
-
const quads =
|
|
4541
|
-
if (!quads.length
|
|
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,
|
|
4598
|
-
|
|
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
|
-
|
|
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
|
|
4634
|
-
function setMarkupPreview(m, 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
|
|
4637
|
-
const quads =
|
|
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)
|
|
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 (
|
|
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 &&
|
|
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
|
|
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,
|
|
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
|
|
4977
|
-
*
|
|
4978
|
-
|
|
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
|
-
|
|
4981
|
-
|
|
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
|
|
4985
|
-
*
|
|
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
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
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
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
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(
|
|
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: "
|
|
5049
|
-
|
|
5050
|
-
|
|
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
|
-
|
|
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
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
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 [
|
|
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((
|
|
5147
|
-
kind: "
|
|
5148
|
-
|
|
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
|
|
5159
|
-
kind: "
|
|
5160
|
-
|
|
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 =
|
|
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;
|