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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/authoring-BhtyfsMx.d.cts +37 -0
  2. package/dist/authoring-R4ezdnXd.d.ts +37 -0
  3. package/dist/authoring.cjs +16 -0
  4. package/dist/authoring.d.cts +2 -0
  5. package/dist/authoring.d.ts +2 -0
  6. package/dist/authoring.js +3 -0
  7. package/dist/{capability-hb2AU0f6.cjs → capability-Csb9WMm-.cjs} +816 -482
  8. package/dist/capability-Csb9WMm-.cjs.map +1 -0
  9. package/dist/{capability-fghl0JmE.js → capability-DBsr2D-L.js} +772 -420
  10. package/dist/capability-DBsr2D-L.js.map +1 -0
  11. package/dist/contract/host.cjs +3 -0
  12. package/dist/contract/host.d.cts +2 -0
  13. package/dist/contract/host.d.ts +2 -0
  14. package/dist/contract/host.js +2 -0
  15. package/dist/contract.cjs +8 -0
  16. package/dist/contract.cjs.map +1 -0
  17. package/dist/contract.d.cts +10 -0
  18. package/dist/contract.d.ts +10 -0
  19. package/dist/contract.js +8 -0
  20. package/dist/contract.js.map +1 -0
  21. package/dist/index.cjs +104 -104
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +5 -47
  24. package/dist/index.d.ts +5 -47
  25. package/dist/index.js +96 -96
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal.cjs +4 -2
  28. package/dist/internal.d.cts +2 -1
  29. package/dist/internal.d.ts +2 -1
  30. package/dist/internal.js +3 -1
  31. package/dist/seam-6pJeKOOJ.cjs +296 -0
  32. package/dist/seam-6pJeKOOJ.cjs.map +1 -0
  33. package/dist/seam-C4L_7rDt.js +225 -0
  34. package/dist/seam-C4L_7rDt.js.map +1 -0
  35. package/dist/types-CwytT-TI.js +13 -0
  36. package/dist/types-CwytT-TI.js.map +1 -0
  37. package/dist/types-DimcLIic.cjs +17 -0
  38. package/dist/types-DimcLIic.cjs.map +1 -0
  39. package/dist/{types-Da6JRmhw.d.ts → types-eLTpocXz.d.cts} +232 -25
  40. package/dist/{types-BhWgqgbX.d.cts → types-otwv-Cdl.d.ts} +232 -25
  41. package/package.json +21 -7
  42. package/dist/capability-fghl0JmE.js.map +0 -1
  43. package/dist/capability-hb2AU0f6.cjs.map +0 -1
