@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.
@@ -1,14 +1,16 @@
1
1
  let _embedpdf_core = require("@embedpdf/core");
2
2
  let _embedpdf_plugin_interaction = require("@embedpdf/plugin-interaction");
3
3
  let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
4
+ let _embedpdf_plugin_selection = require("@embedpdf/plugin-selection");
4
5
  let _embedpdf_core_annotation = require("@embedpdf/core-annotation");
6
+ let _embedpdf_core_geometry = require("@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 = (0, _embedpdf_core.createCapabilityToken)("annotation");
13
+ const AnnotationToken = (0, _embedpdf_core.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: (0, _embedpdf_core_annotation.geomPdfBounds)(g, a.style.strokeWidth, crop, a.style.border) };
334
336
  return {};
335
337
  };
336
- /** Stroke-family prop exceptions: width and border feed the derived /Rect. */
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, crop) => ({
339
- strokeWidth: a.style.strokeWidth,
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
- ...a.geom.t === "poly" && a.geom.closed ? visualRect(a, crop) : {}
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
- const quadsFromDTO = (quadPoints, crop) => quadPoints.map((q) => [
526
- (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p1, crop),
527
- (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p2, crop),
528
- (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p3, crop),
529
- (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p4, crop)
530
- ]);
531
- /** Content quads → engine `/QuadPoints` (PDF user space); null off quads geom. */
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) => (0, _embedpdf_core_geometry.normalizeQuad)({
546
+ p1: (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p1, crop),
547
+ p2: (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p2, crop),
548
+ p3: (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p3, crop),
549
+ p4: (0, _embedpdf_core_annotation.pdfToContentPoint)(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: (0, _embedpdf_core_annotation.contentToPdfPoint)(q[0], crop),
536
- p2: (0, _embedpdf_core_annotation.contentToPdfPoint)(q[1], crop),
537
- p3: (0, _embedpdf_core_annotation.contentToPdfPoint)(q[2], crop),
538
- p4: (0, _embedpdf_core_annotation.contentToPdfPoint)(q[3], crop)
557
+ p1: (0, _embedpdf_core_annotation.contentToPdfPoint)(q.upperStart, crop),
558
+ p2: (0, _embedpdf_core_annotation.contentToPdfPoint)(q.upperEnd, crop),
559
+ p3: (0, _embedpdf_core_annotation.contentToPdfPoint)(q.lowerStart, crop),
560
+ p4: (0, _embedpdf_core_annotation.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: (0, _embedpdf_core_annotation.pdfToContentRect)(d.rect, crop)
622
+ rect: (0, _embedpdf_core_annotation.pdfToContentRect)(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" ? { rect: (0, _embedpdf_core_annotation.contentToPdfRect)(a.geom.rect, crop) } : null,
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) => (0, _embedpdf_core_annotation.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) => (0, _embedpdf_core_annotation.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) => (0, _embedpdf_core_annotation.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(quadBounds);
786
+ if (a.geom.t === "quads") return a.geom.quads.map(_embedpdf_core_geometry.textQuadBounds);
801
787
  return [(0, _embedpdf_core_annotation.unionRect)((0, _embedpdf_core_annotation.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 (0, _embedpdf_core_annotation.textBoxes)(m, pon, view).map((tb) => {
950
937
  const a = m.byId[tb.id];
951
938
  const t = a?.text ?? _embedpdf_core_annotation.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
- const chromeGeomAt = (scale) => {
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 = (0, _embedpdf_core_annotation.sharedProps)(members.map((a) => a.subtype));
1643
1645
  const values = {};
1644
1646
  const mixed = [];
1647
+ const valueOf = (a, key) => key === "link" && a.subtype !== "link" ? (0, _embedpdf_core_annotation.linkOf)(m, a.id) : (0, _embedpdf_core_annotation.readProp)(a, key);
1645
1648
  for (const spec of specs) {
1646
- const first = (0, _embedpdf_core_annotation.readProp)(members[0], spec.key);
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((0, _embedpdf_core_annotation.readProp)(a, spec.key)) !== firstJson)) mixed.push(spec.key);
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.link == null) continue;
1673
+ if (!a || a.pon !== pon || a.subtype !== "link") continue;
1671
1674
  if (!(0, _embedpdf_core_annotation.viewable)(a.flags, false)) continue;
1672
- if (a.subtype === "link") {
1673
- if (a.geom.t === "rect") v.push({
1674
- id,
1675
- rect: a.geom.rect,
1676
- target: a.link
1677
- });
1678
- } else linkChildRects(a).forEach((rect, i) => v.push({
1679
- id: `${id}#${i}`,
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: [fromDTO(dto, crop, source)],
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 && !(0, _embedpdf_core_annotation.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 = (0, _embedpdf_engine_core_runtime.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 = (0, _embedpdf_engine_core_runtime.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 && (0, _embedpdf_core_annotation.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 && (0, _embedpdf_core_annotation.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
- const scheduleLinkSync = (id) => {
2096
- const next = (linkSyncChains.get(id) ?? Promise.resolve()).then(() => reconcileLinkChildren(id));
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" ? (0, _embedpdf_core_annotation.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 (the delete-with-parent expansion in the core is the one other
2105
- * consumer of `linkRefs`). Declarative: desired state is derived fresh from
2106
- * the parent's `link` value + committed geometry (`linkChildRects`), then
2107
- * diffed against the join keys the last fold reported. Idempotent — foreign
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(a.link);
2118
- const rects = a.link == null ? [] : linkChildRects(a).map((r) => (0, _embedpdf_core_annotation.contentToPdfRect)(r, crop));
2119
- const current = a.linkRefs ?? [];
2120
- const nextRefs = [];
2466
+ const target = writableTarget(desired);
2467
+ const rects = desired == null ? [] : linkChildRects(a).map((r) => (0, _embedpdf_core_annotation.contentToPdfRect)(r, crop));
2468
+ const current = (0, _embedpdf_core_annotation.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 res = await page.annotations.update(current[i], {
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
- nextRefs.push(res.updated.ref);
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
- nextRefs.push(res.created.ref);
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 (a.linkRefs?.length) scheduleLinkSync(fx.id);
2278
- }, () => {});
2279
- } else if (fx.fx === "syncLink") scheduleLinkSync(fx.id);
2632
+ if ((0, _embedpdf_core_annotation.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(_embedpdf_plugin_interaction.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
- canCreate: () => ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false,
2366
- canEdit: () => ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false,
2367
- canDelete: () => ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false,
2726
+ canRead: () => canRead(),
2727
+ canCreate: () => allowsCreate(),
2728
+ canEdit: (ref) => {
2729
+ const a = model().byId[refKey(ref)];
2730
+ return !!a && (0, _embedpdf_core_annotation.annotTransformable)(a);
2731
+ },
2732
+ canDelete: (ref) => {
2733
+ const a = model().byId[refKey(ref)];
2734
+ return !!a && (0, _embedpdf_core_annotation.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 : (0, _embedpdf_core_annotation.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) => (0, _embedpdf_core_annotation.hitTest)(model(), pon, point, chromeGeomAt(scale), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)).t,
2766
+ hitKind: (pon, point, scale, rotation, zoom, touch) => (0, _embedpdf_core_annotation.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 = (0, _embedpdf_core_annotation.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) => (0, _embedpdf_core_annotation.canMove)(m, id));
2772
+ return false;
2773
+ },
2389
2774
  cursorAt: (pon, point, scale, rotation, zoom) => (0, _embedpdf_core_annotation.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 ((0, _embedpdf_core_annotation.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 ? (0, _embedpdf_core_annotation.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
- createMarkup: (subtype, pon, rects, preset) => apply({
2481
- t: "createMarkup",
2482
- subtype,
2483
- pon,
2484
- rects,
2485
- preset,
2486
- flags: preset ? registry.get(preset)?.flags : void 0
2487
- }),
2488
- createCaret: (pon, textEndRect) => apply({
2489
- t: "createCaret",
2490
- pon,
2491
- rect: textEndRect
2492
- }),
2493
- createReplaceText: (pon, rects, textEndRect, preset) => apply({
2494
- t: "createReplaceText",
2495
- pon,
2496
- rects,
2497
- endRect: textEndRect,
2498
- preset
2499
- }),
2500
- previewMarkup: (subtype, rectsByPage, preset) => apply({
2867
+ markupFromSelection: (subtype, preset) => {
2868
+ if (!allowsCreate()) return false;
2869
+ const selection = ctx.tryGet(_embedpdf_plugin_selection.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
- rectsByPage,
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) => (0, _embedpdf_core_annotation.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
- return m.selected.some((id) => (0, _embedpdf_core_annotation.groupKeyOf)(m, id) != null);
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 = (0, _embedpdf_core_annotation.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: foldAttachedLinks(snap.annotations.map((d) => fromDTO(d, crop)))
3019
+ annots: snap.annotations.map((d) => ingestDTO(d, crop))
2616
3020
  });
2617
- } catch {
2618
- loaded.delete(pon);
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 = (0, _embedpdf_core_annotation.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") apply({
2632
- t: "beginTextEdit",
2633
- id
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({
@@ -2705,6 +3125,12 @@ Object.defineProperty(exports, "fromDTO", {
2705
3125
  return fromDTO;
2706
3126
  }
2707
3127
  });
3128
+ Object.defineProperty(exports, "isTouchDirect", {
3129
+ enumerable: true,
3130
+ get: function() {
3131
+ return isTouchDirect;
3132
+ }
3133
+ });
2708
3134
  Object.defineProperty(exports, "refKey", {
2709
3135
  enumerable: true,
2710
3136
  get: function() {
@@ -2736,4 +3162,4 @@ Object.defineProperty(exports, "widgetAppearanceFromProps", {
2736
3162
  }
2737
3163
  });
2738
3164
 
2739
- //# sourceMappingURL=capability-hb2AU0f6.cjs.map
3165
+ //# sourceMappingURL=capability-Cl9x8oLS.cjs.map