@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.
- package/dist/authoring-BhtyfsMx.d.cts +37 -0
- package/dist/authoring-R4ezdnXd.d.ts +37 -0
- package/dist/authoring.cjs +16 -0
- package/dist/authoring.d.cts +2 -0
- package/dist/authoring.d.ts +2 -0
- package/dist/authoring.js +3 -0
- package/dist/{capability-hb2AU0f6.cjs → capability-Csb9WMm-.cjs} +816 -482
- package/dist/capability-Csb9WMm-.cjs.map +1 -0
- package/dist/{capability-fghl0JmE.js → capability-DBsr2D-L.js} +772 -420
- package/dist/capability-DBsr2D-L.js.map +1 -0
- package/dist/contract/host.cjs +3 -0
- package/dist/contract/host.d.cts +2 -0
- package/dist/contract/host.d.ts +2 -0
- package/dist/contract/host.js +2 -0
- package/dist/contract.cjs +8 -0
- package/dist/contract.cjs.map +1 -0
- package/dist/contract.d.cts +10 -0
- package/dist/contract.d.ts +10 -0
- package/dist/contract.js +8 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.cjs +104 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -47
- package/dist/index.d.ts +5 -47
- package/dist/index.js +96 -96
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +4 -2
- package/dist/internal.d.cts +2 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +3 -1
- package/dist/seam-6pJeKOOJ.cjs +296 -0
- package/dist/seam-6pJeKOOJ.cjs.map +1 -0
- package/dist/seam-C4L_7rDt.js +225 -0
- package/dist/seam-C4L_7rDt.js.map +1 -0
- package/dist/types-CwytT-TI.js +13 -0
- package/dist/types-CwytT-TI.js.map +1 -0
- package/dist/types-DimcLIic.cjs +17 -0
- package/dist/types-DimcLIic.cjs.map +1 -0
- package/dist/{types-Da6JRmhw.d.ts → types-eLTpocXz.d.cts} +232 -25
- package/dist/{types-BhWgqgbX.d.cts → types-otwv-Cdl.d.ts} +232 -25
- package/package.json +21 -7
- package/dist/capability-fghl0JmE.js.map +0 -1
- package/dist/capability-hb2AU0f6.cjs.map +0 -1
|
@@ -1,236 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
/**
|
|
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
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
536
|
-
p2: contentToPdfPoint(q
|
|
537
|
-
p3: contentToPdfPoint(q
|
|
538
|
-
p4: contentToPdfPoint(q
|
|
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" ?
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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.
|
|
1489
|
+
if (!a || a.pon !== pon || a.subtype !== "link") continue;
|
|
1671
1490
|
if (!viewable(a.flags, false)) continue;
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
id
|
|
1680
|
-
rect,
|
|
1681
|
-
target
|
|
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: [
|
|
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
|
-
|
|
2096
|
-
|
|
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
|
|
2105
|
-
*
|
|
2106
|
-
* the
|
|
2107
|
-
*
|
|
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(
|
|
2118
|
-
const rects =
|
|
2119
|
-
const current =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
2278
|
-
}, () => {
|
|
2279
|
-
|
|
2280
|
-
|
|
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
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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)
|
|
2398
|
-
|
|
2399
|
-
|
|
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
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
if (
|
|
2592
|
-
|
|
2593
|
-
|
|
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
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
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")
|
|
2632
|
-
|
|
2633
|
-
|
|
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 {
|
|
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-
|
|
3032
|
+
//# sourceMappingURL=capability-DBsr2D-L.js.map
|