@@ -1,236 +1,12 @@
1
- import { CONTINUOUS_RENDER_POLICY, createCapabilityToken, snapAppearanceScale } from "@embedpdf/core";
2
- import { InteractionToken } from "@embedpdf/plugin-interaction";
3
- import { resolveBinarySource, sniffBinaryMetadata } from "@embedpdf/engine-core/runtime";
4
- import { FLAG_KEYS, calloutLinePoints, chrome, clickCreateGeom, cloudyBorderExtent, contentToPdfPoint, contentToPdfRect, contentToPdfRect as contentToPdfRect$1, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, geomPdfBounds, geomRotation, geomVisualBounds, groupKeyOf, hitTest, initialTextStyle, 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";
5
- //#region src/types.ts
6
- /**
7
- * The annotation capability token. Typed to the full {@link AnnotationHostCapability}
8
- * here (the package internals + the `/internal` entry use this view). The package
9
- * root re-exports the SAME token narrowed to {@link AnnotationCapability}.
10
- */
11
- const AnnotationToken = createCapabilityToken("annotation");
12
- //#endregion
13
- //#region src/repository/seam.ts
14
- function refKey(ref) {
15
- return ref.kind === "objectNumber" ? `obj:${ref.annotObjectNumber}` : ref.kind === "nm" ? `nm:${ref.nm}` : `idx:${ref.pageObjectNumber}:${ref.index}`;
16
- }
17
- const h2 = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
18
- const colorToCss = (c) => `#${h2(c.r)}${h2(c.g)}${h2(c.b)}`;
19
- function cssToColor(css) {
20
- const s = css.trim();
21
- const m6 = /^#?([0-9a-f]{6})$/i.exec(s);
22
- if (m6) {
23
- const n = parseInt(m6[1], 16);
24
- return {
25
- r: n >> 16 & 255,
26
- g: n >> 8 & 255,
27
- b: n & 255
28
- };
29
- }
30
- const m3 = /^#?([0-9a-f]{3})$/i.exec(s);
31
- if (m3) {
32
- const [a, b, c] = m3[1];
33
- return {
34
- r: parseInt(a + a, 16),
35
- g: parseInt(b + b, 16),
36
- b: parseInt(c + c, 16)
37
- };
38
- }
39
- return {
40
- r: 0,
41
- g: 0,
42
- b: 0
43
- };
44
- }
45
- const toPdfRotation = (rotCW) => normalizeDeg(-rotCW);
46
- const fromPdfRotation = (rotPdf) => normalizeDeg(-rotPdf);
47
- /** Advisory `rot` for a vertex geom, from a DTO's (PDF-convention) `rotation`.
48
- * Absent → no `rot` key (kept off the geom so unrotated shapes stay clean). */
49
- const rotFromDTO = (rotation) => rotation ? { rot: fromPdfRotation(rotation) } : {};
50
- /**
51
- * Geometry/rotation fields for a BOX kind (square/circle/plain free-text). The
52
- * model's `rect` is the UNROTATED logical box and `rot` the applied tilt, so:
53
- * - rot == 0 → `/Rect` IS the box; the transform pair is stated as `null`
54
- * (TOTAL projection — the engine's tri-state writes preserve omissions, so
55
- * an omitted field would keep a stale rotation instead of flattening it).
56
- * - rot != 0 → `/Rect` is the rotated visual AABB (PDFium clips the baked /AP
57
- * to it), `unrotatedRect` the logical box, `rotation` the PDF angle.
58
- */
59
- function boxGeomFields(rect, rot, crop) {
60
- if (!rot) return {
61
- rect: contentToPdfRect(rect, crop),
62
- rotation: null,
63
- unrotatedRect: null
64
- };
65
- return {
66
- rect: contentToPdfRect(rotatedAabb(rect, rot), crop),
67
- unrotatedRect: contentToPdfRect(rect, crop),
68
- rotation: toPdfRotation(rot)
69
- };
70
- }
71
- /** A box geom (square/circle/stamp) from its DTO: when rotated, the LOCAL box
72
- * is the stored `unrotatedRect` (the AABB `/Rect` is the rendered envelope)
73
- * and `rot` the converted tilt; unrotated, `/Rect` IS the box. */
74
- function boxGeomFromDTO(dto, rotation, unrotatedRect, crop, ellipse) {
75
- const rot = rotation ? fromPdfRotation(rotation) : 0;
76
- return {
77
- t: "rect",
78
- rect: pdfToContentRect(rot && unrotatedRect ? unrotatedRect : dto.rect, crop),
79
- ellipse,
80
- ...rot ? { rot } : {}
81
- };
82
- }
83
- /** Inset a PdfRect by a `/RD` (PDF user space, y-up: all four are non-negative
84
- * insets from the matching `/Rect` edge). Used to recover the callout text box. */
85
- const insetPdfRectByRD = (r, rd) => rd ? {
86
- left: r.left + rd.left,
87
- bottom: r.bottom + rd.bottom,
88
- right: r.right - rd.right,
89
- top: r.top - rd.top
90
- } : r;
91
- /** Engine border fields (`/BS /S`, `/BS /D`, `/BE /I`) → the `Border` union. A
92
- * cloudy effect wins over the underlying border style (which stays solid). */
93
- function borderFromDTO(d) {
94
- if ((d.cloudyIntensity ?? 0) > 0) return {
95
- kind: "cloudy",
96
- intensity: d.cloudyIntensity
97
- };
98
- if (d.borderStyle === "dashed") return {
99
- kind: "dashed",
100
- dash: d.dashArray?.length ? d.dashArray : [3, 3]
101
- };
102
- return { kind: "solid" };
103
- }
104
- /** The writable projection of a `link` value: `goto`/`uri` pass through,
105
- * read-only arms (`javascript`, `named`, `goto-remote`, `launch`,
106
- * `unsupported`) yield `null` — they can be carried, never (re)written. */
107
- function writableTarget(t) {
108
- return t && (t.kind === "goto" || t.kind === "uri") ? t : null;
109
- }
110
- const TEXT_MARKUP = /* @__PURE__ */ new Set([
111
- "highlight",
112
- "underline",
113
- "squiggly",
114
- "strikeout"
115
- ]);
116
- const STROKE_KINDS = /* @__PURE__ */ new Set([
117
- "square",
118
- "circle",
119
- "line",
120
- "polygon",
121
- "polyline",
122
- "ink"
123
- ]);
124
- /** Engine DTO → content-space `Style` (CSS colours, `Border` union). Exported so
125
- * selection-aware UIs can read display values straight off a {@link AnnotationDTO}
126
- * without re-deriving the colour/border mapping. */
127
- function styleFromDTO(dto) {
128
- if (dto.subtype === "widget") return {
129
- color: dto.color ? colorToCss(dto.color) : "#1a1a1a",
130
- interiorColor: dto.interiorColor ? colorToCss(dto.interiorColor) : null,
131
- strokeWidth: dto.strokeWidth,
132
- opacity: 1,
133
- blendMode: dto.blendMode,
134
- border: dto.borderStyle === "dashed" ? {
135
- kind: "dashed",
136
- dash: [3, 3]
137
- } : { kind: "solid" }
138
- };
139
- if (STROKE_KINDS.has(dto.subtype)) {
140
- const d = dto;
141
- return {
142
- color: colorToCss(d.color),
143
- interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
144
- strokeWidth: d.strokeWidth,
145
- opacity: d.opacity,
146
- blendMode: dto.blendMode,
147
- border: borderFromDTO(d)
148
- };
149
- }
150
- if (TEXT_MARKUP.has(dto.subtype)) {
151
- const d = dto;
152
- return {
153
- color: colorToCss(d.color),
154
- interiorColor: null,
155
- strokeWidth: 0,
156
- opacity: d.opacity,
157
- blendMode: dto.blendMode,
158
- border: { kind: "solid" }
159
- };
160
- }
161
- if (dto.subtype === "caret") {
162
- const d = dto;
163
- return {
164
- color: colorToCss(d.color),
165
- interiorColor: null,
166
- strokeWidth: 1,
167
- opacity: d.opacity,
168
- blendMode: dto.blendMode,
169
- border: { kind: "solid" }
170
- };
171
- }
172
- if (dto.subtype === "free-text") {
173
- const d = dto;
174
- return {
175
- color: colorToCss(d.color),
176
- interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
177
- strokeWidth: d.strokeWidth,
178
- opacity: d.opacity,
179
- blendMode: dto.blendMode,
180
- border: borderFromDTO(d)
181
- };
182
- }
183
- if (dto.subtype === "redact") {
184
- const d = dto;
185
- return {
186
- color: colorToCss(d.color),
187
- interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
188
- strokeWidth: 1.5,
189
- opacity: d.opacity,
190
- blendMode: dto.blendMode,
191
- border: { kind: "solid" }
192
- };
193
- }
194
- if (dto.subtype === "text" || dto.subtype === "file-attachment") {
195
- const d = dto;
196
- return {
197
- color: colorToCss(d.color),
198
- interiorColor: null,
199
- strokeWidth: 1,
200
- opacity: d.opacity,
201
- blendMode: dto.blendMode,
202
- border: { kind: "solid" }
203
- };
204
- }
205
- return {
206
- color: "#444444",
207
- interiorColor: null,
208
- strokeWidth: 1,
209
- opacity: 1,
210
- blendMode: dto.blendMode,
211
- border: { kind: "solid" }
212
- };
213
- }
214
- /**
215
- * The flat props vocabulary (CSS colours, house keys) → the engine's widget
216
- * appearance for `doc.forms` authoring — the SAME mapping the widget patch
217
- * lowering uses, exported as a boundary utility so the form plugin can style
218
- * `placeField` from a tool's `currentDefaults` without growing a second CSS
219
- * parser. Absent keys stay absent (the engine writes nothing for them).
220
- */
221
- function widgetAppearanceFromProps(props) {
222
- return {
223
- ...props.color !== void 0 ? { color: cssToColor(props.color) } : {},
224
- ...props.interiorColor !== void 0 ? { interiorColor: props.interiorColor ? cssToColor(props.interiorColor) : null } : {},
225
- ...props.strokeWidth !== void 0 ? { strokeWidth: props.strokeWidth } : {},
226
- ...props.border !== void 0 ? { borderStyle: props.border.kind === "dashed" ? "dashed" : "solid" } : {},
227
- ...props.fontFamily !== void 0 ? { fontFamily: props.fontFamily } : {},
228
- ...props.fontSize !== void 0 ? { fontSize: props.fontSize } : {},
229
- ...props.fontColor !== void 0 ? { fontColor: cssToColor(props.fontColor) } : {},
230
- ...props.textAlign !== void 0 ? { textAlign: props.textAlign } : {}
231
- };
232
- }
233
- //#endregion
1
+ import { a as contentToPdfRect$1, c as insetPdfRectByRD, d as refKey, f as rotFromDTO, g as writableTarget, i as contentToPdfPoint, l as pdfToContentPoint$1, m as toPdfRotation, n as boxGeomFromDTO, o as cssToColor, p as styleFromDTO, r as colorToCss, s as fromPdfRotation, t as boxGeomFields, u as pdfToContentRect$1 } from "./seam-C4L_7rDt.js";
2
+ import { CONTINUOUS_RENDER_POLICY, snapAppearanceScale } from "@embedpdf/core";
3
+ import { ActionsToken, createHoverPump } from "@embedpdf/plugin-actions/contract";
4
+ import { InteractionToken } from "@embedpdf/plugin-interaction/contract";
5
+ import { buildCommentThreads, encodeStableIdKey, resolveBinarySource, sniffBinaryMetadata } from "@embedpdf/engine-core/runtime";
6
+ import { scriptColorToRgb } from "@embedpdf/core-acrojs";
7
+ import { SelectionToken } from "@embedpdf/plugin-selection/contract";
8
+ import { FLAG_KEYS, annotContentsEditable, annotDeletable, annotTransformable, calloutLinePoints, canMove, chrome, clickCreateGeom, cloudyBorderExtent, contentToPdfRect, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, geomPdfBounds, geomRotation, geomVisualBounds, groupKeyOf, hitTest, initialTextStyle, isSubstrateOnly, linkChildrenOf, linkOf, pageItems, pdfToContentPoint, pdfToContentRect, propsFor, readProp, resolveClickPlacement, rotatedAabb, selectionAnchor, selectionQuad, sharedProps, styleFromProps, textBoxes, unionRect, update, uprightRotation, viewable } from "@embedpdf/core-annotation";
9
+ import { normalizeQuad, textQuadBounds } from "@embedpdf/core-geometry";
234
10
  //#region src/repository/props.ts
235
11
  /**
236
12
  * GENERIC per-key prop lowerings — the 1:1 mappings from the flat props
@@ -333,18 +109,29 @@ const visualRect = (a, crop) => {
333
109
  if (g.t === "poly") return { rect: geomPdfBounds(g, a.style.strokeWidth, crop, a.style.border) };
334
110
  return {};
335
111
  };
336
- /** Stroke-family prop exceptions: width and border feed the derived /Rect. */
112
+ /**
113
+ * A lowering whose key is an INPUT of the derived /Rect: the visual bounds
114
+ * ride along with every emission, so a sparse patch can never change an input
115
+ * without re-emitting the derivation. (The bug this kills: patch `lineEndings`
116
+ * alone → the engine re-bakes the /AP inside the stale /Rect → the new
117
+ * arrowhead is clipped in every viewer except the live vector one.)
118
+ */
119
+ const withRect = (lower) => (a, crop) => ({
120
+ ...lower(a, crop),
121
+ ...visualRect(a, crop)
122
+ });
123
+ /** Stroke-family prop exceptions: width, border, AND line endings feed the
124
+ * derived /Rect — an ending's arrowhead reaches well past the endpoint, and
125
+ * ISO 32000 requires /Rect to enclose it. Where a key can't change the
126
+ * bounds (a line's border restyle), the re-emitted rect is an idempotent
127
+ * no-op — uniformity beats per-case reasoning here. */
337
128
  const strokeProps = {
338
- strokeWidth: (a, crop) => ({
339
- strokeWidth: a.style.strokeWidth,
340
- ...visualRect(a, crop)
341
- }),
342
- border: (a, crop) => ({
129
+ strokeWidth: withRect((a) => ({ strokeWidth: a.style.strokeWidth })),
130
+ border: withRect((a) => ({
343
131
  ...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 } : {}
132
+ ...polyCloudy(a)
133
+ })),
134
+ lineEndings: withRect((a) => (a.geom.t === "line" || a.geom.t === "poly") && a.geom.ends ? { lineEndings: a.geom.ends } : {})
348
135
  };
349
136
  const line = {
350
137
  ingest: (dto, crop) => {
@@ -522,20 +309,29 @@ const freeText = {
522
309
  };
523
310
  //#endregion
524
311
  //#region src/repository/kinds/quads.ts
525
- const quadsFromDTO = (quadPoints, crop) => quadPoints.map((q) => [
526
- pdfToContentPoint$1(q.p1, crop),
527
- pdfToContentPoint$1(q.p2, crop),
528
- pdfToContentPoint$1(q.p3, crop),
529
- pdfToContentPoint$1(q.p4, crop)
530
- ]);
531
- /** Content quads → engine `/QuadPoints` (PDF user space); null off quads geom. */
312
+ /**
313
+ * Imported `/QuadPoints` → semantic TextQuads. `normalizeQuad` is a
314
+ * NORMALIZER, not a cast: the de-facto zigzag order passes through, ring-order
315
+ * producers are repaired, and garbage gets a deterministic labeling — so
316
+ * drawing code can never put an underline on the wrong edge of a well-formed
317
+ * quad, rotated ones included.
318
+ */
319
+ const quadsFromDTO = (quadPoints, crop) => quadPoints.map((q) => normalizeQuad({
320
+ p1: pdfToContentPoint$1(q.p1, crop),
321
+ p2: pdfToContentPoint$1(q.p2, crop),
322
+ p3: pdfToContentPoint$1(q.p3, crop),
323
+ p4: pdfToContentPoint$1(q.p4, crop)
324
+ }));
325
+ /** Content quads → engine `/QuadPoints` (PDF user space, zigzag slot order:
326
+ * p1..p4 = upper-start, upper-end, lower-start, lower-end — under the y-flip
327
+ * exactly PDFium's documented TL, TR, BL, BR); null off quads geom. */
532
328
  function quadPointsFor(a, crop) {
533
329
  if (a.geom.t !== "quads") return null;
534
330
  return a.geom.quads.map((q) => ({
535
- p1: contentToPdfPoint(q[0], crop),
536
- p2: contentToPdfPoint(q[1], crop),
537
- p3: contentToPdfPoint(q[2], crop),
538
- p4: contentToPdfPoint(q[3], crop)
331
+ p1: contentToPdfPoint(q.upperStart, crop),
332
+ p2: contentToPdfPoint(q.upperEnd, crop),
333
+ p3: contentToPdfPoint(q.lowerStart, crop),
334
+ p4: contentToPdfPoint(q.lowerEnd, crop)
539
335
  }));
540
336
  }
541
337
  /** PDF-space bounding box of a set of `/QuadPoints` quads — the `/Rect` a
@@ -593,15 +389,17 @@ const strikeout = markupProjection("strikeout");
593
389
  const caret = {
594
390
  ingest: (dto, crop) => {
595
391
  const d = dto;
392
+ const rot = d.rotation ? fromPdfRotation(d.rotation) : 0;
596
393
  return {
597
394
  geom: {
598
395
  t: "caret",
599
- rect: pdfToContentRect$1(d.rect, crop)
396
+ rect: pdfToContentRect$1(rot && d.unrotatedRect ? d.unrotatedRect : d.rect, crop),
397
+ ...rot ? { rot } : {}
600
398
  },
601
399
  ...d.intent ? { intent: d.intent } : {}
602
400
  };
603
401
  },
604
- geometry: (a, crop) => a.geom.t === "caret" ? { rect: contentToPdfRect$1(a.geom.rect, crop) } : null,
402
+ geometry: (a, crop) => a.geom.t === "caret" ? boxGeomFields(a.geom.rect, a.geom.rot ?? 0, crop) : null,
605
403
  draftExtras: (a) => ({
606
404
  rectDifferences: {
607
405
  left: .5,
@@ -744,44 +542,6 @@ const unsupported = {
744
542
  * parent's `link` prop, and the reconciler derives the children it should
745
543
  * have from the parent's committed geometry.
746
544
  */
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
545
  /**
786
546
  * The DESIRED hit rects (content space) of a parent's attached link
787
547
  * children — derived fresh from the parent's committed geometry, never
@@ -797,7 +557,7 @@ const quadBounds = (q) => unionRect(q);
797
557
  * `/QuadPoints` (tier 2).
798
558
  */
799
559
  function linkChildRects(a) {
800
- if (a.geom.t === "quads") return a.geom.quads.map(quadBounds);
560
+ if (a.geom.t === "quads") return a.geom.quads.map(textQuadBounds);
801
561
  return [unionRect(selectionQuad(a.geom, a.style.strokeWidth, a.style.border))];
802
562
  }
803
563
  //#endregion
@@ -918,10 +678,51 @@ function toCreateDraft(a, crop) {
918
678
  return {
919
679
  ...base,
920
680
  ...extras,
681
+ nm: crypto.randomUUID(),
921
682
  flags: a.flags
922
683
  };
923
684
  }
924
685
  //#endregion
686
+ //#region src/hover-feed.ts
687
+ /**
688
+ * The annotation plane's E/X trigger feed — a thin adapter from the ONE
689
+ * pointer-driven hover seam (the `hoverAt` diff) onto the shared hover pump.
690
+ *
691
+ * Anti-cascade is structural and lives in the CALLER's seam placement:
692
+ * reducer-side hover clears (session-hide, remove, reload) never pass
693
+ * through `hoverAt`, so an effect-induced hover loss can never masquerade as
694
+ * a cursor exit. This module only decides WHO is hoverable here:
695
+ *
696
+ * - Drafts (no engine ref) can't carry /AA — skipped.
697
+ * - Widgets and links belong to their own event planes (fill controls /
698
+ * LinkLayer anchors own their pixels). When they ARE hit-testable on this
699
+ * plane, an authoring tool owns them — firing hover actions while editing
700
+ * would be wrong — so they are skipped unconditionally.
701
+ * - Tree-less annotations are FREE: no target, no dispatch, and per-event
702
+ * flags keep a lone /E or /X from dispatching its inert twin.
703
+ */
704
+ function createAnnotationHoverFeed(actions, annotOf) {
705
+ const pump = createHoverPump(actions.dispatch);
706
+ const targetOf = (id) => {
707
+ if (!id) return null;
708
+ const annot = annotOf(id);
709
+ if (!annot?.ref) return null;
710
+ if (annot.subtype.startsWith("widget") || annot.subtype === "link") return null;
711
+ const enter = Boolean(annot.data?.actions?.cursorEnter?.root);
712
+ const exit = Boolean(annot.data?.actions?.cursorExit?.root);
713
+ if (!enter && !exit) return null;
714
+ return {
715
+ ref: annot.ref,
716
+ pon: annot.pon,
717
+ events: {
718
+ enter,
719
+ exit
720
+ }
721
+ };
722
+ };
723
+ return { hover: (id) => pump.hover(targetOf(id)) };
724
+ }
725
+ //#endregion
925
726
  //#region src/text-item.ts
926
727
  /**
927
728
  * The free-text presentation projection: the core's geometry-only `textBoxes`
@@ -949,6 +750,8 @@ function buildTextItems(m, pon, view) {
949
750
  return textBoxes(m, pon, view).map((tb) => {
950
751
  const a = m.byId[tb.id];
951
752
  const t = a?.text ?? initialTextStyle;
753
+ const sw = a?.style.strokeWidth ?? 0;
754
+ const isCallout = a?.geom.t === "text" && !!a.geom.callout;
952
755
  return {
953
756
  id: tb.id,
954
757
  ref: a?.ref ?? null,
@@ -962,7 +765,7 @@ function buildTextItems(m, pon, view) {
962
765
  lineHeight: t.fontSize,
963
766
  color: t.fontColor,
964
767
  align: t.textAlign,
965
- padding: 2,
768
+ padding: isCallout ? sw + 2 : sw,
966
769
  background: a?.style.interiorColor ?? null
967
770
  }
968
771
  };
@@ -1115,6 +918,17 @@ const TOOL_DEFAULT_KEYS = {
1115
918
  const DRAW_TAGS = ["annotation-draw", "annotation-edit"];
1116
919
  const MARKUP_TAGS = ["text-select", "annotation-edit"];
1117
920
  /**
921
+ * Whether arming this ANNOTATION tool is touch consent (`Tool.touchDirect`):
922
+ * drag-CREATE tools — shape/ink draw and text-markup select — take
923
+ * single-finger touch wholesale (finger draws/marks, two fingers navigate:
924
+ * the drawing-app convention). Click-to-PLACE tools (stamp, note) do not: a
925
+ * tap places without any claim, and drags keep scrolling. Derived from the
926
+ * gesture tags, so custom registered tools inherit the right behavior. Only
927
+ * annotation-owned tools run through this — the built-in `pointer` tool also
928
+ * enables `text-select`, but it is registered by the hub, not here.
929
+ */
930
+ const isTouchDirect = (enables) => enables.has("annotation-draw") || enables.has("text-select");
931
+ /**
1118
932
  * The built-in tools — a data mirror of v2's registrations (shapes + lines + ink
1119
933
  * + free text in the draw channel, text markup + caret behind text selection, and
1120
934
  * the click-to-place stamp). Order is display-neutral; the toolbar owns layout.
@@ -1504,10 +1318,6 @@ const viewEnv = (zoom, rotation) => zoom != null || rotation != null ? {
1504
1318
  zoom: zoom ?? 1,
1505
1319
  rotation: rotation ?? 0
1506
1320
  } : 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
1321
  const TEXT_COMMIT_DEBOUNCE_MS = 250;
1512
1322
  /**
1513
1323
  * The annotation shell. The pure `update` runs HERE (so it can emit effects);
@@ -1516,7 +1326,6 @@ const TEXT_COMMIT_DEBOUNCE_MS = 250;
1516
1326
  * ref, patch/delete fire-and-forget.
1517
1327
  */
1518
1328
  function createAnnotationCapability(ctx, config = {}) {
1519
- const loaded = /* @__PURE__ */ new Set();
1520
1329
  const behaviors = [];
1521
1330
  /** Per-annotation debounce timer for the engine `contents` write while typing. */
1522
1331
  const textTimers = /* @__PURE__ */ new Map();
@@ -1527,6 +1336,8 @@ function createAnnotationCapability(ctx, config = {}) {
1527
1336
  * slot. See {@link FilePickerProvider}. */
1528
1337
  let filePickerProvider = null;
1529
1338
  const model = () => ctx.getState().model;
1339
+ const actionsForHover = ctx.tryGet(ActionsToken);
1340
+ const hoverFeed = actionsForHover ? createAnnotationHoverFeed(actionsForHover, (id) => model().byId[id] ?? null) : null;
1530
1341
  const chromeSettings = () => ctx.getState().chrome;
1531
1342
  /**
1532
1343
  * Ids on a page whose Behavior is currently ENGAGED (form widgets under a
@@ -1549,16 +1360,23 @@ function createAnnotationCapability(ctx, config = {}) {
1549
1360
  };
1550
1361
  /** The CSS-px chrome settings converted to CONTENT units by the page's view
1551
1362
  * 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) => {
1363
+ * zoom. No scale → the values are read as content units (headless callers).
1364
+ * `boost` widens the GRAB tolerances only (never the drawn chrome or the
1365
+ * knob's position) — the touch path passes {@link TOUCH_GRAB_BOOST} so
1366
+ * handles present finger-sized targets. */
1367
+ const chromeGeomAt = (scale, boost = 1) => {
1554
1368
  const cs = chromeSettings();
1555
1369
  const s = scale || 1;
1556
1370
  return {
1557
- handleTol: cs.handles.hitSize / 2 / s,
1558
- knobTol: cs.knob.hitSize / 2 / s,
1371
+ handleTol: cs.handles.hitSize / 2 / s * boost,
1372
+ knobTol: cs.knob.hitSize / 2 / s * boost,
1559
1373
  knobOffset: cs.knob.offset / s
1560
1374
  };
1561
1375
  };
1376
+ /** Finger-sized grab zones: 2× the mouse tolerances lands a ~24px handle hit
1377
+ * box in Apple's ~44pt-target territory without moving any visuals. */
1378
+ const TOUCH_GRAB_BOOST = 2;
1379
+ const grabBoost = (touch) => touch ? TOUCH_GRAB_BOOST : 1;
1562
1380
  const itemsCache = /* @__PURE__ */ new Map();
1563
1381
  const chromeCache = /* @__PURE__ */ new Map();
1564
1382
  const memoItems = (pon, view) => {
@@ -1642,11 +1460,12 @@ function createAnnotationCapability(ctx, config = {}) {
1642
1460
  const specs = sharedProps(members.map((a) => a.subtype));
1643
1461
  const values = {};
1644
1462
  const mixed = [];
1463
+ const valueOf = (a, key) => key === "link" && a.subtype !== "link" ? linkOf(m, a.id) : readProp(a, key);
1645
1464
  for (const spec of specs) {
1646
- const first = readProp(members[0], spec.key);
1465
+ const first = valueOf(members[0], spec.key);
1647
1466
  values[spec.key] = first;
1648
1467
  const firstJson = JSON.stringify(first);
1649
- if (members.some((a) => JSON.stringify(readProp(a, spec.key)) !== firstJson)) mixed.push(spec.key);
1468
+ if (members.some((a) => JSON.stringify(valueOf(a, spec.key)) !== firstJson)) mixed.push(spec.key);
1650
1469
  }
1651
1470
  const v = {
1652
1471
  specs,
@@ -1667,19 +1486,26 @@ function createAnnotationCapability(ctx, config = {}) {
1667
1486
  const v = [];
1668
1487
  for (const id of m.order) {
1669
1488
  const a = m.byId[id];
1670
- if (!a || a.pon !== pon || a.link == null) continue;
1489
+ if (!a || a.pon !== pon || a.subtype !== "link") continue;
1671
1490
  if (!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
- }));
1491
+ const target = a.link ?? (a.data?.subtype === "link" ? a.data.target ?? null : null);
1492
+ if (target == null || a.geom.t !== "rect") continue;
1493
+ const activate = a.data?.actions?.activate;
1494
+ const ref = a.ref ?? a.data?.ref ?? void 0;
1495
+ const hoverEnter = Boolean(a.data?.actions?.cursorEnter?.root);
1496
+ const hoverExit = Boolean(a.data?.actions?.cursorExit?.root);
1497
+ v.push({
1498
+ id,
1499
+ rect: a.geom.rect,
1500
+ target,
1501
+ attached: a.group !== void 0,
1502
+ ...activate ? { activate } : {},
1503
+ ...ref ? { ref } : {},
1504
+ ...hoverEnter || hoverExit ? { hoverEvents: {
1505
+ enter: hoverEnter,
1506
+ exit: hoverExit
1507
+ } } : {}
1508
+ });
1683
1509
  }
1684
1510
  linkItemsCache.set(pon, {
1685
1511
  model: m,
@@ -2063,7 +1889,7 @@ function createAnnotationCapability(ctx, config = {}) {
2063
1889
  const crop = cropOf(dto.pageObjectNumber);
2064
1890
  if (crop) apply({
2065
1891
  t: "upsert",
2066
- annots: [fromDTO(dto, crop, source)],
1892
+ annots: [ingestDTO(dto, crop, source)],
2067
1893
  bumpAp
2068
1894
  });
2069
1895
  };
@@ -2085,6 +1911,346 @@ function createAnnotationCapability(ctx, config = {}) {
2085
1911
  });
2086
1912
  for (const fx of effects) perform(fx, next);
2087
1913
  }
1914
+ let hydrationStarted = false;
1915
+ let hydrationWindow = false;
1916
+ let hydrationRunning = null;
1917
+ let hydrateAgain = false;
1918
+ const pendingRemote = [];
1919
+ const setHydration = (hydration) => ctx.dispatch({
1920
+ type: "SET_HYDRATION",
1921
+ hydration
1922
+ });
1923
+ const ingestSnapshot = (snap, bumpAp) => {
1924
+ const annots = [];
1925
+ for (const page of snap.pages) {
1926
+ const crop = cropOf(page.pageState.pageObjectNumber);
1927
+ if (!crop) continue;
1928
+ annots.push(...page.annotations.map((d) => ingestDTO(d, crop)));
1929
+ }
1930
+ apply({
1931
+ t: "hydrated",
1932
+ annots,
1933
+ bumpAp
1934
+ });
1935
+ };
1936
+ const canRead = () => ctx.doc?.security.allows("doc.annotate.read") ?? true;
1937
+ const runHydration = (bumpAp) => {
1938
+ const doc = ctx.doc;
1939
+ if (!doc) return Promise.resolve();
1940
+ if (!canRead()) {
1941
+ setHydration({ status: "forbidden" });
1942
+ return Promise.resolve();
1943
+ }
1944
+ hydrationWindow = true;
1945
+ setHydration({ status: "loading" });
1946
+ const run = doc.annotations.listRawAll().then((snap) => {
1947
+ ingestSnapshot(snap, bumpAp);
1948
+ const head = snap.auditHead;
1949
+ const queued = pendingRemote.splice(0);
1950
+ hydrationWindow = false;
1951
+ for (const event of queued) {
1952
+ const serverId = "origin" in event ? event.origin.serverId : null;
1953
+ if (head !== void 0 && serverId !== null && serverId <= head) continue;
1954
+ applyRemoteAnnotationEvent(event);
1955
+ }
1956
+ setHydration({ status: "complete" });
1957
+ }).catch((error) => {
1958
+ hydrationWindow = false;
1959
+ for (const event of pendingRemote.splice(0)) applyRemoteAnnotationEvent(event);
1960
+ setHydration({
1961
+ status: "error",
1962
+ error
1963
+ });
1964
+ }).finally(() => {
1965
+ hydrationRunning = null;
1966
+ if (hydrateAgain) {
1967
+ hydrateAgain = false;
1968
+ hydrationRunning = runHydration(true);
1969
+ }
1970
+ });
1971
+ hydrationRunning = run;
1972
+ return run;
1973
+ };
1974
+ const rehydrate = () => {
1975
+ if (hydrationRunning) {
1976
+ hydrateAgain = true;
1977
+ return hydrationRunning;
1978
+ }
1979
+ hydrationRunning = runHydration(true);
1980
+ return hydrationRunning;
1981
+ };
1982
+ /**
1983
+ * Fold one REMOTE annotation event into the model (transplanted from the
1984
+ * effects layer so hydration can queue + replay through the same code
1985
+ * path). `bump` re-fetches rasters, `keep` doesn't — driven by the
1986
+ * ENGINE'S appearance echo riding the event, so a remote MOVE costs
1987
+ * peers zero re-renders while a remote restyle refreshes exactly once.
1988
+ */
1989
+ const upsertRemote = (dtos, bumpAp) => {
1990
+ const bump = [];
1991
+ const keep = [];
1992
+ for (const dto of dtos) {
1993
+ const crop = cropOf(dto.pageObjectNumber);
1994
+ if (!crop) continue;
1995
+ const a = ingestDTO(dto, crop, "baked");
1996
+ (bumpAp && !isSubstrateOnly(a) ? bump : keep).push(a);
1997
+ }
1998
+ if (bump.length) apply({
1999
+ t: "upsert",
2000
+ annots: bump,
2001
+ bumpAp: true
2002
+ });
2003
+ if (keep.length) apply({
2004
+ t: "upsert",
2005
+ annots: keep
2006
+ });
2007
+ };
2008
+ const applyRemoteAnnotationEvent = (event) => {
2009
+ switch (event.type) {
2010
+ case "annotation.created":
2011
+ upsertRemote([event.created], true);
2012
+ break;
2013
+ case "annotation.updated":
2014
+ upsertRemote([event.updated], event.appearance.changed);
2015
+ break;
2016
+ case "annotation.moved":
2017
+ upsertRemote(event.moved, false);
2018
+ break;
2019
+ case "annotation.deleted":
2020
+ if (event.deleted) {
2021
+ const key = encodeStableIdKey(event.deleted);
2022
+ if (model().byId[key]) apply({
2023
+ t: "remove",
2024
+ ids: [key]
2025
+ });
2026
+ }
2027
+ break;
2028
+ default: break;
2029
+ }
2030
+ };
2031
+ const currentUserId = () => ctx.doc?.security.identity?.user_id;
2032
+ let threadsMemo = null;
2033
+ /** Value-stable layout key: display order is all the sort consumes, so
2034
+ * the memo must survive hosts that rebuild the pages array per read. */
2035
+ const layoutSignature = () => (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber).join(",");
2036
+ const computeThreadsIndex = () => {
2037
+ const m = model();
2038
+ const dtos = [];
2039
+ for (const id of m.order) {
2040
+ const data = m.byId[id]?.data;
2041
+ if (data) dtos.push(data);
2042
+ }
2043
+ const threads = buildCommentThreads(dtos, { currentUserId: currentUserId() });
2044
+ const pages = ctx.document()?.pages ?? [];
2045
+ const displayIndex = new Map(pages.map((p, i) => [p.pageObjectNumber, i]));
2046
+ threads.sort((a, b) => {
2047
+ const pa = displayIndex.get(a.pageObjectNumber) ?? Number.MAX_SAFE_INTEGER;
2048
+ const pb = displayIndex.get(b.pageObjectNumber) ?? Number.MAX_SAFE_INTEGER;
2049
+ if (pa !== pb) return pa - pb;
2050
+ if (a.root.rect.top !== b.root.rect.top) return b.root.rect.top - a.root.rect.top;
2051
+ const ca = a.root.created ?? "";
2052
+ const cb = b.root.created ?? "";
2053
+ return ca < cb ? -1 : ca > cb ? 1 : 0;
2054
+ });
2055
+ const byMember = /* @__PURE__ */ new Map();
2056
+ for (const t of threads) {
2057
+ byMember.set(refKey(t.root.ref), t);
2058
+ for (const r of t.replies) byMember.set(refKey(r.ref), t);
2059
+ for (const g of t.groupedParts) byMember.set(refKey(g.ref), t);
2060
+ for (const s of t.review.statusRefs) byMember.set(refKey(s), t);
2061
+ }
2062
+ return {
2063
+ threads,
2064
+ byMember
2065
+ };
2066
+ };
2067
+ const threadsIndex = () => {
2068
+ const m = model();
2069
+ const layout = layoutSignature();
2070
+ const userId = currentUserId();
2071
+ if (threadsMemo && threadsMemo.byId === m.byId && threadsMemo.order === m.order && threadsMemo.layout === layout && threadsMemo.userId === userId) return threadsMemo;
2072
+ threadsMemo = {
2073
+ ...computeThreadsIndex(),
2074
+ byId: m.byId,
2075
+ order: m.order,
2076
+ layout,
2077
+ userId
2078
+ };
2079
+ return threadsMemo;
2080
+ };
2081
+ const threadOf = (ref) => {
2082
+ const t = threadsIndex().byMember.get(refKey(ref));
2083
+ if (!t) throw new Error("[annotation] no comment thread contains this ref");
2084
+ return t;
2085
+ };
2086
+ /** Engine create + model sync for conversation-plane annotations (they
2087
+ * never paint, so the render source is immaterial — 'baked' avoids any
2088
+ * vector-scene work). */
2089
+ const createConversationAnnot = async (pon, draft) => {
2090
+ const doc = ctx.doc;
2091
+ if (!doc) throw new Error("[annotation] no document bound");
2092
+ const res = await doc.page(pon).annotations.create(draft);
2093
+ syncDTO(res.created, "baked");
2094
+ return res.created;
2095
+ };
2096
+ const deleteOne = async (ref) => {
2097
+ const doc = ctx.doc;
2098
+ if (!doc) throw new Error("[annotation] no document bound");
2099
+ await doc.page(ref.pageObjectNumber).annotations.delete(ref);
2100
+ apply({
2101
+ t: "remove",
2102
+ ids: [refKey(ref)]
2103
+ });
2104
+ };
2105
+ const REPLY_FLAGS = {
2106
+ print: true,
2107
+ noZoom: true,
2108
+ noRotate: true
2109
+ };
2110
+ const STATUS_FLAGS = {
2111
+ hidden: true,
2112
+ noZoom: true,
2113
+ noRotate: true
2114
+ };
2115
+ /** Coarse authority mirror (doc.annotate.modify) — B3 replaces this with
2116
+ * the collab-scope mirrors; the FLAG split is already the real rule. */
2117
+ const mutationTarget = (ref) => {
2118
+ const d = model().byId[refKey(ref)]?.data;
2119
+ return {
2120
+ ...d?.userId !== void 0 ? { userId: d.userId } : {},
2121
+ ...d?.groupId !== void 0 ? { groupId: d.groupId } : {}
2122
+ };
2123
+ };
2124
+ const allowsCreate = () => ctx.doc?.security.allowsAnnotationCreate() ?? false;
2125
+ /**
2126
+ * Ingest an engine DTO with this session's per-record AUTHORITY projected
2127
+ * onto it (permissions.md) — asked of the SAME mirrors the server enforces
2128
+ * with, against the record's stamped owner. Fused into
2129
+ * `annotTransformable`/`annotDeletable`, so hit-test, chrome, props and
2130
+ * delete all agree with the engine by construction. No security context
2131
+ * (bare local engines, tests) → unstamped → allowed; the engine enforces.
2132
+ */
2133
+ const ingestDTO = (dto, crop, source) => {
2134
+ const a = fromDTO(dto, crop, source);
2135
+ const sec = ctx.doc?.security;
2136
+ if (!sec) return a;
2137
+ const target = {
2138
+ ...dto.userId !== void 0 ? { userId: dto.userId } : {},
2139
+ ...dto.groupId !== void 0 ? { groupId: dto.groupId } : {}
2140
+ };
2141
+ return {
2142
+ ...a,
2143
+ authority: {
2144
+ update: sec.allowsAnnotationMutation("update", target),
2145
+ delete: sec.allowsAnnotationMutation("delete", target)
2146
+ }
2147
+ };
2148
+ };
2149
+ const allowsMutation = (action, ref) => ctx.doc?.security.allowsAnnotationMutation(action, mutationTarget(ref)) ?? false;
2150
+ const deletableOne = (ref) => {
2151
+ const a = model().byId[refKey(ref)];
2152
+ return !!a && annotDeletable(a);
2153
+ };
2154
+ const threadMemberRefs = (t) => [
2155
+ ...t.replies.map((r) => r.ref),
2156
+ ...t.groupedParts.map((g) => g.ref),
2157
+ ...t.review.statusRefs,
2158
+ t.root.ref
2159
+ ];
2160
+ const comments = {
2161
+ threads: () => threadsIndex().threads,
2162
+ thread: (ref) => threadsIndex().byMember.get(refKey(ref)) ?? null,
2163
+ hydration: () => ctx.getState().hydration,
2164
+ rehydrate,
2165
+ reply: async (ref, text) => {
2166
+ const t = threadOf(ref);
2167
+ return (await createConversationAnnot(t.pageObjectNumber, {
2168
+ subtype: "text",
2169
+ rect: t.root.rect,
2170
+ icon: "comment",
2171
+ contents: text,
2172
+ inReplyTo: t.root.ref,
2173
+ flags: { ...REPLY_FLAGS }
2174
+ })).ref;
2175
+ },
2176
+ edit: async (ref, text) => {
2177
+ const subtype = model().byId[refKey(ref)]?.data?.subtype;
2178
+ if (!subtype) throw new Error("[annotation] cannot edit an uncommitted annotation");
2179
+ await updateOne(ref, {
2180
+ subtype,
2181
+ contents: text
2182
+ });
2183
+ },
2184
+ setStatus: async (ref, state) => {
2185
+ const t = threadOf(ref);
2186
+ const userId = currentUserId();
2187
+ const previous = userId ? t.review.byReviewer[userId] : void 0;
2188
+ await createConversationAnnot(t.pageObjectNumber, {
2189
+ subtype: "text",
2190
+ rect: t.root.rect,
2191
+ inReplyTo: previous?.ref ?? t.root.ref,
2192
+ state,
2193
+ stateModel: "review",
2194
+ flags: { ...STATUS_FLAGS }
2195
+ });
2196
+ },
2197
+ setMarked: async (ref, marked) => {
2198
+ const t = threadOf(ref);
2199
+ await createConversationAnnot(t.pageObjectNumber, {
2200
+ subtype: "text",
2201
+ rect: t.root.rect,
2202
+ inReplyTo: t.root.ref,
2203
+ state: marked ? "marked" : "unmarked",
2204
+ stateModel: "marked",
2205
+ flags: { ...STATUS_FLAGS }
2206
+ });
2207
+ },
2208
+ remove: deleteOne,
2209
+ removeThread: async (ref) => {
2210
+ const t = threadOf(ref);
2211
+ const members = threadMemberRefs(t);
2212
+ const blocked = members.filter((r) => !deletableOne(r));
2213
+ if (blocked.length > 0) return {
2214
+ deleted: [],
2215
+ failed: blocked.map((r) => ({
2216
+ ref: r,
2217
+ error: /* @__PURE__ */ new Error("delete not permitted")
2218
+ }))
2219
+ };
2220
+ const deleted = [];
2221
+ const failed = [];
2222
+ for (const member of members) {
2223
+ if (failed.length > 0) break;
2224
+ try {
2225
+ await deleteOne(member);
2226
+ deleted.push(member);
2227
+ } catch (error) {
2228
+ failed.push({
2229
+ ref: member,
2230
+ error
2231
+ });
2232
+ }
2233
+ }
2234
+ return {
2235
+ deleted,
2236
+ failed
2237
+ };
2238
+ },
2239
+ permissionsFor: (ref) => {
2240
+ model().byId[refKey(ref)]?.flags;
2241
+ const t = threadsIndex().byMember.get(refKey(ref)) ?? null;
2242
+ return {
2243
+ canReply: allowsCreate(),
2244
+ canSetStatus: allowsCreate(),
2245
+ canEditText: (() => {
2246
+ const a = model().byId[refKey(ref)];
2247
+ return !!a && annotContentsEditable(a);
2248
+ })(),
2249
+ canDelete: deletableOne(ref),
2250
+ canDeleteThread: t !== null && threadMemberRefs(t).every(deletableOne)
2251
+ };
2252
+ }
2253
+ };
2088
2254
  /**
2089
2255
  * Per-parent serialization of attached-link reconciles: rapid edits chain
2090
2256
  * instead of interleaving (two overlapping runs could double-create
@@ -2092,41 +2258,54 @@ function createAnnotationCapability(ctx, config = {}) {
2092
2258
  * chained run converges on the latest desired state.
2093
2259
  */
2094
2260
  const linkSyncChains = /* @__PURE__ */ new Map();
2095
- const scheduleLinkSync = (id) => {
2096
- const next = (linkSyncChains.get(id) ?? Promise.resolve()).then(() => reconcileLinkChildren(id));
2261
+ /**
2262
+ * Queue a reconcile. `intent` is either an explicit target (a set/clear —
2263
+ * captured by THIS run's closure, so chained sets stay latest-wins) or
2264
+ * `'keep'` (a geometry follow: re-rect the children toward whatever target
2265
+ * the substrate holds AT RUN TIME — so a remote retarget is never undone
2266
+ * by a local move). Returns the chain, so `links.set()` can await commit.
2267
+ */
2268
+ const scheduleLinkSync = (id, intent) => {
2269
+ const next = (linkSyncChains.get(id) ?? Promise.resolve()).then(() => reconcileLinkChildren(id, intent === "keep" ? linkOf(model(), id) : intent.target));
2097
2270
  linkSyncChains.set(id, next);
2098
2271
  next.finally(() => {
2099
2272
  if (linkSyncChains.get(id) === next) linkSyncChains.delete(id);
2100
2273
  });
2274
+ return next;
2101
2275
  };
2102
2276
  /**
2103
2277
  * 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
2278
+ * or deleted. Declarative: desired state = `desired` target + the parent's
2279
+ * committed geometry (`linkChildRects`); CURRENT state is read straight
2280
+ * from the substrate (`linkChildrenOf`) — no join-key ledger. Results land
2281
+ * as ordinary substrate upserts/removes, so the `linkOf` lens converges
2282
+ * immediately locally and via events everywhere else. Idempotent — foreign
2108
2283
  * inconsistencies heal on the next local edit.
2109
2284
  */
2110
- const reconcileLinkChildren = async (id) => {
2285
+ const reconcileLinkChildren = async (id, desired) => {
2111
2286
  const doc = ctx.doc;
2112
2287
  const a = model().byId[id];
2113
2288
  if (!doc || !a || !a.ref || a.subtype === "link") return;
2114
2289
  const crop = cropOf(a.pon);
2115
2290
  if (!crop) return;
2116
2291
  const page = doc.page(a.pon);
2117
- const target = writableTarget(a.link);
2118
- const rects = a.link == null ? [] : linkChildRects(a).map((r) => contentToPdfRect(r, crop));
2119
- const current = a.linkRefs ?? [];
2120
- const nextRefs = [];
2292
+ const target = writableTarget(desired);
2293
+ const rects = desired == null ? [] : linkChildRects(a).map((r) => contentToPdfRect(r, crop));
2294
+ const current = linkChildrenOf(model(), id);
2121
2295
  try {
2122
2296
  const paired = Math.min(current.length, rects.length);
2123
2297
  for (let i = 0; i < paired; i++) {
2124
- const res = await page.annotations.update(current[i], {
2298
+ const ref = current[i].ref;
2299
+ if (!ref) continue;
2300
+ const res = await page.annotations.update(ref, {
2125
2301
  subtype: "link",
2126
2302
  rect: rects[i],
2127
2303
  ...target ? { target } : {}
2128
2304
  });
2129
- nextRefs.push(res.updated.ref);
2305
+ apply({
2306
+ t: "upsert",
2307
+ annots: [ingestDTO(res.updated, crop, "baked")]
2308
+ });
2130
2309
  }
2131
2310
  for (let i = current.length; i < rects.length; i++) {
2132
2311
  const res = await page.annotations.create({
@@ -2136,20 +2315,22 @@ function createAnnotationCapability(ctx, config = {}) {
2136
2315
  inReplyTo: a.ref,
2137
2316
  replyType: "group"
2138
2317
  });
2139
- nextRefs.push(res.created.ref);
2318
+ apply({
2319
+ t: "upsert",
2320
+ annots: [ingestDTO(res.created, crop, "baked")]
2321
+ });
2322
+ }
2323
+ for (let i = rects.length; i < current.length; i++) {
2324
+ const child = current[i];
2325
+ if (child.ref) await page.annotations.delete(child.ref);
2326
+ apply({
2327
+ t: "remove",
2328
+ ids: [child.id]
2329
+ });
2140
2330
  }
2141
- for (let i = rects.length; i < current.length; i++) await page.annotations.delete(current[i]);
2142
2331
  } catch (err) {
2143
2332
  console.error("[annotation] attached-link sync failed:", err);
2144
2333
  }
2145
- const cur = model().byId[id];
2146
- if (cur) apply({
2147
- t: "upsert",
2148
- annots: [{
2149
- ...cur,
2150
- linkRefs: nextRefs
2151
- }]
2152
- });
2153
2334
  };
2154
2335
  /** A relationship-only engine patch (sets/clears `/IRT` + `/RT`) — geometry and
2155
2336
  * style are left untouched, so grouping never re-bakes an appearance. */
@@ -2274,11 +2455,70 @@ function createAnnotationCapability(ctx, config = {}) {
2274
2455
  if (!a || !a.ref || !patch) return;
2275
2456
  doc.page(a.pon).annotations.update(a.ref, patch).then((res) => {
2276
2457
  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);
2280
- else doc.page(fx.ref.pageObjectNumber).annotations.delete(fx.ref).then(() => {}, () => {});
2458
+ if (linkChildrenOf(model(), fx.id).length) scheduleLinkSync(fx.id, "keep");
2459
+ }, () => {
2460
+ if (a.data && crop) apply({
2461
+ t: "upsert",
2462
+ annots: [ingestDTO(a.data, crop, a.source)]
2463
+ });
2464
+ });
2465
+ } else if (fx.fx === "syncLink") scheduleLinkSync(fx.id, { target: fx.target });
2466
+ else {
2467
+ refKey(fx.ref);
2468
+ doc.page(fx.ref.pageObjectNumber).annotations.delete(fx.ref).then(() => {}, () => {});
2469
+ }
2281
2470
  }
2471
+ const reloadPageNow = async (pon) => {
2472
+ const doc = ctx.doc;
2473
+ const crop = cropOf(pon);
2474
+ if (!doc || !crop) return;
2475
+ try {
2476
+ const snap = await doc.page(pon).annotations.list();
2477
+ const m = model();
2478
+ const stale = m.order.filter((id) => m.byId[id]?.pon === pon);
2479
+ if (stale.length) apply({
2480
+ t: "remove",
2481
+ ids: stale
2482
+ });
2483
+ apply({
2484
+ t: "loaded",
2485
+ annots: snap.annotations.map((d) => ingestDTO(d, crop))
2486
+ });
2487
+ } catch {}
2488
+ };
2489
+ /** Script patch → the engine's per-kind patch vocabulary. Colors cross the
2490
+ * Acrobat-array → engine {r,g,b}/255 boundary here; everything else maps
2491
+ * one-to-one (the VM's validity matrix already scoped keys per kind). */
2492
+ const engineScriptPatch = (subtype, patch) => {
2493
+ const out = { subtype };
2494
+ const toEngineColor = (color) => {
2495
+ const rgb = scriptColorToRgb(color);
2496
+ return rgb ? {
2497
+ r: Math.round(rgb.r * 255),
2498
+ g: Math.round(rgb.g * 255),
2499
+ b: Math.round(rgb.b * 255)
2500
+ } : null;
2501
+ };
2502
+ if (patch.strokeColor) {
2503
+ const color = toEngineColor(patch.strokeColor);
2504
+ if (!color) return /* @__PURE__ */ new Error("transparent stroke colours are not supported");
2505
+ out.color = color;
2506
+ }
2507
+ if (patch.fillColor) out.interiorColor = toEngineColor(patch.fillColor);
2508
+ if (patch.opacity !== void 0) out.opacity = patch.opacity;
2509
+ if (patch.width !== void 0) out.strokeWidth = patch.width;
2510
+ if (patch.borderStyle) out.borderStyle = patch.borderStyle === "D" ? "dashed" : "solid";
2511
+ if (patch.dash) out.dashArray = patch.dash;
2512
+ if (patch.rect) out.rect = {
2513
+ left: patch.rect[0],
2514
+ bottom: patch.rect[1],
2515
+ right: patch.rect[2],
2516
+ top: patch.rect[3]
2517
+ };
2518
+ if (patch.contents !== void 0) out.contents = patch.contents;
2519
+ if (patch.flags) out.flags = patch.flags;
2520
+ return out;
2521
+ };
2282
2522
  return {
2283
2523
  create: async (pon, draft) => {
2284
2524
  const doc = ctx.doc;
@@ -2329,7 +2569,8 @@ function createAnnotationCapability(ctx, config = {}) {
2329
2569
  const un = ctx.tryGet(InteractionToken)?.registerTool({
2330
2570
  id: resolved.id,
2331
2571
  cursor: resolved.cursor,
2332
- enables: resolved.enables
2572
+ enables: resolved.enables,
2573
+ touchDirect: isTouchDirect(resolved.enables)
2333
2574
  });
2334
2575
  if (resolved.defaults) apply({
2335
2576
  t: "setDefaults",
@@ -2362,9 +2603,26 @@ function createAnnotationCapability(ctx, config = {}) {
2362
2603
  ids: [refKey(ref)]
2363
2604
  });
2364
2605
  },
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,
2606
+ canRead: () => canRead(),
2607
+ canCreate: () => allowsCreate(),
2608
+ canEdit: (ref) => {
2609
+ const a = model().byId[refKey(ref)];
2610
+ return !!a && annotTransformable(a);
2611
+ },
2612
+ canDelete: (ref) => {
2613
+ const a = model().byId[refKey(ref)];
2614
+ return !!a && annotDeletable(a);
2615
+ },
2616
+ comments,
2617
+ links: {
2618
+ of: (ref) => {
2619
+ const a = model().byId[refKey(ref)];
2620
+ if (!a) return null;
2621
+ return a.subtype === "link" ? a.link ?? null : linkOf(model(), a.id);
2622
+ },
2623
+ set: (ref, target) => scheduleLinkSync(refKey(ref), { target }),
2624
+ clear: (ref) => scheduleLinkSync(refKey(ref), { target: null })
2625
+ },
2368
2626
  getSelection: () => {
2369
2627
  const m = model();
2370
2628
  return m.selected.map((id) => m.byId[id]?.ref).filter((r) => r != null);
@@ -2385,7 +2643,14 @@ function createAnnotationCapability(ctx, config = {}) {
2385
2643
  selectionAnchor: (scale, rotation, zoom) => memoAnchor(scale, rotation, zoom),
2386
2644
  creationDraftAnchor: () => memoDraftAnchor(),
2387
2645
  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,
2646
+ 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,
2647
+ claimsTouchAt: (pon, point, scale, rotation, zoom) => {
2648
+ const m = model();
2649
+ const t = hitTest(m, pon, point, chromeGeomAt(scale, TOUCH_GRAB_BOOST), m.hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation));
2650
+ if (t.t === "handle" || t.t === "rotate" || t.t === "group-handle") return true;
2651
+ if (t.t === "annot") return m.selected.includes(t.id) && m.selected.every((id) => canMove(m, id));
2652
+ return false;
2653
+ },
2389
2654
  cursorAt: (pon, point, scale, rotation, zoom) => cursorAt(model(), pon, point, chromeGeomAt(scale), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)),
2390
2655
  hoverAt: (at) => {
2391
2656
  const m = model();
@@ -2394,10 +2659,13 @@ function createAnnotationCapability(ctx, config = {}) {
2394
2659
  const h = hitTest(m, at.pon, at.point, chromeGeomAt(at.scale), m.hitMargin, pageBoxOf(at.pon), inertIdsAt(at.pon), viewEnv(at.zoom, at.rotation));
2395
2660
  if (h.t === "annot") id = h.id;
2396
2661
  }
2397
- if (m.hovered !== id) apply({
2398
- t: "hover",
2399
- id
2400
- });
2662
+ if (m.hovered !== id) {
2663
+ hoverFeed?.hover(id);
2664
+ apply({
2665
+ t: "hover",
2666
+ id
2667
+ });
2668
+ }
2401
2669
  },
2402
2670
  behaviorFor: (a) => behaviors.find((b) => b.matches(a) && b.engaged()) ?? null,
2403
2671
  linkItemsOn: (pon) => memoLinkItems(pon),
@@ -2407,6 +2675,7 @@ function createAnnotationCapability(ctx, config = {}) {
2407
2675
  for (const id of m.order) {
2408
2676
  const a = m.byId[id];
2409
2677
  if (!a || a.pon !== pon || a.source !== "baked" || !a.ref) continue;
2678
+ if (isSubstrateOnly(a)) continue;
2410
2679
  parts.push(`${id}@${a.apVersion ?? 0}`);
2411
2680
  }
2412
2681
  return parts.sort().join("|");
@@ -2424,7 +2693,7 @@ function createAnnotationCapability(ctx, config = {}) {
2424
2693
  const crop = cropOf(pon);
2425
2694
  return crop ? pdfToContentRect(rect, crop) : null;
2426
2695
  },
2427
- editPointer: (phase, pon, point, shift, scale, rotation, zoom) => apply({
2696
+ editPointer: (phase, pon, point, shift, scale, rotation, zoom, touch) => apply({
2428
2697
  t: "editPointer",
2429
2698
  phase,
2430
2699
  in: {
@@ -2432,7 +2701,7 @@ function createAnnotationCapability(ctx, config = {}) {
2432
2701
  point,
2433
2702
  shift,
2434
2703
  pageBox: pageBoxOf(pon),
2435
- chrome: chromeGeomAt(scale),
2704
+ chrome: chromeGeomAt(scale, grabBoost(touch)),
2436
2705
  inert: inertIdsAt(pon),
2437
2706
  ...zoom != null ? { zoom } : {},
2438
2707
  ...rotation != null ? { displayRotation: rotation } : {}
@@ -2452,6 +2721,7 @@ function createAnnotationCapability(ctx, config = {}) {
2452
2721
  }
2453
2722
  }),
2454
2723
  createPointer: (tool, phase, pon, point, finish = false, displayRotation) => {
2724
+ if (!allowsCreate()) return;
2455
2725
  const t = registry.get(tool);
2456
2726
  apply({
2457
2727
  t: "createPointer",
@@ -2477,30 +2747,60 @@ function createAnnotationCapability(ctx, config = {}) {
2477
2747
  finishCreationDraft: () => apply({ t: "finishCreationDraft" }),
2478
2748
  finishInkDraft: () => apply({ t: "finishInkDraft" }),
2479
2749
  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({
2750
+ markupFromSelection: (subtype, preset) => {
2751
+ if (!allowsCreate()) return false;
2752
+ const selection = ctx.tryGet(SelectionToken);
2753
+ if (!selection || !selection.hasSelection()) return false;
2754
+ const snapshot = selection.snapshot();
2755
+ let created = false;
2756
+ for (const page of snapshot.pages) {
2757
+ if (!page.segments.length) continue;
2758
+ apply({
2759
+ t: "createMarkup",
2760
+ subtype,
2761
+ pon: page.pon,
2762
+ quads: page.segments.map((s) => s.quad),
2763
+ preset,
2764
+ flags: preset ? registry.get(preset)?.flags : void 0
2765
+ });
2766
+ created = true;
2767
+ }
2768
+ if (created) selection.clear();
2769
+ return created;
2770
+ },
2771
+ createMarkup: (subtype, pon, quads, preset) => {
2772
+ if (!allowsCreate()) return;
2773
+ apply({
2774
+ t: "createMarkup",
2775
+ subtype,
2776
+ pon,
2777
+ quads,
2778
+ preset,
2779
+ flags: preset ? registry.get(preset)?.flags : void 0
2780
+ });
2781
+ },
2782
+ createCaret: (pon, anchor) => {
2783
+ if (!allowsCreate()) return;
2784
+ apply({
2785
+ t: "createCaret",
2786
+ pon,
2787
+ anchor
2788
+ });
2789
+ },
2790
+ createReplaceText: (pon, quads, anchor, preset) => {
2791
+ if (!allowsCreate()) return;
2792
+ apply({
2793
+ t: "createReplaceText",
2794
+ pon,
2795
+ quads,
2796
+ anchor,
2797
+ preset
2798
+ });
2799
+ },
2800
+ previewMarkup: (subtype, quadsByPage, preset) => apply({
2501
2801
  t: "setMarkupPreview",
2502
2802
  subtype,
2503
- rectsByPage,
2803
+ quadsByPage,
2504
2804
  preset
2505
2805
  }),
2506
2806
  clearMarkupPreview: () => apply({ t: "clearMarkupPreview" }),
@@ -2570,53 +2870,35 @@ function createAnnotationCapability(ctx, config = {}) {
2570
2870
  await Promise.all(subs.map((a) => writeRelationship(a, { inReplyTo: null })));
2571
2871
  },
2572
2872
  canGroup: () => {
2573
- if (!(ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false)) return false;
2574
2873
  const m = model();
2575
2874
  const members = selectedCommitted();
2576
2875
  if (members.length < 2) return false;
2577
2876
  if (members.some((a) => a.pon !== members[0].pon)) return false;
2877
+ if (!members.every((a) => a.ref != null && allowsMutation("update", a.ref))) return false;
2578
2878
  const keys = new Set(m.selected.map((id) => groupKeyOf(m, id)));
2579
2879
  if (keys.size === 1 && !keys.has(null)) return false;
2580
2880
  return true;
2581
2881
  },
2582
2882
  canUngroup: () => {
2583
- if (!(ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false)) return false;
2584
2883
  const m = model();
2585
- return m.selected.some((id) => groupKeyOf(m, id) != null);
2884
+ const subs = expandGroups(m, m.selected).map((id) => m.byId[id]).filter((a) => !!a && !!a.group);
2885
+ return subs.length > 0 && subs.every((a) => a.ref != null && allowsMutation("update", a.ref));
2586
2886
  },
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
- });
2887
+ ensurePage: () => {},
2888
+ reloadPage: (pon) => reloadPageNow(pon),
2889
+ hydration: () => ctx.getState().hydration,
2890
+ ensureHydrated: () => {
2891
+ if (hydrationStarted) return;
2892
+ hydrationStarted = true;
2893
+ runHydration(false);
2599
2894
  },
2600
- reloadPage: async (pon) => {
2601
- const doc = ctx.doc;
2602
- const crop = cropOf(pon);
2603
- if (!doc || !crop) return;
2604
- loaded.add(pon);
2605
- try {
2606
- const snap = await doc.page(pon).annotations.list();
2607
- const m = model();
2608
- const stale = m.order.filter((id) => m.byId[id]?.pon === pon);
2609
- if (stale.length) apply({
2610
- t: "remove",
2611
- ids: stale
2612
- });
2613
- apply({
2614
- t: "loaded",
2615
- annots: foldAttachedLinks(snap.annotations.map((d) => fromDTO(d, crop)))
2616
- });
2617
- } catch {
2618
- loaded.delete(pon);
2895
+ rehydrate,
2896
+ deliverRemoteAnnotationEvent: (event) => {
2897
+ if (hydrationWindow) {
2898
+ pendingRemote.push(event);
2899
+ return;
2619
2900
  }
2901
+ applyRemoteAnnotationEvent(event);
2620
2902
  },
2621
2903
  textItems: (pon, view) => memoTexts(pon, view),
2622
2904
  currentEditing: () => model().editing,
@@ -2628,10 +2910,14 @@ function createAnnotationCapability(ctx, config = {}) {
2628
2910
  const m = model();
2629
2911
  const h = hitTest(m, pon, point, chromeGeomAt(scale), m.hitMargin, pageBoxOf(pon), void 0, viewEnv(zoom, rotation));
2630
2912
  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
- });
2913
+ if (id != null && m.byId[id]?.geom.t === "text") {
2914
+ apply({
2915
+ t: "beginTextEdit",
2916
+ id
2917
+ });
2918
+ return true;
2919
+ }
2920
+ return false;
2635
2921
  },
2636
2922
  setContents: (ref, text) => {
2637
2923
  apply({
@@ -2665,6 +2951,72 @@ function createAnnotationCapability(ctx, config = {}) {
2665
2951
  textTimers.clear();
2666
2952
  apply({ t: "endTextEdit" });
2667
2953
  },
2954
+ commitScriptEffects: async (entries) => {
2955
+ const doc = ctx.doc;
2956
+ if (!doc) return { results: entries.map((entry) => ({
2957
+ annotObjectNumber: entry.annotObjectNumber,
2958
+ status: "failed",
2959
+ error: "no document"
2960
+ })) };
2961
+ const results = [];
2962
+ const touchedPons = /* @__PURE__ */ new Set();
2963
+ let failed = false;
2964
+ for (const entry of entries) {
2965
+ if (failed) {
2966
+ results.push({
2967
+ annotObjectNumber: entry.annotObjectNumber,
2968
+ status: "skipped"
2969
+ });
2970
+ continue;
2971
+ }
2972
+ const loaded = model().byId[`obj:${entry.annotObjectNumber}`];
2973
+ const pon = loaded?.pon ?? entry.pageObjectNumber;
2974
+ let ref = loaded?.ref ?? null;
2975
+ let subtype = loaded?.subtype;
2976
+ if ((!ref || !subtype) && pon !== void 0) try {
2977
+ const { annotations } = await doc.page(pon).annotations.list();
2978
+ const dto = annotations.find((candidate) => candidate.ref.kind === "objectNumber" && candidate.ref.annotObjectNumber === entry.annotObjectNumber);
2979
+ if (dto) {
2980
+ ref = dto.ref;
2981
+ subtype = dto.subtype;
2982
+ }
2983
+ } catch {}
2984
+ if (pon === void 0 || !ref || !subtype) {
2985
+ results.push({
2986
+ annotObjectNumber: entry.annotObjectNumber,
2987
+ status: "failed",
2988
+ error: "annotation not resolved (page not loaded)"
2989
+ });
2990
+ continue;
2991
+ }
2992
+ const patch = engineScriptPatch(subtype, entry.patch);
2993
+ if (patch instanceof Error) {
2994
+ results.push({
2995
+ annotObjectNumber: entry.annotObjectNumber,
2996
+ status: "failed",
2997
+ error: patch.message
2998
+ });
2999
+ continue;
3000
+ }
3001
+ try {
3002
+ await doc.page(pon).annotations.update(ref, patch);
3003
+ touchedPons.add(pon);
3004
+ results.push({
3005
+ annotObjectNumber: entry.annotObjectNumber,
3006
+ status: "applied"
3007
+ });
3008
+ } catch (error) {
3009
+ failed = true;
3010
+ results.push({
3011
+ annotObjectNumber: entry.annotObjectNumber,
3012
+ status: "failed",
3013
+ error: error instanceof Error ? error.message : String(error)
3014
+ });
3015
+ }
3016
+ }
3017
+ for (const touched of touchedPons) await reloadPageNow(touched);
3018
+ return { results };
3019
+ },
2668
3020
  registerBehavior: (b) => {
2669
3021
  behaviors.push(b);
2670
3022
  return () => {
@@ -2675,6 +3027,6 @@ function createAnnotationCapability(ctx, config = {}) {
2675
3027
  };
2676
3028
  }
2677
3029
  //#endregion
2678
- export { toCreateDraft as a, styleFromDTO as c, fromDTO as i, widgetAppearanceFromProps as l, DEFAULT_TOOLS as n, toPatch as o, buildToolRegistry as r, refKey as s, createAnnotationCapability as t, AnnotationToken as u };
3030
+ export { fromDTO as a, isTouchDirect as i, DEFAULT_TOOLS as n, toCreateDraft as o, buildToolRegistry as r, toPatch as s, createAnnotationCapability as t };
2679
3031
 
2680
- //# sourceMappingURL=capability-fghl0JmE.js.map
3032
+ //# sourceMappingURL=capability-DBsr2D-L.js.map