@embedpdf/plugin-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/{capability-fghl0JmE.js → capability-BRf0HrOi.js} +591 -171
- package/dist/capability-BRf0HrOi.js.map +1 -0
- package/dist/{capability-hb2AU0f6.cjs → capability-Cl9x8oLS.cjs} +594 -168
- package/dist/capability-Cl9x8oLS.cjs.map +1 -0
- package/dist/index.cjs +87 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +86 -85
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/{types-BhWgqgbX.d.cts → types-DITX_3CG.d.ts} +198 -19
- package/dist/{types-Da6JRmhw.d.ts → types-Due7UgzE.d.cts} +198 -19
- package/package.json +7 -7
- package/dist/capability-fghl0JmE.js.map +0 -1
- package/dist/capability-hb2AU0f6.cjs.map +0 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { CONTINUOUS_RENDER_POLICY, createCapabilityToken, snapAppearanceScale } from "@embedpdf/core";
|
|
2
2
|
import { InteractionToken } from "@embedpdf/plugin-interaction";
|
|
3
|
-
import { resolveBinarySource, sniffBinaryMetadata } from "@embedpdf/engine-core/runtime";
|
|
4
|
-
import {
|
|
3
|
+
import { buildCommentThreads, encodeStableIdKey, resolveBinarySource, sniffBinaryMetadata } from "@embedpdf/engine-core/runtime";
|
|
4
|
+
import { SelectionToken } from "@embedpdf/plugin-selection";
|
|
5
|
+
import { FLAG_KEYS, annotContentsEditable, annotDeletable, annotTransformable, calloutLinePoints, canMove, chrome, clickCreateGeom, cloudyBorderExtent, contentToPdfPoint, contentToPdfRect, contentToPdfRect as contentToPdfRect$1, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, geomPdfBounds, geomRotation, geomVisualBounds, groupKeyOf, hitTest, initialTextStyle, isSubstrateOnly, linkChildrenOf, linkOf, normalizeDeg, pageItems, pdfToContentPoint, pdfToContentPoint as pdfToContentPoint$1, pdfToContentRect, pdfToContentRect as pdfToContentRect$1, propsFor, readProp, resolveClickPlacement, rotatedAabb, selectionAnchor, selectionQuad, sharedProps, styleFromProps, textBoxes, unionRect, update, uprightRotation, viewable } from "@embedpdf/core-annotation";
|
|
6
|
+
import { normalizeQuad, textQuadBounds } from "@embedpdf/core-geometry";
|
|
5
7
|
//#region src/types.ts
|
|
6
8
|
/**
|
|
7
9
|
* The annotation capability token. Typed to the full {@link AnnotationHostCapability}
|
|
8
10
|
* here (the package internals + the `/internal` entry use this view). The package
|
|
9
11
|
* root re-exports the SAME token narrowed to {@link AnnotationCapability}.
|
|
10
12
|
*/
|
|
11
|
-
const AnnotationToken = createCapabilityToken("annotation");
|
|
13
|
+
const AnnotationToken = createCapabilityToken("annotation", { hint: `add annotationPlugin() from '@embedpdf/plugin-annotation' to your plugins list` });
|
|
12
14
|
//#endregion
|
|
13
15
|
//#region src/repository/seam.ts
|
|
14
16
|
function refKey(ref) {
|
|
@@ -333,18 +335,29 @@ const visualRect = (a, crop) => {
|
|
|
333
335
|
if (g.t === "poly") return { rect: geomPdfBounds(g, a.style.strokeWidth, crop, a.style.border) };
|
|
334
336
|
return {};
|
|
335
337
|
};
|
|
336
|
-
/**
|
|
338
|
+
/**
|
|
339
|
+
* A lowering whose key is an INPUT of the derived /Rect: the visual bounds
|
|
340
|
+
* ride along with every emission, so a sparse patch can never change an input
|
|
341
|
+
* without re-emitting the derivation. (The bug this kills: patch `lineEndings`
|
|
342
|
+
* alone → the engine re-bakes the /AP inside the stale /Rect → the new
|
|
343
|
+
* arrowhead is clipped in every viewer except the live vector one.)
|
|
344
|
+
*/
|
|
345
|
+
const withRect = (lower) => (a, crop) => ({
|
|
346
|
+
...lower(a, crop),
|
|
347
|
+
...visualRect(a, crop)
|
|
348
|
+
});
|
|
349
|
+
/** Stroke-family prop exceptions: width, border, AND line endings feed the
|
|
350
|
+
* derived /Rect — an ending's arrowhead reaches well past the endpoint, and
|
|
351
|
+
* ISO 32000 requires /Rect to enclose it. Where a key can't change the
|
|
352
|
+
* bounds (a line's border restyle), the re-emitted rect is an idempotent
|
|
353
|
+
* no-op — uniformity beats per-case reasoning here. */
|
|
337
354
|
const strokeProps = {
|
|
338
|
-
strokeWidth: (a
|
|
339
|
-
|
|
340
|
-
...visualRect(a, crop)
|
|
341
|
-
}),
|
|
342
|
-
border: (a, crop) => ({
|
|
355
|
+
strokeWidth: withRect((a) => ({ strokeWidth: a.style.strokeWidth })),
|
|
356
|
+
border: withRect((a) => ({
|
|
343
357
|
...borderSlice(a.style),
|
|
344
|
-
...polyCloudy(a)
|
|
345
|
-
|
|
346
|
-
})
|
|
347
|
-
lineEndings: (a) => (a.geom.t === "line" || a.geom.t === "poly") && a.geom.ends ? { lineEndings: a.geom.ends } : {}
|
|
358
|
+
...polyCloudy(a)
|
|
359
|
+
})),
|
|
360
|
+
lineEndings: withRect((a) => (a.geom.t === "line" || a.geom.t === "poly") && a.geom.ends ? { lineEndings: a.geom.ends } : {})
|
|
348
361
|
};
|
|
349
362
|
const line = {
|
|
350
363
|
ingest: (dto, crop) => {
|
|
@@ -522,20 +535,29 @@ const freeText = {
|
|
|
522
535
|
};
|
|
523
536
|
//#endregion
|
|
524
537
|
//#region src/repository/kinds/quads.ts
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
538
|
+
/**
|
|
539
|
+
* Imported `/QuadPoints` → semantic TextQuads. `normalizeQuad` is a
|
|
540
|
+
* NORMALIZER, not a cast: the de-facto zigzag order passes through, ring-order
|
|
541
|
+
* producers are repaired, and garbage gets a deterministic labeling — so
|
|
542
|
+
* drawing code can never put an underline on the wrong edge of a well-formed
|
|
543
|
+
* quad, rotated ones included.
|
|
544
|
+
*/
|
|
545
|
+
const quadsFromDTO = (quadPoints, crop) => quadPoints.map((q) => normalizeQuad({
|
|
546
|
+
p1: pdfToContentPoint$1(q.p1, crop),
|
|
547
|
+
p2: pdfToContentPoint$1(q.p2, crop),
|
|
548
|
+
p3: pdfToContentPoint$1(q.p3, crop),
|
|
549
|
+
p4: pdfToContentPoint$1(q.p4, crop)
|
|
550
|
+
}));
|
|
551
|
+
/** Content quads → engine `/QuadPoints` (PDF user space, zigzag slot order:
|
|
552
|
+
* p1..p4 = upper-start, upper-end, lower-start, lower-end — under the y-flip
|
|
553
|
+
* exactly PDFium's documented TL, TR, BL, BR); null off quads geom. */
|
|
532
554
|
function quadPointsFor(a, crop) {
|
|
533
555
|
if (a.geom.t !== "quads") return null;
|
|
534
556
|
return a.geom.quads.map((q) => ({
|
|
535
|
-
p1: contentToPdfPoint(q
|
|
536
|
-
p2: contentToPdfPoint(q
|
|
537
|
-
p3: contentToPdfPoint(q
|
|
538
|
-
p4: contentToPdfPoint(q
|
|
557
|
+
p1: contentToPdfPoint(q.upperStart, crop),
|
|
558
|
+
p2: contentToPdfPoint(q.upperEnd, crop),
|
|
559
|
+
p3: contentToPdfPoint(q.lowerStart, crop),
|
|
560
|
+
p4: contentToPdfPoint(q.lowerEnd, crop)
|
|
539
561
|
}));
|
|
540
562
|
}
|
|
541
563
|
/** PDF-space bounding box of a set of `/QuadPoints` quads — the `/Rect` a
|
|
@@ -593,15 +615,17 @@ const strikeout = markupProjection("strikeout");
|
|
|
593
615
|
const caret = {
|
|
594
616
|
ingest: (dto, crop) => {
|
|
595
617
|
const d = dto;
|
|
618
|
+
const rot = d.rotation ? fromPdfRotation(d.rotation) : 0;
|
|
596
619
|
return {
|
|
597
620
|
geom: {
|
|
598
621
|
t: "caret",
|
|
599
|
-
rect: pdfToContentRect$1(d.rect, crop)
|
|
622
|
+
rect: pdfToContentRect$1(rot && d.unrotatedRect ? d.unrotatedRect : d.rect, crop),
|
|
623
|
+
...rot ? { rot } : {}
|
|
600
624
|
},
|
|
601
625
|
...d.intent ? { intent: d.intent } : {}
|
|
602
626
|
};
|
|
603
627
|
},
|
|
604
|
-
geometry: (a, crop) => a.geom.t === "caret" ?
|
|
628
|
+
geometry: (a, crop) => a.geom.t === "caret" ? boxGeomFields(a.geom.rect, a.geom.rot ?? 0, crop) : null,
|
|
605
629
|
draftExtras: (a) => ({
|
|
606
630
|
rectDifferences: {
|
|
607
631
|
left: .5,
|
|
@@ -744,44 +768,6 @@ const unsupported = {
|
|
|
744
768
|
* parent's `link` prop, and the reconciler derives the children it should
|
|
745
769
|
* have from the parent's committed geometry.
|
|
746
770
|
*/
|
|
747
|
-
/** Does this kind's table declare the `link` prop (may it carry an attached
|
|
748
|
-
* link)? Widgets/caret/redact/file-attachment deliberately don't. */
|
|
749
|
-
const takesLink = (subtype) => propsFor(subtype).some((s) => s.key === "link");
|
|
750
|
-
/**
|
|
751
|
-
* Fold attached link children onto their parents: a `/Link` annotation
|
|
752
|
-
* grouped (`/RT /Group`) under a linkable non-link parent is not an object
|
|
753
|
-
* in the client model — it becomes the parent's `link` prop value plus the
|
|
754
|
-
* `linkRefs` join keys the write side needs. Folded children are REMOVED
|
|
755
|
-
* from the returned list, so they never render, hit-test, or select.
|
|
756
|
-
* Anything that doesn't match (orphans, links grouped under widgets or
|
|
757
|
-
* other links) stays a standalone annotation — nothing is ever hidden.
|
|
758
|
-
*
|
|
759
|
-
* Multi-segment parents (markup) carry several children with one shared
|
|
760
|
-
* target; the first child's target wins and the next write heals any
|
|
761
|
-
* foreign disagreement.
|
|
762
|
-
*/
|
|
763
|
-
function foldAttachedLinks(annots) {
|
|
764
|
-
const byId = new Map(annots.map((a) => [a.id, a]));
|
|
765
|
-
const folded = /* @__PURE__ */ new Set();
|
|
766
|
-
const patched = /* @__PURE__ */ new Map();
|
|
767
|
-
for (const a of annots) {
|
|
768
|
-
if (a.subtype !== "link" || a.data?.subtype !== "link") continue;
|
|
769
|
-
if (a.data.replyType !== "group" || !a.data.inReplyTo || !a.ref) continue;
|
|
770
|
-
const parentId = refKey(a.data.inReplyTo);
|
|
771
|
-
const parent = patched.get(parentId) ?? byId.get(parentId);
|
|
772
|
-
if (!parent || parent.subtype === "link" || !takesLink(parent.subtype)) continue;
|
|
773
|
-
patched.set(parentId, {
|
|
774
|
-
...parent,
|
|
775
|
-
link: parent.link ?? a.data.target,
|
|
776
|
-
linkRefs: [...parent.linkRefs ?? [], a.ref]
|
|
777
|
-
});
|
|
778
|
-
folded.add(a.id);
|
|
779
|
-
}
|
|
780
|
-
if (!folded.size) return annots;
|
|
781
|
-
return annots.filter((a) => !folded.has(a.id)).map((a) => patched.get(a.id) ?? a);
|
|
782
|
-
}
|
|
783
|
-
/** Bounds of one quad (content space). */
|
|
784
|
-
const quadBounds = (q) => unionRect(q);
|
|
785
771
|
/**
|
|
786
772
|
* The DESIRED hit rects (content space) of a parent's attached link
|
|
787
773
|
* children — derived fresh from the parent's committed geometry, never
|
|
@@ -797,7 +783,7 @@ const quadBounds = (q) => unionRect(q);
|
|
|
797
783
|
* `/QuadPoints` (tier 2).
|
|
798
784
|
*/
|
|
799
785
|
function linkChildRects(a) {
|
|
800
|
-
if (a.geom.t === "quads") return a.geom.quads.map(
|
|
786
|
+
if (a.geom.t === "quads") return a.geom.quads.map(textQuadBounds);
|
|
801
787
|
return [unionRect(selectionQuad(a.geom, a.style.strokeWidth, a.style.border))];
|
|
802
788
|
}
|
|
803
789
|
//#endregion
|
|
@@ -918,6 +904,7 @@ function toCreateDraft(a, crop) {
|
|
|
918
904
|
return {
|
|
919
905
|
...base,
|
|
920
906
|
...extras,
|
|
907
|
+
nm: crypto.randomUUID(),
|
|
921
908
|
flags: a.flags
|
|
922
909
|
};
|
|
923
910
|
}
|
|
@@ -949,6 +936,8 @@ function buildTextItems(m, pon, view) {
|
|
|
949
936
|
return textBoxes(m, pon, view).map((tb) => {
|
|
950
937
|
const a = m.byId[tb.id];
|
|
951
938
|
const t = a?.text ?? initialTextStyle;
|
|
939
|
+
const sw = a?.style.strokeWidth ?? 0;
|
|
940
|
+
const isCallout = a?.geom.t === "text" && !!a.geom.callout;
|
|
952
941
|
return {
|
|
953
942
|
id: tb.id,
|
|
954
943
|
ref: a?.ref ?? null,
|
|
@@ -962,7 +951,7 @@ function buildTextItems(m, pon, view) {
|
|
|
962
951
|
lineHeight: t.fontSize,
|
|
963
952
|
color: t.fontColor,
|
|
964
953
|
align: t.textAlign,
|
|
965
|
-
padding: 2,
|
|
954
|
+
padding: isCallout ? sw + 2 : sw,
|
|
966
955
|
background: a?.style.interiorColor ?? null
|
|
967
956
|
}
|
|
968
957
|
};
|
|
@@ -1115,6 +1104,17 @@ const TOOL_DEFAULT_KEYS = {
|
|
|
1115
1104
|
const DRAW_TAGS = ["annotation-draw", "annotation-edit"];
|
|
1116
1105
|
const MARKUP_TAGS = ["text-select", "annotation-edit"];
|
|
1117
1106
|
/**
|
|
1107
|
+
* Whether arming this ANNOTATION tool is touch consent (`Tool.touchDirect`):
|
|
1108
|
+
* drag-CREATE tools — shape/ink draw and text-markup select — take
|
|
1109
|
+
* single-finger touch wholesale (finger draws/marks, two fingers navigate:
|
|
1110
|
+
* the drawing-app convention). Click-to-PLACE tools (stamp, note) do not: a
|
|
1111
|
+
* tap places without any claim, and drags keep scrolling. Derived from the
|
|
1112
|
+
* gesture tags, so custom registered tools inherit the right behavior. Only
|
|
1113
|
+
* annotation-owned tools run through this — the built-in `pointer` tool also
|
|
1114
|
+
* enables `text-select`, but it is registered by the hub, not here.
|
|
1115
|
+
*/
|
|
1116
|
+
const isTouchDirect = (enables) => enables.has("annotation-draw") || enables.has("text-select");
|
|
1117
|
+
/**
|
|
1118
1118
|
* The built-in tools — a data mirror of v2's registrations (shapes + lines + ink
|
|
1119
1119
|
* + free text in the draw channel, text markup + caret behind text selection, and
|
|
1120
1120
|
* the click-to-place stamp). Order is display-neutral; the toolbar owns layout.
|
|
@@ -1504,10 +1504,6 @@ const viewEnv = (zoom, rotation) => zoom != null || rotation != null ? {
|
|
|
1504
1504
|
zoom: zoom ?? 1,
|
|
1505
1505
|
rotation: rotation ?? 0
|
|
1506
1506
|
} : void 0;
|
|
1507
|
-
/** Broad annotate-write capability (PDF bit 6). The engine independently
|
|
1508
|
-
* enforces this AND the per-owner collab rules; `canEdit`/`canDelete` are the
|
|
1509
|
-
* UI mirror of the coarse gate. */
|
|
1510
|
-
const ANNOTATE_MODIFY = "doc.annotate.modify";
|
|
1511
1507
|
const TEXT_COMMIT_DEBOUNCE_MS = 250;
|
|
1512
1508
|
/**
|
|
1513
1509
|
* The annotation shell. The pure `update` runs HERE (so it can emit effects);
|
|
@@ -1516,7 +1512,6 @@ const TEXT_COMMIT_DEBOUNCE_MS = 250;
|
|
|
1516
1512
|
* ref, patch/delete fire-and-forget.
|
|
1517
1513
|
*/
|
|
1518
1514
|
function createAnnotationCapability(ctx, config = {}) {
|
|
1519
|
-
const loaded = /* @__PURE__ */ new Set();
|
|
1520
1515
|
const behaviors = [];
|
|
1521
1516
|
/** Per-annotation debounce timer for the engine `contents` write while typing. */
|
|
1522
1517
|
const textTimers = /* @__PURE__ */ new Map();
|
|
@@ -1549,16 +1544,23 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1549
1544
|
};
|
|
1550
1545
|
/** The CSS-px chrome settings converted to CONTENT units by the page's view
|
|
1551
1546
|
* scale (px per content unit) — screen-constant grab zones + stalk at every
|
|
1552
|
-
* zoom. No scale → the values are read as content units (headless callers).
|
|
1553
|
-
|
|
1547
|
+
* zoom. No scale → the values are read as content units (headless callers).
|
|
1548
|
+
* `boost` widens the GRAB tolerances only (never the drawn chrome or the
|
|
1549
|
+
* knob's position) — the touch path passes {@link TOUCH_GRAB_BOOST} so
|
|
1550
|
+
* handles present finger-sized targets. */
|
|
1551
|
+
const chromeGeomAt = (scale, boost = 1) => {
|
|
1554
1552
|
const cs = chromeSettings();
|
|
1555
1553
|
const s = scale || 1;
|
|
1556
1554
|
return {
|
|
1557
|
-
handleTol: cs.handles.hitSize / 2 / s,
|
|
1558
|
-
knobTol: cs.knob.hitSize / 2 / s,
|
|
1555
|
+
handleTol: cs.handles.hitSize / 2 / s * boost,
|
|
1556
|
+
knobTol: cs.knob.hitSize / 2 / s * boost,
|
|
1559
1557
|
knobOffset: cs.knob.offset / s
|
|
1560
1558
|
};
|
|
1561
1559
|
};
|
|
1560
|
+
/** Finger-sized grab zones: 2× the mouse tolerances lands a ~24px handle hit
|
|
1561
|
+
* box in Apple's ~44pt-target territory without moving any visuals. */
|
|
1562
|
+
const TOUCH_GRAB_BOOST = 2;
|
|
1563
|
+
const grabBoost = (touch) => touch ? TOUCH_GRAB_BOOST : 1;
|
|
1562
1564
|
const itemsCache = /* @__PURE__ */ new Map();
|
|
1563
1565
|
const chromeCache = /* @__PURE__ */ new Map();
|
|
1564
1566
|
const memoItems = (pon, view) => {
|
|
@@ -1642,11 +1644,12 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1642
1644
|
const specs = sharedProps(members.map((a) => a.subtype));
|
|
1643
1645
|
const values = {};
|
|
1644
1646
|
const mixed = [];
|
|
1647
|
+
const valueOf = (a, key) => key === "link" && a.subtype !== "link" ? linkOf(m, a.id) : readProp(a, key);
|
|
1645
1648
|
for (const spec of specs) {
|
|
1646
|
-
const first =
|
|
1649
|
+
const first = valueOf(members[0], spec.key);
|
|
1647
1650
|
values[spec.key] = first;
|
|
1648
1651
|
const firstJson = JSON.stringify(first);
|
|
1649
|
-
if (members.some((a) => JSON.stringify(
|
|
1652
|
+
if (members.some((a) => JSON.stringify(valueOf(a, spec.key)) !== firstJson)) mixed.push(spec.key);
|
|
1650
1653
|
}
|
|
1651
1654
|
const v = {
|
|
1652
1655
|
specs,
|
|
@@ -1667,19 +1670,16 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1667
1670
|
const v = [];
|
|
1668
1671
|
for (const id of m.order) {
|
|
1669
1672
|
const a = m.byId[id];
|
|
1670
|
-
if (!a || a.pon !== pon || a.
|
|
1673
|
+
if (!a || a.pon !== pon || a.subtype !== "link") continue;
|
|
1671
1674
|
if (!viewable(a.flags, false)) continue;
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
rect,
|
|
1681
|
-
target: a.link
|
|
1682
|
-
}));
|
|
1675
|
+
const target = a.link ?? (a.data?.subtype === "link" ? a.data.target ?? null : null);
|
|
1676
|
+
if (target == null || a.geom.t !== "rect") continue;
|
|
1677
|
+
v.push({
|
|
1678
|
+
id,
|
|
1679
|
+
rect: a.geom.rect,
|
|
1680
|
+
target,
|
|
1681
|
+
attached: a.group !== void 0
|
|
1682
|
+
});
|
|
1683
1683
|
}
|
|
1684
1684
|
linkItemsCache.set(pon, {
|
|
1685
1685
|
model: m,
|
|
@@ -2063,7 +2063,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2063
2063
|
const crop = cropOf(dto.pageObjectNumber);
|
|
2064
2064
|
if (crop) apply({
|
|
2065
2065
|
t: "upsert",
|
|
2066
|
-
annots: [
|
|
2066
|
+
annots: [ingestDTO(dto, crop, source)],
|
|
2067
2067
|
bumpAp
|
|
2068
2068
|
});
|
|
2069
2069
|
};
|
|
@@ -2085,6 +2085,346 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2085
2085
|
});
|
|
2086
2086
|
for (const fx of effects) perform(fx, next);
|
|
2087
2087
|
}
|
|
2088
|
+
let hydrationStarted = false;
|
|
2089
|
+
let hydrationWindow = false;
|
|
2090
|
+
let hydrationRunning = null;
|
|
2091
|
+
let hydrateAgain = false;
|
|
2092
|
+
const pendingRemote = [];
|
|
2093
|
+
const setHydration = (hydration) => ctx.dispatch({
|
|
2094
|
+
type: "SET_HYDRATION",
|
|
2095
|
+
hydration
|
|
2096
|
+
});
|
|
2097
|
+
const ingestSnapshot = (snap, bumpAp) => {
|
|
2098
|
+
const annots = [];
|
|
2099
|
+
for (const page of snap.pages) {
|
|
2100
|
+
const crop = cropOf(page.pageState.pageObjectNumber);
|
|
2101
|
+
if (!crop) continue;
|
|
2102
|
+
annots.push(...page.annotations.map((d) => ingestDTO(d, crop)));
|
|
2103
|
+
}
|
|
2104
|
+
apply({
|
|
2105
|
+
t: "hydrated",
|
|
2106
|
+
annots,
|
|
2107
|
+
bumpAp
|
|
2108
|
+
});
|
|
2109
|
+
};
|
|
2110
|
+
const canRead = () => ctx.doc?.security.allows("doc.annotate.read") ?? true;
|
|
2111
|
+
const runHydration = (bumpAp) => {
|
|
2112
|
+
const doc = ctx.doc;
|
|
2113
|
+
if (!doc) return Promise.resolve();
|
|
2114
|
+
if (!canRead()) {
|
|
2115
|
+
setHydration({ status: "forbidden" });
|
|
2116
|
+
return Promise.resolve();
|
|
2117
|
+
}
|
|
2118
|
+
hydrationWindow = true;
|
|
2119
|
+
setHydration({ status: "loading" });
|
|
2120
|
+
const run = doc.annotations.listRawAll().then((snap) => {
|
|
2121
|
+
ingestSnapshot(snap, bumpAp);
|
|
2122
|
+
const head = snap.auditHead;
|
|
2123
|
+
const queued = pendingRemote.splice(0);
|
|
2124
|
+
hydrationWindow = false;
|
|
2125
|
+
for (const event of queued) {
|
|
2126
|
+
const serverId = "origin" in event ? event.origin.serverId : null;
|
|
2127
|
+
if (head !== void 0 && serverId !== null && serverId <= head) continue;
|
|
2128
|
+
applyRemoteAnnotationEvent(event);
|
|
2129
|
+
}
|
|
2130
|
+
setHydration({ status: "complete" });
|
|
2131
|
+
}).catch((error) => {
|
|
2132
|
+
hydrationWindow = false;
|
|
2133
|
+
for (const event of pendingRemote.splice(0)) applyRemoteAnnotationEvent(event);
|
|
2134
|
+
setHydration({
|
|
2135
|
+
status: "error",
|
|
2136
|
+
error
|
|
2137
|
+
});
|
|
2138
|
+
}).finally(() => {
|
|
2139
|
+
hydrationRunning = null;
|
|
2140
|
+
if (hydrateAgain) {
|
|
2141
|
+
hydrateAgain = false;
|
|
2142
|
+
hydrationRunning = runHydration(true);
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
hydrationRunning = run;
|
|
2146
|
+
return run;
|
|
2147
|
+
};
|
|
2148
|
+
const rehydrate = () => {
|
|
2149
|
+
if (hydrationRunning) {
|
|
2150
|
+
hydrateAgain = true;
|
|
2151
|
+
return hydrationRunning;
|
|
2152
|
+
}
|
|
2153
|
+
hydrationRunning = runHydration(true);
|
|
2154
|
+
return hydrationRunning;
|
|
2155
|
+
};
|
|
2156
|
+
/**
|
|
2157
|
+
* Fold one REMOTE annotation event into the model (transplanted from the
|
|
2158
|
+
* effects layer so hydration can queue + replay through the same code
|
|
2159
|
+
* path). `bump` re-fetches rasters, `keep` doesn't — driven by the
|
|
2160
|
+
* ENGINE'S appearance echo riding the event, so a remote MOVE costs
|
|
2161
|
+
* peers zero re-renders while a remote restyle refreshes exactly once.
|
|
2162
|
+
*/
|
|
2163
|
+
const upsertRemote = (dtos, bumpAp) => {
|
|
2164
|
+
const bump = [];
|
|
2165
|
+
const keep = [];
|
|
2166
|
+
for (const dto of dtos) {
|
|
2167
|
+
const crop = cropOf(dto.pageObjectNumber);
|
|
2168
|
+
if (!crop) continue;
|
|
2169
|
+
const a = ingestDTO(dto, crop, "baked");
|
|
2170
|
+
(bumpAp && !isSubstrateOnly(a) ? bump : keep).push(a);
|
|
2171
|
+
}
|
|
2172
|
+
if (bump.length) apply({
|
|
2173
|
+
t: "upsert",
|
|
2174
|
+
annots: bump,
|
|
2175
|
+
bumpAp: true
|
|
2176
|
+
});
|
|
2177
|
+
if (keep.length) apply({
|
|
2178
|
+
t: "upsert",
|
|
2179
|
+
annots: keep
|
|
2180
|
+
});
|
|
2181
|
+
};
|
|
2182
|
+
const applyRemoteAnnotationEvent = (event) => {
|
|
2183
|
+
switch (event.type) {
|
|
2184
|
+
case "annotation.created":
|
|
2185
|
+
upsertRemote([event.created], true);
|
|
2186
|
+
break;
|
|
2187
|
+
case "annotation.updated":
|
|
2188
|
+
upsertRemote([event.updated], event.appearance.changed);
|
|
2189
|
+
break;
|
|
2190
|
+
case "annotation.moved":
|
|
2191
|
+
upsertRemote(event.moved, false);
|
|
2192
|
+
break;
|
|
2193
|
+
case "annotation.deleted":
|
|
2194
|
+
if (event.deleted) {
|
|
2195
|
+
const key = encodeStableIdKey(event.deleted);
|
|
2196
|
+
if (model().byId[key]) apply({
|
|
2197
|
+
t: "remove",
|
|
2198
|
+
ids: [key]
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
break;
|
|
2202
|
+
default: break;
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
const currentUserId = () => ctx.doc?.security.identity?.user_id;
|
|
2206
|
+
let threadsMemo = null;
|
|
2207
|
+
/** Value-stable layout key: display order is all the sort consumes, so
|
|
2208
|
+
* the memo must survive hosts that rebuild the pages array per read. */
|
|
2209
|
+
const layoutSignature = () => (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber).join(",");
|
|
2210
|
+
const computeThreadsIndex = () => {
|
|
2211
|
+
const m = model();
|
|
2212
|
+
const dtos = [];
|
|
2213
|
+
for (const id of m.order) {
|
|
2214
|
+
const data = m.byId[id]?.data;
|
|
2215
|
+
if (data) dtos.push(data);
|
|
2216
|
+
}
|
|
2217
|
+
const threads = buildCommentThreads(dtos, { currentUserId: currentUserId() });
|
|
2218
|
+
const pages = ctx.document()?.pages ?? [];
|
|
2219
|
+
const displayIndex = new Map(pages.map((p, i) => [p.pageObjectNumber, i]));
|
|
2220
|
+
threads.sort((a, b) => {
|
|
2221
|
+
const pa = displayIndex.get(a.pageObjectNumber) ?? Number.MAX_SAFE_INTEGER;
|
|
2222
|
+
const pb = displayIndex.get(b.pageObjectNumber) ?? Number.MAX_SAFE_INTEGER;
|
|
2223
|
+
if (pa !== pb) return pa - pb;
|
|
2224
|
+
if (a.root.rect.top !== b.root.rect.top) return b.root.rect.top - a.root.rect.top;
|
|
2225
|
+
const ca = a.root.created ?? "";
|
|
2226
|
+
const cb = b.root.created ?? "";
|
|
2227
|
+
return ca < cb ? -1 : ca > cb ? 1 : 0;
|
|
2228
|
+
});
|
|
2229
|
+
const byMember = /* @__PURE__ */ new Map();
|
|
2230
|
+
for (const t of threads) {
|
|
2231
|
+
byMember.set(refKey(t.root.ref), t);
|
|
2232
|
+
for (const r of t.replies) byMember.set(refKey(r.ref), t);
|
|
2233
|
+
for (const g of t.groupedParts) byMember.set(refKey(g.ref), t);
|
|
2234
|
+
for (const s of t.review.statusRefs) byMember.set(refKey(s), t);
|
|
2235
|
+
}
|
|
2236
|
+
return {
|
|
2237
|
+
threads,
|
|
2238
|
+
byMember
|
|
2239
|
+
};
|
|
2240
|
+
};
|
|
2241
|
+
const threadsIndex = () => {
|
|
2242
|
+
const m = model();
|
|
2243
|
+
const layout = layoutSignature();
|
|
2244
|
+
const userId = currentUserId();
|
|
2245
|
+
if (threadsMemo && threadsMemo.byId === m.byId && threadsMemo.order === m.order && threadsMemo.layout === layout && threadsMemo.userId === userId) return threadsMemo;
|
|
2246
|
+
threadsMemo = {
|
|
2247
|
+
...computeThreadsIndex(),
|
|
2248
|
+
byId: m.byId,
|
|
2249
|
+
order: m.order,
|
|
2250
|
+
layout,
|
|
2251
|
+
userId
|
|
2252
|
+
};
|
|
2253
|
+
return threadsMemo;
|
|
2254
|
+
};
|
|
2255
|
+
const threadOf = (ref) => {
|
|
2256
|
+
const t = threadsIndex().byMember.get(refKey(ref));
|
|
2257
|
+
if (!t) throw new Error("[annotation] no comment thread contains this ref");
|
|
2258
|
+
return t;
|
|
2259
|
+
};
|
|
2260
|
+
/** Engine create + model sync for conversation-plane annotations (they
|
|
2261
|
+
* never paint, so the render source is immaterial — 'baked' avoids any
|
|
2262
|
+
* vector-scene work). */
|
|
2263
|
+
const createConversationAnnot = async (pon, draft) => {
|
|
2264
|
+
const doc = ctx.doc;
|
|
2265
|
+
if (!doc) throw new Error("[annotation] no document bound");
|
|
2266
|
+
const res = await doc.page(pon).annotations.create(draft);
|
|
2267
|
+
syncDTO(res.created, "baked");
|
|
2268
|
+
return res.created;
|
|
2269
|
+
};
|
|
2270
|
+
const deleteOne = async (ref) => {
|
|
2271
|
+
const doc = ctx.doc;
|
|
2272
|
+
if (!doc) throw new Error("[annotation] no document bound");
|
|
2273
|
+
await doc.page(ref.pageObjectNumber).annotations.delete(ref);
|
|
2274
|
+
apply({
|
|
2275
|
+
t: "remove",
|
|
2276
|
+
ids: [refKey(ref)]
|
|
2277
|
+
});
|
|
2278
|
+
};
|
|
2279
|
+
const REPLY_FLAGS = {
|
|
2280
|
+
print: true,
|
|
2281
|
+
noZoom: true,
|
|
2282
|
+
noRotate: true
|
|
2283
|
+
};
|
|
2284
|
+
const STATUS_FLAGS = {
|
|
2285
|
+
hidden: true,
|
|
2286
|
+
noZoom: true,
|
|
2287
|
+
noRotate: true
|
|
2288
|
+
};
|
|
2289
|
+
/** Coarse authority mirror (doc.annotate.modify) — B3 replaces this with
|
|
2290
|
+
* the collab-scope mirrors; the FLAG split is already the real rule. */
|
|
2291
|
+
const mutationTarget = (ref) => {
|
|
2292
|
+
const d = model().byId[refKey(ref)]?.data;
|
|
2293
|
+
return {
|
|
2294
|
+
...d?.userId !== void 0 ? { userId: d.userId } : {},
|
|
2295
|
+
...d?.groupId !== void 0 ? { groupId: d.groupId } : {}
|
|
2296
|
+
};
|
|
2297
|
+
};
|
|
2298
|
+
const allowsCreate = () => ctx.doc?.security.allowsAnnotationCreate() ?? false;
|
|
2299
|
+
/**
|
|
2300
|
+
* Ingest an engine DTO with this session's per-record AUTHORITY projected
|
|
2301
|
+
* onto it (permissions.md) — asked of the SAME mirrors the server enforces
|
|
2302
|
+
* with, against the record's stamped owner. Fused into
|
|
2303
|
+
* `annotTransformable`/`annotDeletable`, so hit-test, chrome, props and
|
|
2304
|
+
* delete all agree with the engine by construction. No security context
|
|
2305
|
+
* (bare local engines, tests) → unstamped → allowed; the engine enforces.
|
|
2306
|
+
*/
|
|
2307
|
+
const ingestDTO = (dto, crop, source) => {
|
|
2308
|
+
const a = fromDTO(dto, crop, source);
|
|
2309
|
+
const sec = ctx.doc?.security;
|
|
2310
|
+
if (!sec) return a;
|
|
2311
|
+
const target = {
|
|
2312
|
+
...dto.userId !== void 0 ? { userId: dto.userId } : {},
|
|
2313
|
+
...dto.groupId !== void 0 ? { groupId: dto.groupId } : {}
|
|
2314
|
+
};
|
|
2315
|
+
return {
|
|
2316
|
+
...a,
|
|
2317
|
+
authority: {
|
|
2318
|
+
update: sec.allowsAnnotationMutation("update", target),
|
|
2319
|
+
delete: sec.allowsAnnotationMutation("delete", target)
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
};
|
|
2323
|
+
const allowsMutation = (action, ref) => ctx.doc?.security.allowsAnnotationMutation(action, mutationTarget(ref)) ?? false;
|
|
2324
|
+
const deletableOne = (ref) => {
|
|
2325
|
+
const a = model().byId[refKey(ref)];
|
|
2326
|
+
return !!a && annotDeletable(a);
|
|
2327
|
+
};
|
|
2328
|
+
const threadMemberRefs = (t) => [
|
|
2329
|
+
...t.replies.map((r) => r.ref),
|
|
2330
|
+
...t.groupedParts.map((g) => g.ref),
|
|
2331
|
+
...t.review.statusRefs,
|
|
2332
|
+
t.root.ref
|
|
2333
|
+
];
|
|
2334
|
+
const comments = {
|
|
2335
|
+
threads: () => threadsIndex().threads,
|
|
2336
|
+
thread: (ref) => threadsIndex().byMember.get(refKey(ref)) ?? null,
|
|
2337
|
+
hydration: () => ctx.getState().hydration,
|
|
2338
|
+
rehydrate,
|
|
2339
|
+
reply: async (ref, text) => {
|
|
2340
|
+
const t = threadOf(ref);
|
|
2341
|
+
return (await createConversationAnnot(t.pageObjectNumber, {
|
|
2342
|
+
subtype: "text",
|
|
2343
|
+
rect: t.root.rect,
|
|
2344
|
+
icon: "comment",
|
|
2345
|
+
contents: text,
|
|
2346
|
+
inReplyTo: t.root.ref,
|
|
2347
|
+
flags: { ...REPLY_FLAGS }
|
|
2348
|
+
})).ref;
|
|
2349
|
+
},
|
|
2350
|
+
edit: async (ref, text) => {
|
|
2351
|
+
const subtype = model().byId[refKey(ref)]?.data?.subtype;
|
|
2352
|
+
if (!subtype) throw new Error("[annotation] cannot edit an uncommitted annotation");
|
|
2353
|
+
await updateOne(ref, {
|
|
2354
|
+
subtype,
|
|
2355
|
+
contents: text
|
|
2356
|
+
});
|
|
2357
|
+
},
|
|
2358
|
+
setStatus: async (ref, state) => {
|
|
2359
|
+
const t = threadOf(ref);
|
|
2360
|
+
const userId = currentUserId();
|
|
2361
|
+
const previous = userId ? t.review.byReviewer[userId] : void 0;
|
|
2362
|
+
await createConversationAnnot(t.pageObjectNumber, {
|
|
2363
|
+
subtype: "text",
|
|
2364
|
+
rect: t.root.rect,
|
|
2365
|
+
inReplyTo: previous?.ref ?? t.root.ref,
|
|
2366
|
+
state,
|
|
2367
|
+
stateModel: "review",
|
|
2368
|
+
flags: { ...STATUS_FLAGS }
|
|
2369
|
+
});
|
|
2370
|
+
},
|
|
2371
|
+
setMarked: async (ref, marked) => {
|
|
2372
|
+
const t = threadOf(ref);
|
|
2373
|
+
await createConversationAnnot(t.pageObjectNumber, {
|
|
2374
|
+
subtype: "text",
|
|
2375
|
+
rect: t.root.rect,
|
|
2376
|
+
inReplyTo: t.root.ref,
|
|
2377
|
+
state: marked ? "marked" : "unmarked",
|
|
2378
|
+
stateModel: "marked",
|
|
2379
|
+
flags: { ...STATUS_FLAGS }
|
|
2380
|
+
});
|
|
2381
|
+
},
|
|
2382
|
+
remove: deleteOne,
|
|
2383
|
+
removeThread: async (ref) => {
|
|
2384
|
+
const t = threadOf(ref);
|
|
2385
|
+
const members = threadMemberRefs(t);
|
|
2386
|
+
const blocked = members.filter((r) => !deletableOne(r));
|
|
2387
|
+
if (blocked.length > 0) return {
|
|
2388
|
+
deleted: [],
|
|
2389
|
+
failed: blocked.map((r) => ({
|
|
2390
|
+
ref: r,
|
|
2391
|
+
error: /* @__PURE__ */ new Error("delete not permitted")
|
|
2392
|
+
}))
|
|
2393
|
+
};
|
|
2394
|
+
const deleted = [];
|
|
2395
|
+
const failed = [];
|
|
2396
|
+
for (const member of members) {
|
|
2397
|
+
if (failed.length > 0) break;
|
|
2398
|
+
try {
|
|
2399
|
+
await deleteOne(member);
|
|
2400
|
+
deleted.push(member);
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
failed.push({
|
|
2403
|
+
ref: member,
|
|
2404
|
+
error
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return {
|
|
2409
|
+
deleted,
|
|
2410
|
+
failed
|
|
2411
|
+
};
|
|
2412
|
+
},
|
|
2413
|
+
permissionsFor: (ref) => {
|
|
2414
|
+
model().byId[refKey(ref)]?.flags;
|
|
2415
|
+
const t = threadsIndex().byMember.get(refKey(ref)) ?? null;
|
|
2416
|
+
return {
|
|
2417
|
+
canReply: allowsCreate(),
|
|
2418
|
+
canSetStatus: allowsCreate(),
|
|
2419
|
+
canEditText: (() => {
|
|
2420
|
+
const a = model().byId[refKey(ref)];
|
|
2421
|
+
return !!a && annotContentsEditable(a);
|
|
2422
|
+
})(),
|
|
2423
|
+
canDelete: deletableOne(ref),
|
|
2424
|
+
canDeleteThread: t !== null && threadMemberRefs(t).every(deletableOne)
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2088
2428
|
/**
|
|
2089
2429
|
* Per-parent serialization of attached-link reconciles: rapid edits chain
|
|
2090
2430
|
* instead of interleaving (two overlapping runs could double-create
|
|
@@ -2092,41 +2432,54 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2092
2432
|
* chained run converges on the latest desired state.
|
|
2093
2433
|
*/
|
|
2094
2434
|
const linkSyncChains = /* @__PURE__ */ new Map();
|
|
2095
|
-
|
|
2096
|
-
|
|
2435
|
+
/**
|
|
2436
|
+
* Queue a reconcile. `intent` is either an explicit target (a set/clear —
|
|
2437
|
+
* captured by THIS run's closure, so chained sets stay latest-wins) or
|
|
2438
|
+
* `'keep'` (a geometry follow: re-rect the children toward whatever target
|
|
2439
|
+
* the substrate holds AT RUN TIME — so a remote retarget is never undone
|
|
2440
|
+
* by a local move). Returns the chain, so `links.set()` can await commit.
|
|
2441
|
+
*/
|
|
2442
|
+
const scheduleLinkSync = (id, intent) => {
|
|
2443
|
+
const next = (linkSyncChains.get(id) ?? Promise.resolve()).then(() => reconcileLinkChildren(id, intent === "keep" ? linkOf(model(), id) : intent.target));
|
|
2097
2444
|
linkSyncChains.set(id, next);
|
|
2098
2445
|
next.finally(() => {
|
|
2099
2446
|
if (linkSyncChains.get(id) === next) linkSyncChains.delete(id);
|
|
2100
2447
|
});
|
|
2448
|
+
return next;
|
|
2101
2449
|
};
|
|
2102
2450
|
/**
|
|
2103
2451
|
* THE one place attached link children are created, retargeted, re-rected,
|
|
2104
|
-
* or deleted
|
|
2105
|
-
*
|
|
2106
|
-
* the
|
|
2107
|
-
*
|
|
2452
|
+
* or deleted. Declarative: desired state = `desired` target + the parent's
|
|
2453
|
+
* committed geometry (`linkChildRects`); CURRENT state is read straight
|
|
2454
|
+
* from the substrate (`linkChildrenOf`) — no join-key ledger. Results land
|
|
2455
|
+
* as ordinary substrate upserts/removes, so the `linkOf` lens converges
|
|
2456
|
+
* immediately locally and via events everywhere else. Idempotent — foreign
|
|
2108
2457
|
* inconsistencies heal on the next local edit.
|
|
2109
2458
|
*/
|
|
2110
|
-
const reconcileLinkChildren = async (id) => {
|
|
2459
|
+
const reconcileLinkChildren = async (id, desired) => {
|
|
2111
2460
|
const doc = ctx.doc;
|
|
2112
2461
|
const a = model().byId[id];
|
|
2113
2462
|
if (!doc || !a || !a.ref || a.subtype === "link") return;
|
|
2114
2463
|
const crop = cropOf(a.pon);
|
|
2115
2464
|
if (!crop) return;
|
|
2116
2465
|
const page = doc.page(a.pon);
|
|
2117
|
-
const target = writableTarget(
|
|
2118
|
-
const rects =
|
|
2119
|
-
const current =
|
|
2120
|
-
const nextRefs = [];
|
|
2466
|
+
const target = writableTarget(desired);
|
|
2467
|
+
const rects = desired == null ? [] : linkChildRects(a).map((r) => contentToPdfRect(r, crop));
|
|
2468
|
+
const current = linkChildrenOf(model(), id);
|
|
2121
2469
|
try {
|
|
2122
2470
|
const paired = Math.min(current.length, rects.length);
|
|
2123
2471
|
for (let i = 0; i < paired; i++) {
|
|
2124
|
-
const
|
|
2472
|
+
const ref = current[i].ref;
|
|
2473
|
+
if (!ref) continue;
|
|
2474
|
+
const res = await page.annotations.update(ref, {
|
|
2125
2475
|
subtype: "link",
|
|
2126
2476
|
rect: rects[i],
|
|
2127
2477
|
...target ? { target } : {}
|
|
2128
2478
|
});
|
|
2129
|
-
|
|
2479
|
+
apply({
|
|
2480
|
+
t: "upsert",
|
|
2481
|
+
annots: [ingestDTO(res.updated, crop, "baked")]
|
|
2482
|
+
});
|
|
2130
2483
|
}
|
|
2131
2484
|
for (let i = current.length; i < rects.length; i++) {
|
|
2132
2485
|
const res = await page.annotations.create({
|
|
@@ -2136,20 +2489,22 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2136
2489
|
inReplyTo: a.ref,
|
|
2137
2490
|
replyType: "group"
|
|
2138
2491
|
});
|
|
2139
|
-
|
|
2492
|
+
apply({
|
|
2493
|
+
t: "upsert",
|
|
2494
|
+
annots: [ingestDTO(res.created, crop, "baked")]
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
for (let i = rects.length; i < current.length; i++) {
|
|
2498
|
+
const child = current[i];
|
|
2499
|
+
if (child.ref) await page.annotations.delete(child.ref);
|
|
2500
|
+
apply({
|
|
2501
|
+
t: "remove",
|
|
2502
|
+
ids: [child.id]
|
|
2503
|
+
});
|
|
2140
2504
|
}
|
|
2141
|
-
for (let i = rects.length; i < current.length; i++) await page.annotations.delete(current[i]);
|
|
2142
2505
|
} catch (err) {
|
|
2143
2506
|
console.error("[annotation] attached-link sync failed:", err);
|
|
2144
2507
|
}
|
|
2145
|
-
const cur = model().byId[id];
|
|
2146
|
-
if (cur) apply({
|
|
2147
|
-
t: "upsert",
|
|
2148
|
-
annots: [{
|
|
2149
|
-
...cur,
|
|
2150
|
-
linkRefs: nextRefs
|
|
2151
|
-
}]
|
|
2152
|
-
});
|
|
2153
2508
|
};
|
|
2154
2509
|
/** A relationship-only engine patch (sets/clears `/IRT` + `/RT`) — geometry and
|
|
2155
2510
|
* style are left untouched, so grouping never re-bakes an appearance. */
|
|
@@ -2274,9 +2629,14 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2274
2629
|
if (!a || !a.ref || !patch) return;
|
|
2275
2630
|
doc.page(a.pon).annotations.update(a.ref, patch).then((res) => {
|
|
2276
2631
|
syncDTO(res.updated, a.source, res.appearance.changed);
|
|
2277
|
-
if (
|
|
2278
|
-
}, () => {
|
|
2279
|
-
|
|
2632
|
+
if (linkChildrenOf(model(), fx.id).length) scheduleLinkSync(fx.id, "keep");
|
|
2633
|
+
}, () => {
|
|
2634
|
+
if (a.data && crop) apply({
|
|
2635
|
+
t: "upsert",
|
|
2636
|
+
annots: [ingestDTO(a.data, crop, a.source)]
|
|
2637
|
+
});
|
|
2638
|
+
});
|
|
2639
|
+
} else if (fx.fx === "syncLink") scheduleLinkSync(fx.id, { target: fx.target });
|
|
2280
2640
|
else doc.page(fx.ref.pageObjectNumber).annotations.delete(fx.ref).then(() => {}, () => {});
|
|
2281
2641
|
}
|
|
2282
2642
|
return {
|
|
@@ -2329,7 +2689,8 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2329
2689
|
const un = ctx.tryGet(InteractionToken)?.registerTool({
|
|
2330
2690
|
id: resolved.id,
|
|
2331
2691
|
cursor: resolved.cursor,
|
|
2332
|
-
enables: resolved.enables
|
|
2692
|
+
enables: resolved.enables,
|
|
2693
|
+
touchDirect: isTouchDirect(resolved.enables)
|
|
2333
2694
|
});
|
|
2334
2695
|
if (resolved.defaults) apply({
|
|
2335
2696
|
t: "setDefaults",
|
|
@@ -2362,9 +2723,26 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2362
2723
|
ids: [refKey(ref)]
|
|
2363
2724
|
});
|
|
2364
2725
|
},
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2726
|
+
canRead: () => canRead(),
|
|
2727
|
+
canCreate: () => allowsCreate(),
|
|
2728
|
+
canEdit: (ref) => {
|
|
2729
|
+
const a = model().byId[refKey(ref)];
|
|
2730
|
+
return !!a && annotTransformable(a);
|
|
2731
|
+
},
|
|
2732
|
+
canDelete: (ref) => {
|
|
2733
|
+
const a = model().byId[refKey(ref)];
|
|
2734
|
+
return !!a && annotDeletable(a);
|
|
2735
|
+
},
|
|
2736
|
+
comments,
|
|
2737
|
+
links: {
|
|
2738
|
+
of: (ref) => {
|
|
2739
|
+
const a = model().byId[refKey(ref)];
|
|
2740
|
+
if (!a) return null;
|
|
2741
|
+
return a.subtype === "link" ? a.link ?? null : linkOf(model(), a.id);
|
|
2742
|
+
},
|
|
2743
|
+
set: (ref, target) => scheduleLinkSync(refKey(ref), { target }),
|
|
2744
|
+
clear: (ref) => scheduleLinkSync(refKey(ref), { target: null })
|
|
2745
|
+
},
|
|
2368
2746
|
getSelection: () => {
|
|
2369
2747
|
const m = model();
|
|
2370
2748
|
return m.selected.map((id) => m.byId[id]?.ref).filter((r) => r != null);
|
|
@@ -2385,7 +2763,14 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2385
2763
|
selectionAnchor: (scale, rotation, zoom) => memoAnchor(scale, rotation, zoom),
|
|
2386
2764
|
creationDraftAnchor: () => memoDraftAnchor(),
|
|
2387
2765
|
selection: () => model().selected,
|
|
2388
|
-
hitKind: (pon, point, scale, rotation, zoom) => hitTest(model(), pon, point, chromeGeomAt(scale), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)).t,
|
|
2766
|
+
hitKind: (pon, point, scale, rotation, zoom, touch) => hitTest(model(), pon, point, chromeGeomAt(scale, grabBoost(touch)), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)).t,
|
|
2767
|
+
claimsTouchAt: (pon, point, scale, rotation, zoom) => {
|
|
2768
|
+
const m = model();
|
|
2769
|
+
const t = hitTest(m, pon, point, chromeGeomAt(scale, TOUCH_GRAB_BOOST), m.hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation));
|
|
2770
|
+
if (t.t === "handle" || t.t === "rotate" || t.t === "group-handle") return true;
|
|
2771
|
+
if (t.t === "annot") return m.selected.includes(t.id) && m.selected.every((id) => canMove(m, id));
|
|
2772
|
+
return false;
|
|
2773
|
+
},
|
|
2389
2774
|
cursorAt: (pon, point, scale, rotation, zoom) => cursorAt(model(), pon, point, chromeGeomAt(scale), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)),
|
|
2390
2775
|
hoverAt: (at) => {
|
|
2391
2776
|
const m = model();
|
|
@@ -2407,6 +2792,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2407
2792
|
for (const id of m.order) {
|
|
2408
2793
|
const a = m.byId[id];
|
|
2409
2794
|
if (!a || a.pon !== pon || a.source !== "baked" || !a.ref) continue;
|
|
2795
|
+
if (isSubstrateOnly(a)) continue;
|
|
2410
2796
|
parts.push(`${id}@${a.apVersion ?? 0}`);
|
|
2411
2797
|
}
|
|
2412
2798
|
return parts.sort().join("|");
|
|
@@ -2424,7 +2810,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2424
2810
|
const crop = cropOf(pon);
|
|
2425
2811
|
return crop ? pdfToContentRect(rect, crop) : null;
|
|
2426
2812
|
},
|
|
2427
|
-
editPointer: (phase, pon, point, shift, scale, rotation, zoom) => apply({
|
|
2813
|
+
editPointer: (phase, pon, point, shift, scale, rotation, zoom, touch) => apply({
|
|
2428
2814
|
t: "editPointer",
|
|
2429
2815
|
phase,
|
|
2430
2816
|
in: {
|
|
@@ -2432,7 +2818,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2432
2818
|
point,
|
|
2433
2819
|
shift,
|
|
2434
2820
|
pageBox: pageBoxOf(pon),
|
|
2435
|
-
chrome: chromeGeomAt(scale),
|
|
2821
|
+
chrome: chromeGeomAt(scale, grabBoost(touch)),
|
|
2436
2822
|
inert: inertIdsAt(pon),
|
|
2437
2823
|
...zoom != null ? { zoom } : {},
|
|
2438
2824
|
...rotation != null ? { displayRotation: rotation } : {}
|
|
@@ -2452,6 +2838,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2452
2838
|
}
|
|
2453
2839
|
}),
|
|
2454
2840
|
createPointer: (tool, phase, pon, point, finish = false, displayRotation) => {
|
|
2841
|
+
if (!allowsCreate()) return;
|
|
2455
2842
|
const t = registry.get(tool);
|
|
2456
2843
|
apply({
|
|
2457
2844
|
t: "createPointer",
|
|
@@ -2477,30 +2864,60 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2477
2864
|
finishCreationDraft: () => apply({ t: "finishCreationDraft" }),
|
|
2478
2865
|
finishInkDraft: () => apply({ t: "finishInkDraft" }),
|
|
2479
2866
|
cancelCreationDraft: () => apply({ t: "cancel" }),
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2867
|
+
markupFromSelection: (subtype, preset) => {
|
|
2868
|
+
if (!allowsCreate()) return false;
|
|
2869
|
+
const selection = ctx.tryGet(SelectionToken);
|
|
2870
|
+
if (!selection || !selection.hasSelection()) return false;
|
|
2871
|
+
const snapshot = selection.snapshot();
|
|
2872
|
+
let created = false;
|
|
2873
|
+
for (const page of snapshot.pages) {
|
|
2874
|
+
if (!page.segments.length) continue;
|
|
2875
|
+
apply({
|
|
2876
|
+
t: "createMarkup",
|
|
2877
|
+
subtype,
|
|
2878
|
+
pon: page.pon,
|
|
2879
|
+
quads: page.segments.map((s) => s.quad),
|
|
2880
|
+
preset,
|
|
2881
|
+
flags: preset ? registry.get(preset)?.flags : void 0
|
|
2882
|
+
});
|
|
2883
|
+
created = true;
|
|
2884
|
+
}
|
|
2885
|
+
if (created) selection.clear();
|
|
2886
|
+
return created;
|
|
2887
|
+
},
|
|
2888
|
+
createMarkup: (subtype, pon, quads, preset) => {
|
|
2889
|
+
if (!allowsCreate()) return;
|
|
2890
|
+
apply({
|
|
2891
|
+
t: "createMarkup",
|
|
2892
|
+
subtype,
|
|
2893
|
+
pon,
|
|
2894
|
+
quads,
|
|
2895
|
+
preset,
|
|
2896
|
+
flags: preset ? registry.get(preset)?.flags : void 0
|
|
2897
|
+
});
|
|
2898
|
+
},
|
|
2899
|
+
createCaret: (pon, anchor) => {
|
|
2900
|
+
if (!allowsCreate()) return;
|
|
2901
|
+
apply({
|
|
2902
|
+
t: "createCaret",
|
|
2903
|
+
pon,
|
|
2904
|
+
anchor
|
|
2905
|
+
});
|
|
2906
|
+
},
|
|
2907
|
+
createReplaceText: (pon, quads, anchor, preset) => {
|
|
2908
|
+
if (!allowsCreate()) return;
|
|
2909
|
+
apply({
|
|
2910
|
+
t: "createReplaceText",
|
|
2911
|
+
pon,
|
|
2912
|
+
quads,
|
|
2913
|
+
anchor,
|
|
2914
|
+
preset
|
|
2915
|
+
});
|
|
2916
|
+
},
|
|
2917
|
+
previewMarkup: (subtype, quadsByPage, preset) => apply({
|
|
2501
2918
|
t: "setMarkupPreview",
|
|
2502
2919
|
subtype,
|
|
2503
|
-
|
|
2920
|
+
quadsByPage,
|
|
2504
2921
|
preset
|
|
2505
2922
|
}),
|
|
2506
2923
|
clearMarkupPreview: () => apply({ t: "clearMarkupPreview" }),
|
|
@@ -2570,38 +2987,25 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2570
2987
|
await Promise.all(subs.map((a) => writeRelationship(a, { inReplyTo: null })));
|
|
2571
2988
|
},
|
|
2572
2989
|
canGroup: () => {
|
|
2573
|
-
if (!(ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false)) return false;
|
|
2574
2990
|
const m = model();
|
|
2575
2991
|
const members = selectedCommitted();
|
|
2576
2992
|
if (members.length < 2) return false;
|
|
2577
2993
|
if (members.some((a) => a.pon !== members[0].pon)) return false;
|
|
2994
|
+
if (!members.every((a) => a.ref != null && allowsMutation("update", a.ref))) return false;
|
|
2578
2995
|
const keys = new Set(m.selected.map((id) => groupKeyOf(m, id)));
|
|
2579
2996
|
if (keys.size === 1 && !keys.has(null)) return false;
|
|
2580
2997
|
return true;
|
|
2581
2998
|
},
|
|
2582
2999
|
canUngroup: () => {
|
|
2583
|
-
if (!(ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false)) return false;
|
|
2584
3000
|
const m = model();
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
ensurePage: (pon) => {
|
|
2588
|
-
if (loaded.has(pon)) return;
|
|
2589
|
-
const doc = ctx.doc;
|
|
2590
|
-
const crop = cropOf(pon);
|
|
2591
|
-
if (!doc || !crop) return;
|
|
2592
|
-
loaded.add(pon);
|
|
2593
|
-
doc.page(pon).annotations.list().then((snap) => apply({
|
|
2594
|
-
t: "loaded",
|
|
2595
|
-
annots: foldAttachedLinks(snap.annotations.map((d) => fromDTO(d, crop)))
|
|
2596
|
-
}), () => {
|
|
2597
|
-
loaded.delete(pon);
|
|
2598
|
-
});
|
|
3001
|
+
const subs = expandGroups(m, m.selected).map((id) => m.byId[id]).filter((a) => !!a && !!a.group);
|
|
3002
|
+
return subs.length > 0 && subs.every((a) => a.ref != null && allowsMutation("update", a.ref));
|
|
2599
3003
|
},
|
|
3004
|
+
ensurePage: () => {},
|
|
2600
3005
|
reloadPage: async (pon) => {
|
|
2601
3006
|
const doc = ctx.doc;
|
|
2602
3007
|
const crop = cropOf(pon);
|
|
2603
3008
|
if (!doc || !crop) return;
|
|
2604
|
-
loaded.add(pon);
|
|
2605
3009
|
try {
|
|
2606
3010
|
const snap = await doc.page(pon).annotations.list();
|
|
2607
3011
|
const m = model();
|
|
@@ -2612,11 +3016,23 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2612
3016
|
});
|
|
2613
3017
|
apply({
|
|
2614
3018
|
t: "loaded",
|
|
2615
|
-
annots:
|
|
3019
|
+
annots: snap.annotations.map((d) => ingestDTO(d, crop))
|
|
2616
3020
|
});
|
|
2617
|
-
} catch {
|
|
2618
|
-
|
|
3021
|
+
} catch {}
|
|
3022
|
+
},
|
|
3023
|
+
hydration: () => ctx.getState().hydration,
|
|
3024
|
+
ensureHydrated: () => {
|
|
3025
|
+
if (hydrationStarted) return;
|
|
3026
|
+
hydrationStarted = true;
|
|
3027
|
+
runHydration(false);
|
|
3028
|
+
},
|
|
3029
|
+
rehydrate,
|
|
3030
|
+
deliverRemoteAnnotationEvent: (event) => {
|
|
3031
|
+
if (hydrationWindow) {
|
|
3032
|
+
pendingRemote.push(event);
|
|
3033
|
+
return;
|
|
2619
3034
|
}
|
|
3035
|
+
applyRemoteAnnotationEvent(event);
|
|
2620
3036
|
},
|
|
2621
3037
|
textItems: (pon, view) => memoTexts(pon, view),
|
|
2622
3038
|
currentEditing: () => model().editing,
|
|
@@ -2628,10 +3044,14 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2628
3044
|
const m = model();
|
|
2629
3045
|
const h = hitTest(m, pon, point, chromeGeomAt(scale), m.hitMargin, pageBoxOf(pon), void 0, viewEnv(zoom, rotation));
|
|
2630
3046
|
const id = h.t === "annot" || h.t === "handle" ? h.id : null;
|
|
2631
|
-
if (id != null && m.byId[id]?.geom.t === "text")
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
3047
|
+
if (id != null && m.byId[id]?.geom.t === "text") {
|
|
3048
|
+
apply({
|
|
3049
|
+
t: "beginTextEdit",
|
|
3050
|
+
id
|
|
3051
|
+
});
|
|
3052
|
+
return true;
|
|
3053
|
+
}
|
|
3054
|
+
return false;
|
|
2635
3055
|
},
|
|
2636
3056
|
setContents: (ref, text) => {
|
|
2637
3057
|
apply({
|
|
@@ -2675,6 +3095,6 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2675
3095
|
};
|
|
2676
3096
|
}
|
|
2677
3097
|
//#endregion
|
|
2678
|
-
export {
|
|
3098
|
+
export { fromDTO as a, refKey as c, AnnotationToken as d, isTouchDirect as i, styleFromDTO as l, DEFAULT_TOOLS as n, toCreateDraft as o, buildToolRegistry as r, toPatch as s, createAnnotationCapability as t, widgetAppearanceFromProps as u };
|
|
2679
3099
|
|
|
2680
|
-
//# sourceMappingURL=capability-
|
|
3100
|
+
//# sourceMappingURL=capability-BRf0HrOi.js.map
|