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