@embedpdf/plugin-annotation 3.0.0-next.13 → 3.0.0-next.15
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/README.md +129 -0
- package/dist/authoring.cjs +12 -2
- package/dist/authoring.d.cts +2 -2
- package/dist/authoring.d.ts +2 -2
- package/dist/authoring.js +2 -3
- package/dist/contract/host.cjs +18 -2
- package/dist/contract/host.d.cts +3 -2
- package/dist/contract/host.d.ts +3 -2
- package/dist/contract/host.js +3 -2
- package/dist/contract-BK4LHe9v.d.cts +1127 -0
- package/dist/contract-BOo9iZKo.d.ts +1127 -0
- package/dist/contract-CPzpRG3h.cjs +692 -0
- package/dist/contract-CPzpRG3h.cjs.map +1 -0
- package/dist/contract-b2e_LZvu.js +651 -0
- package/dist/contract-b2e_LZvu.js.map +1 -0
- package/dist/contract.cjs +11 -10
- package/dist/contract.d.cts +2 -3
- package/dist/contract.d.ts +2 -3
- package/dist/contract.js +2 -4
- package/dist/host-contract-BbjhS_Fo.d.ts +310 -0
- package/dist/host-contract-CxrQPcPj.js +52 -0
- package/dist/host-contract-CxrQPcPj.js.map +1 -0
- package/dist/{contract-a4eaQWga.cjs → host-contract-DY9jFPPF.cjs} +36 -16
- package/dist/host-contract-DY9jFPPF.cjs.map +1 -0
- package/dist/host-contract-Nzxwh0XS.d.cts +310 -0
- package/dist/index.cjs +4995 -414
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -17
- package/dist/index.d.ts +12 -17
- package/dist/index.js +4949 -388
- package/dist/index.js.map +1 -1
- package/package.json +11 -14
- package/dist/authoring-BhtyfsMx.d.cts +0 -37
- package/dist/authoring-R4ezdnXd.d.ts +0 -37
- package/dist/capability-B_dkoB1K.js +0 -3041
- package/dist/capability-B_dkoB1K.js.map +0 -1
- package/dist/capability-DVx1NmTe.cjs +0 -3064
- package/dist/capability-DVx1NmTe.cjs.map +0 -1
- package/dist/contract-CgAcfAE7.d.cts +0 -34
- package/dist/contract-CiG2_HBq.d.ts +0 -34
- package/dist/contract-DtXE1hjY.js +0 -32
- package/dist/contract-DtXE1hjY.js.map +0 -1
- package/dist/contract-a4eaQWga.cjs.map +0 -1
- package/dist/internal.cjs +0 -8
- package/dist/internal.d.cts +0 -14
- package/dist/internal.d.ts +0 -14
- package/dist/internal.js +0 -5
- package/dist/seam-6pJeKOOJ.cjs +0 -296
- package/dist/seam-6pJeKOOJ.cjs.map +0 -1
- package/dist/seam-C4L_7rDt.js +0 -225
- package/dist/seam-C4L_7rDt.js.map +0 -1
- package/dist/tools-0NI3b9z8.cjs +0 -536
- package/dist/tools-0NI3b9z8.cjs.map +0 -1
- package/dist/tools-DBEYx3-c.js +0 -513
- package/dist/tools-DBEYx3-c.js.map +0 -1
- package/dist/types-BvB1vUKb.js +0 -20
- package/dist/types-BvB1vUKb.js.map +0 -1
- package/dist/types-D5V0TIwk.d.ts +0 -1200
- package/dist/types-Dlhdw2k_.cjs +0 -31
- package/dist/types-Dlhdw2k_.cjs.map +0 -1
- package/dist/types-ObcPYc3-.d.cts +0 -1200
|
@@ -1,3064 +0,0 @@
|
|
|
1
|
-
const require_seam = require("./seam-6pJeKOOJ.cjs");
|
|
2
|
-
const require_tools = require("./tools-0NI3b9z8.cjs");
|
|
3
|
-
const require_types = require("./types-Dlhdw2k_.cjs");
|
|
4
|
-
let _embedpdf_core = require("@embedpdf/core");
|
|
5
|
-
let _embedpdf_plugin_actions_contract = require("@embedpdf/plugin-actions/contract");
|
|
6
|
-
let _embedpdf_plugin_interaction_contract = require("@embedpdf/plugin-interaction/contract");
|
|
7
|
-
let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
|
|
8
|
-
let _embedpdf_core_acrojs = require("@embedpdf/core-acrojs");
|
|
9
|
-
let _embedpdf_plugin_selection_contract = require("@embedpdf/plugin-selection/contract");
|
|
10
|
-
let _embedpdf_core_annotation = require("@embedpdf/core-annotation");
|
|
11
|
-
let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
12
|
-
//#region src/repository/props.ts
|
|
13
|
-
/**
|
|
14
|
-
* GENERIC per-key prop lowerings — the 1:1 mappings from the flat props
|
|
15
|
-
* vocabulary to wire fields that hold for every kind that declares the key.
|
|
16
|
-
* Kind modules override ONLY their exceptions (a coupling lives in its
|
|
17
|
-
* owner's file: cloudy `/RD` in shape.ts, visual-bounds `/Rect` in stroke.ts,
|
|
18
|
-
* the link target in link.ts).
|
|
19
|
-
*/
|
|
20
|
-
/** /BS slice of the style — a cloudy border keeps a solid underlying stroke
|
|
21
|
-
* (the scallops are the /BE effect, layered on by the shape kinds). */
|
|
22
|
-
const borderSlice = (style) => ({
|
|
23
|
-
borderStyle: style.border.kind === "dashed" ? "dashed" : "solid",
|
|
24
|
-
...style.border.kind === "dashed" ? { dashArray: style.border.dash } : {}
|
|
25
|
-
});
|
|
26
|
-
/** The `/DA`-styled text slice falls back to the draw-time seed exactly like
|
|
27
|
-
* the old projections did (a fresh draft may not carry `text` yet). */
|
|
28
|
-
const textOf = (a) => a.text ?? _embedpdf_core_annotation.initialTextStyle;
|
|
29
|
-
const GENERIC_PROPS = {
|
|
30
|
-
color: (a) => ({ color: require_seam.cssToColor(a.style.color) }),
|
|
31
|
-
opacity: (a) => ({ opacity: a.style.opacity }),
|
|
32
|
-
blendMode: (a) => ({ blendMode: a.style.blendMode }),
|
|
33
|
-
interiorColor: (a) => ({ interiorColor: a.style.interiorColor ? require_seam.cssToColor(a.style.interiorColor) : null }),
|
|
34
|
-
strokeWidth: (a) => ({ strokeWidth: a.style.strokeWidth }),
|
|
35
|
-
border: (a) => borderSlice(a.style),
|
|
36
|
-
fontFamily: (a) => ({ fontFamily: textOf(a).fontFamily }),
|
|
37
|
-
fontSize: (a) => ({ fontSize: textOf(a).fontSize }),
|
|
38
|
-
fontColor: (a) => ({ fontColor: require_seam.cssToColor(textOf(a).fontColor) }),
|
|
39
|
-
textAlign: (a) => ({ textAlign: textOf(a).textAlign }),
|
|
40
|
-
icon: (a) => a.icon !== void 0 ? { icon: a.icon } : {},
|
|
41
|
-
link: () => ({})
|
|
42
|
-
};
|
|
43
|
-
//#endregion
|
|
44
|
-
//#region src/repository/projection.ts
|
|
45
|
-
/** Box-kind geometry emission: the model's unrotated `rect` + applied tilt →
|
|
46
|
-
* `/Rect`(+`unrotatedRect`+`rotation`), total (nulls state the clears). */
|
|
47
|
-
const boxEmit = (a, crop) => {
|
|
48
|
-
const g = a.geom;
|
|
49
|
-
return require_seam.boxGeomFields(g.rect, (0, _embedpdf_core_annotation.geomRotation)(a.geom), crop);
|
|
50
|
-
};
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/repository/kinds/shape.ts
|
|
53
|
-
/** Cloudy `/BE` + `/RD` for a rect shape — TOTAL: the non-cloudy state is
|
|
54
|
-
* stated as `null` (tri-state remove), never omitted, so toggling cloudy OFF
|
|
55
|
-
* removes the stale inset (the Adobe phantom-padding bug). */
|
|
56
|
-
function cloudyExtras(a) {
|
|
57
|
-
if (a.geom.t !== "rect") return {};
|
|
58
|
-
if (a.style.border.kind === "cloudy") {
|
|
59
|
-
const inset = (0, _embedpdf_core_annotation.cloudyBorderExtent)(a.style.border.intensity, a.style.strokeWidth, a.geom.ellipse);
|
|
60
|
-
return {
|
|
61
|
-
cloudyIntensity: a.style.border.intensity,
|
|
62
|
-
rectDifferences: {
|
|
63
|
-
left: inset,
|
|
64
|
-
top: inset,
|
|
65
|
-
right: inset,
|
|
66
|
-
bottom: inset
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
cloudyIntensity: null,
|
|
72
|
-
rectDifferences: null
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const ingest = (dto, crop, ellipse) => {
|
|
76
|
-
const d = dto;
|
|
77
|
-
return { geom: require_seam.boxGeomFromDTO(d, d.rotation, d.unrotatedRect, crop, ellipse) };
|
|
78
|
-
};
|
|
79
|
-
const projection = (ellipse) => ({
|
|
80
|
-
ingest: (dto, crop) => ingest(dto, crop, ellipse),
|
|
81
|
-
geometry: (a, crop) => boxEmit(a, crop),
|
|
82
|
-
prop: {
|
|
83
|
-
strokeWidth: (a) => ({
|
|
84
|
-
strokeWidth: a.style.strokeWidth,
|
|
85
|
-
...cloudyExtras(a)
|
|
86
|
-
}),
|
|
87
|
-
border: (a) => ({
|
|
88
|
-
...borderSlice(a.style),
|
|
89
|
-
...cloudyExtras(a)
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
const square = projection(false);
|
|
94
|
-
const circle = projection(true);
|
|
95
|
-
//#endregion
|
|
96
|
-
//#region src/repository/kinds/stroke.ts
|
|
97
|
-
/** Advisory rotation, TOTAL: rotation 0 states `null` (tri-state clear) —
|
|
98
|
-
* omission would preserve a stale advisory angle. */
|
|
99
|
-
const advisoryRotation = (g) => {
|
|
100
|
-
const rot = (0, _embedpdf_core_annotation.geomRotation)(g);
|
|
101
|
-
return { rotation: rot ? require_seam.toPdfRotation(rot) : null };
|
|
102
|
-
};
|
|
103
|
-
/** `/BE` intensity for a closed poly (polygon): the curls are generated from
|
|
104
|
-
* /Vertices + /BE alone — per ISO 32000 no /RD applies — and the /Rect
|
|
105
|
-
* (geomPdfBounds WITH the border) already includes the outward cloud extent. */
|
|
106
|
-
const polyCloudy = (a) => a.geom.t === "poly" && a.geom.closed ? { cloudyIntensity: a.style.border.kind === "cloudy" ? a.style.border.intensity : null } : {};
|
|
107
|
-
/** The visual-bounds `/Rect` (stroke + border included) of a stroke geom. */
|
|
108
|
-
const visualRect = (a, crop) => {
|
|
109
|
-
const g = a.geom;
|
|
110
|
-
if (g.t === "line" || g.t === "ink") return { rect: (0, _embedpdf_core_annotation.geomPdfBounds)(g, a.style.strokeWidth, crop) };
|
|
111
|
-
if (g.t === "poly") return { rect: (0, _embedpdf_core_annotation.geomPdfBounds)(g, a.style.strokeWidth, crop, a.style.border) };
|
|
112
|
-
return {};
|
|
113
|
-
};
|
|
114
|
-
/**
|
|
115
|
-
* A lowering whose key is an INPUT of the derived /Rect: the visual bounds
|
|
116
|
-
* ride along with every emission, so a sparse patch can never change an input
|
|
117
|
-
* without re-emitting the derivation. (The bug this kills: patch `lineEndings`
|
|
118
|
-
* alone → the engine re-bakes the /AP inside the stale /Rect → the new
|
|
119
|
-
* arrowhead is clipped in every viewer except the live vector one.)
|
|
120
|
-
*/
|
|
121
|
-
const withRect = (lower) => (a, crop) => ({
|
|
122
|
-
...lower(a, crop),
|
|
123
|
-
...visualRect(a, crop)
|
|
124
|
-
});
|
|
125
|
-
/** Stroke-family prop exceptions: width, border, AND line endings feed the
|
|
126
|
-
* derived /Rect — an ending's arrowhead reaches well past the endpoint, and
|
|
127
|
-
* ISO 32000 requires /Rect to enclose it. Where a key can't change the
|
|
128
|
-
* bounds (a line's border restyle), the re-emitted rect is an idempotent
|
|
129
|
-
* no-op — uniformity beats per-case reasoning here. */
|
|
130
|
-
const strokeProps = {
|
|
131
|
-
strokeWidth: withRect((a) => ({ strokeWidth: a.style.strokeWidth })),
|
|
132
|
-
border: withRect((a) => ({
|
|
133
|
-
...borderSlice(a.style),
|
|
134
|
-
...polyCloudy(a)
|
|
135
|
-
})),
|
|
136
|
-
lineEndings: withRect((a) => (a.geom.t === "line" || a.geom.t === "poly") && a.geom.ends ? { lineEndings: a.geom.ends } : {})
|
|
137
|
-
};
|
|
138
|
-
const line = {
|
|
139
|
-
ingest: (dto, crop) => {
|
|
140
|
-
const d = dto;
|
|
141
|
-
return { geom: {
|
|
142
|
-
t: "line",
|
|
143
|
-
a: (0, _embedpdf_core_annotation.pdfToContentPoint)(d.linePoints.start, crop),
|
|
144
|
-
b: (0, _embedpdf_core_annotation.pdfToContentPoint)(d.linePoints.end, crop),
|
|
145
|
-
ends: d.lineEndings,
|
|
146
|
-
...require_seam.rotFromDTO(d.rotation)
|
|
147
|
-
} };
|
|
148
|
-
},
|
|
149
|
-
geometry: (a, crop) => {
|
|
150
|
-
const g = a.geom;
|
|
151
|
-
if (g.t !== "line") return null;
|
|
152
|
-
return {
|
|
153
|
-
linePoints: {
|
|
154
|
-
start: (0, _embedpdf_core_annotation.contentToPdfPoint)(g.a, crop),
|
|
155
|
-
end: (0, _embedpdf_core_annotation.contentToPdfPoint)(g.b, crop)
|
|
156
|
-
},
|
|
157
|
-
...visualRect(a, crop),
|
|
158
|
-
...advisoryRotation(g)
|
|
159
|
-
};
|
|
160
|
-
},
|
|
161
|
-
prop: strokeProps
|
|
162
|
-
};
|
|
163
|
-
const polyProjection = (closed) => ({
|
|
164
|
-
ingest: (dto, crop) => {
|
|
165
|
-
const d = dto;
|
|
166
|
-
return { geom: {
|
|
167
|
-
t: "poly",
|
|
168
|
-
points: d.vertices.map((p) => (0, _embedpdf_core_annotation.pdfToContentPoint)(p, crop)),
|
|
169
|
-
closed,
|
|
170
|
-
..."lineEndings" in d ? { ends: d.lineEndings } : {},
|
|
171
|
-
...require_seam.rotFromDTO(d.rotation)
|
|
172
|
-
} };
|
|
173
|
-
},
|
|
174
|
-
geometry: (a, crop) => {
|
|
175
|
-
const g = a.geom;
|
|
176
|
-
if (g.t !== "poly") return null;
|
|
177
|
-
return {
|
|
178
|
-
vertices: g.points.map((p) => (0, _embedpdf_core_annotation.contentToPdfPoint)(p, crop)),
|
|
179
|
-
...visualRect(a, crop),
|
|
180
|
-
...advisoryRotation(g)
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
prop: strokeProps
|
|
184
|
-
});
|
|
185
|
-
const polygon = polyProjection(true);
|
|
186
|
-
const polyline = polyProjection(false);
|
|
187
|
-
const ink = {
|
|
188
|
-
ingest: (dto, crop) => {
|
|
189
|
-
const d = dto;
|
|
190
|
-
return {
|
|
191
|
-
geom: {
|
|
192
|
-
t: "ink",
|
|
193
|
-
strokes: d.inkList.map((stroke) => stroke.map((p) => (0, _embedpdf_core_annotation.pdfToContentPoint)(p, crop))),
|
|
194
|
-
...require_seam.rotFromDTO(d.rotation)
|
|
195
|
-
},
|
|
196
|
-
...d.intent ? { intent: d.intent } : {}
|
|
197
|
-
};
|
|
198
|
-
},
|
|
199
|
-
geometry: (a, crop) => {
|
|
200
|
-
const g = a.geom;
|
|
201
|
-
if (g.t !== "ink") return null;
|
|
202
|
-
return {
|
|
203
|
-
inkList: g.strokes.map((stroke) => stroke.map((p) => (0, _embedpdf_core_annotation.contentToPdfPoint)(p, crop))),
|
|
204
|
-
...visualRect(a, crop),
|
|
205
|
-
...advisoryRotation(g)
|
|
206
|
-
};
|
|
207
|
-
},
|
|
208
|
-
prop: strokeProps,
|
|
209
|
-
draftExtras: (a) => a.intent === "ink-highlight" ? { intent: a.intent } : {}
|
|
210
|
-
};
|
|
211
|
-
//#endregion
|
|
212
|
-
//#region src/rich-text.ts
|
|
213
|
-
/**
|
|
214
|
-
* Rich text POLICY — what the annotation plugin decides about a free-text
|
|
215
|
-
* annotation's rich document, kept pure so every framework's editor glue
|
|
216
|
-
* behaves identically (the mechanics live in `@embedpdf/web`'s binding, the
|
|
217
|
-
* run algebra in the core):
|
|
218
|
-
*
|
|
219
|
-
* • the rich document of any annot (the DTO's, or one synthesised from the
|
|
220
|
-
* plain text + the `/DA` text style for a draft that has no DTO yet)
|
|
221
|
-
* • a flat props patch → the run delta it means for a text RANGE (font,
|
|
222
|
-
* size, colour, bold/italic/underline) and the keys left for the body
|
|
223
|
-
* • what a range reads back for those keys (agree → the value, else mixed)
|
|
224
|
-
* • faces: a DTO font (standard kebab name / registered key) ↔ the face a
|
|
225
|
-
* run names (family + weight + italic) ↔ a CSS family list
|
|
226
|
-
* • the commit rule: a document nothing overrides on a plain annotation
|
|
227
|
-
* commits as `contents`; anything else as `richText`
|
|
228
|
-
*/
|
|
229
|
-
/** The props keys a text range takes as run deltas. */
|
|
230
|
-
const RANGE_KEYS = [
|
|
231
|
-
"fontFamily",
|
|
232
|
-
"fontSize",
|
|
233
|
-
"fontColor",
|
|
234
|
-
"bold",
|
|
235
|
-
"italic",
|
|
236
|
-
"underline"
|
|
237
|
-
];
|
|
238
|
-
/** The engine's standard-14 vocabulary (the `StandardFont` kebab names). */
|
|
239
|
-
const STANDARD_FACES = {
|
|
240
|
-
courier: {
|
|
241
|
-
family: "Courier",
|
|
242
|
-
weight: 400,
|
|
243
|
-
italic: false
|
|
244
|
-
},
|
|
245
|
-
"courier-bold": {
|
|
246
|
-
family: "Courier",
|
|
247
|
-
weight: 700,
|
|
248
|
-
italic: false
|
|
249
|
-
},
|
|
250
|
-
"courier-bold-oblique": {
|
|
251
|
-
family: "Courier",
|
|
252
|
-
weight: 700,
|
|
253
|
-
italic: true
|
|
254
|
-
},
|
|
255
|
-
"courier-oblique": {
|
|
256
|
-
family: "Courier",
|
|
257
|
-
weight: 400,
|
|
258
|
-
italic: true
|
|
259
|
-
},
|
|
260
|
-
helvetica: {
|
|
261
|
-
family: "Helvetica",
|
|
262
|
-
weight: 400,
|
|
263
|
-
italic: false
|
|
264
|
-
},
|
|
265
|
-
"helvetica-bold": {
|
|
266
|
-
family: "Helvetica",
|
|
267
|
-
weight: 700,
|
|
268
|
-
italic: false
|
|
269
|
-
},
|
|
270
|
-
"helvetica-bold-oblique": {
|
|
271
|
-
family: "Helvetica",
|
|
272
|
-
weight: 700,
|
|
273
|
-
italic: true
|
|
274
|
-
},
|
|
275
|
-
"helvetica-oblique": {
|
|
276
|
-
family: "Helvetica",
|
|
277
|
-
weight: 400,
|
|
278
|
-
italic: true
|
|
279
|
-
},
|
|
280
|
-
"times-roman": {
|
|
281
|
-
family: "Times",
|
|
282
|
-
weight: 400,
|
|
283
|
-
italic: false
|
|
284
|
-
},
|
|
285
|
-
"times-bold": {
|
|
286
|
-
family: "Times",
|
|
287
|
-
weight: 700,
|
|
288
|
-
italic: false
|
|
289
|
-
},
|
|
290
|
-
"times-bold-italic": {
|
|
291
|
-
family: "Times",
|
|
292
|
-
weight: 700,
|
|
293
|
-
italic: true
|
|
294
|
-
},
|
|
295
|
-
"times-italic": {
|
|
296
|
-
family: "Times",
|
|
297
|
-
weight: 400,
|
|
298
|
-
italic: true
|
|
299
|
-
},
|
|
300
|
-
symbol: {
|
|
301
|
-
family: "Symbol",
|
|
302
|
-
weight: 400,
|
|
303
|
-
italic: false
|
|
304
|
-
},
|
|
305
|
-
"zapf-dingbats": {
|
|
306
|
-
family: "ZapfDingbats",
|
|
307
|
-
weight: 400,
|
|
308
|
-
italic: false
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
/** Family names compare without case, spaces, hyphens, underscores and
|
|
312
|
-
* quotes — the engine's own rule. */
|
|
313
|
-
function familyKey(family) {
|
|
314
|
-
return family.toLowerCase().replace(/[\s\-_'"]/g, "");
|
|
315
|
-
}
|
|
316
|
-
const STANDARD_FAMILY_KEYS = {
|
|
317
|
-
helvetica: "Helvetica",
|
|
318
|
-
arial: "Helvetica",
|
|
319
|
-
arialmt: "Helvetica",
|
|
320
|
-
sansserif: "Helvetica",
|
|
321
|
-
times: "Times",
|
|
322
|
-
timesroman: "Times",
|
|
323
|
-
timesnewroman: "Times",
|
|
324
|
-
timesnewromanpsmt: "Times",
|
|
325
|
-
serif: "Times",
|
|
326
|
-
courier: "Courier",
|
|
327
|
-
couriernew: "Courier",
|
|
328
|
-
couriernewpsmt: "Courier",
|
|
329
|
-
monospace: "Courier",
|
|
330
|
-
symbol: "Symbol",
|
|
331
|
-
zapfdingbats: "ZapfDingbats",
|
|
332
|
-
dingbats: "ZapfDingbats"
|
|
333
|
-
};
|
|
334
|
-
/** Web stacks with the standard families' metrics (what the DOM editor and
|
|
335
|
-
* the vector renderer show for text the engine sets in a standard font). */
|
|
336
|
-
const STANDARD_STACKS = {
|
|
337
|
-
Helvetica: "Helvetica, Arial, sans-serif",
|
|
338
|
-
Times: "\"Times New Roman\", Times, serif",
|
|
339
|
-
Courier: "\"Courier New\", Courier, monospace",
|
|
340
|
-
Symbol: "serif",
|
|
341
|
-
ZapfDingbats: "serif"
|
|
342
|
-
};
|
|
343
|
-
/** The face a DTO font names: a standard font's family/weight/italic, a
|
|
344
|
-
* registered key's identity, else the string itself as a family. */
|
|
345
|
-
function faceForFont(font, fonts) {
|
|
346
|
-
const standard = STANDARD_FACES[font];
|
|
347
|
-
if (standard) return { ...standard };
|
|
348
|
-
const registered = fonts?.().find((f) => f.key === font);
|
|
349
|
-
if (registered) return {
|
|
350
|
-
family: registered.familyName,
|
|
351
|
-
weight: registered.weight,
|
|
352
|
-
italic: registered.italic
|
|
353
|
-
};
|
|
354
|
-
return { family: font };
|
|
355
|
-
}
|
|
356
|
-
/** The DTO font for a face: the registered key whose identity matches (the
|
|
357
|
-
* closest weight, italic first), else the standard kebab name, else the
|
|
358
|
-
* family itself. The inverse of {@link faceForFont}. */
|
|
359
|
-
function fontForFace(face, fonts) {
|
|
360
|
-
const wanted = familyKey(face.family);
|
|
361
|
-
const weight = face.weight ?? 400;
|
|
362
|
-
const italic = face.italic ?? false;
|
|
363
|
-
let best;
|
|
364
|
-
for (const f of fonts?.() ?? []) {
|
|
365
|
-
if (familyKey(f.familyName) !== wanted) continue;
|
|
366
|
-
const score = Math.abs(f.weight - weight) + (f.italic === italic ? 0 : 1e3);
|
|
367
|
-
if (!best || score < best.score) best = {
|
|
368
|
-
key: f.key,
|
|
369
|
-
score
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
if (best) return best.key;
|
|
373
|
-
const standard = STANDARD_FAMILY_KEYS[wanted];
|
|
374
|
-
if (standard) {
|
|
375
|
-
const bold = weight >= 600;
|
|
376
|
-
for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard && spec.weight >= 600 === bold && spec.italic === italic) return name;
|
|
377
|
-
for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard) return name;
|
|
378
|
-
}
|
|
379
|
-
return face.family;
|
|
380
|
-
}
|
|
381
|
-
/** The CSS family list for a face family: a standard family's web stack, a
|
|
382
|
-
* registered family's `@font-face` name (its key — see `mountWebFont`),
|
|
383
|
-
* else the family itself with a generic fallback. */
|
|
384
|
-
function cssFontFamilyForFace(family, fonts) {
|
|
385
|
-
const standard = STANDARD_FAMILY_KEYS[familyKey(family)];
|
|
386
|
-
if (standard) return STANDARD_STACKS[standard];
|
|
387
|
-
const wanted = familyKey(family);
|
|
388
|
-
const registered = fonts?.().find((f) => familyKey(f.familyName) === wanted);
|
|
389
|
-
if (registered) return `"${registered.key}"`;
|
|
390
|
-
return `"${family}", sans-serif`;
|
|
391
|
-
}
|
|
392
|
-
/** The CSS family list for a DTO font (a standard kebab name or a key). */
|
|
393
|
-
function cssFontFamilyForFont(font, fonts) {
|
|
394
|
-
const standard = STANDARD_FACES[font];
|
|
395
|
-
if (standard) return STANDARD_STACKS[standard.family];
|
|
396
|
-
return `"${font}", sans-serif`;
|
|
397
|
-
}
|
|
398
|
-
const hex = (css) => css.trim().toUpperCase();
|
|
399
|
-
/** The rich body the `/DA` text style describes (a draft's body before its
|
|
400
|
-
* DTO exists; also the fallback for a DTO without `richText`). */
|
|
401
|
-
function bodyFromTextStyle(t, fonts) {
|
|
402
|
-
const face = faceForFont(t.fontFamily, fonts);
|
|
403
|
-
return {
|
|
404
|
-
family: face.family,
|
|
405
|
-
weight: t.bold ? 700 : face.weight ?? 400,
|
|
406
|
-
italic: t.italic ?? face.italic ?? false,
|
|
407
|
-
size: t.fontSize,
|
|
408
|
-
color: hex(t.fontColor),
|
|
409
|
-
decoration: t.underline ? ["underline"] : [],
|
|
410
|
-
script: "normal",
|
|
411
|
-
letterSpacing: 0,
|
|
412
|
-
horizontalScale: 1,
|
|
413
|
-
align: t.textAlign,
|
|
414
|
-
dir: "ltr"
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
/** The annotation's rich document: the DTO's, else one synthesised from
|
|
418
|
-
* its plain text and text style (a draft the engine has not echoed yet). */
|
|
419
|
-
function richDocOf(a, fonts) {
|
|
420
|
-
if (a.data?.subtype === "free-text" && a.data.richText) return a.data.richText;
|
|
421
|
-
return {
|
|
422
|
-
body: bodyFromTextStyle(a.text ?? {
|
|
423
|
-
fontFamily: "helvetica",
|
|
424
|
-
fontSize: 12,
|
|
425
|
-
fontColor: "#000000",
|
|
426
|
-
textAlign: "left"
|
|
427
|
-
}, fonts),
|
|
428
|
-
paragraphs: (0, _embedpdf_core_annotation.paragraphsFromPlainText)(a.data?.contents ?? "")
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* The write a text edit commits: the rich paragraphs, with paragraph
|
|
433
|
-
* properties equal to the body's stripped (they are inherited, not
|
|
434
|
-
* overrides) and the body omitted — it stays what the props path last
|
|
435
|
-
* wrote. One engine, one path: plain and formatted text alike.
|
|
436
|
-
*/
|
|
437
|
-
function textCommitPatch(a, paragraphs, fonts) {
|
|
438
|
-
return { richText: { paragraphs: stripBodyDefaults(paragraphs, richDocOf(a, fonts).body) } };
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Paragraph alignment/direction equal to the body's are not overrides: the
|
|
442
|
-
* engine echoes them resolved, the editor renders them as inline styles and
|
|
443
|
-
* serialises them back, and the commit rule must see through that (a plain
|
|
444
|
-
* `/Contents` annotation must not grow an `/RC` from typing).
|
|
445
|
-
*/
|
|
446
|
-
function stripBodyDefaults(paragraphs, body) {
|
|
447
|
-
return paragraphs.map((p) => {
|
|
448
|
-
if ((p.align === void 0 || p.align !== body.align) && (p.dir === void 0 || p.dir !== body.dir)) return p;
|
|
449
|
-
const { align, dir, ...rest } = p;
|
|
450
|
-
return {
|
|
451
|
-
...rest,
|
|
452
|
-
...align !== void 0 && align !== body.align ? { align } : {},
|
|
453
|
-
...dir !== void 0 && dir !== body.dir ? { dir } : {}
|
|
454
|
-
};
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Split a props patch for an annotation whose editor holds a text RANGE:
|
|
459
|
-
* the run delta the range takes (font → face, size, colour, bold → weight,
|
|
460
|
-
* italic, underline → decoration) and the keys that still go to the body.
|
|
461
|
-
*/
|
|
462
|
-
function runDeltaForProps(patch, fonts) {
|
|
463
|
-
const delta = {};
|
|
464
|
-
const rest = {};
|
|
465
|
-
for (const key of Object.keys(patch)) {
|
|
466
|
-
const value = patch[key];
|
|
467
|
-
if (value === void 0) continue;
|
|
468
|
-
switch (key) {
|
|
469
|
-
case "fontFamily": {
|
|
470
|
-
const face = faceForFont(patch.fontFamily, fonts);
|
|
471
|
-
delta.family = face.family;
|
|
472
|
-
if (face.weight !== void 0) delta.weight = face.weight;
|
|
473
|
-
if (face.italic !== void 0) delta.italic = face.italic;
|
|
474
|
-
break;
|
|
475
|
-
}
|
|
476
|
-
case "fontSize":
|
|
477
|
-
delta.size = patch.fontSize;
|
|
478
|
-
break;
|
|
479
|
-
case "fontColor":
|
|
480
|
-
delta.color = hex(patch.fontColor);
|
|
481
|
-
break;
|
|
482
|
-
case "bold":
|
|
483
|
-
delta.weight = patch.bold ? 700 : 400;
|
|
484
|
-
break;
|
|
485
|
-
case "italic":
|
|
486
|
-
delta.italic = patch.italic;
|
|
487
|
-
break;
|
|
488
|
-
case "underline":
|
|
489
|
-
delta.decoration = patch.underline ? ["underline"] : [];
|
|
490
|
-
break;
|
|
491
|
-
default: rest[key] = value;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
495
|
-
delta,
|
|
496
|
-
rest
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
/** The style deltas of every non-empty run a range overlaps. */
|
|
500
|
-
function runsInRange(doc, range) {
|
|
501
|
-
const start = Math.min(range.start, range.end);
|
|
502
|
-
const end = Math.max(range.start, range.end);
|
|
503
|
-
if (end <= start) return [];
|
|
504
|
-
const from = (0, _embedpdf_core_annotation.locateOffset)(doc, start);
|
|
505
|
-
const to = (0, _embedpdf_core_annotation.locateOffset)(doc, end);
|
|
506
|
-
const out = [];
|
|
507
|
-
for (let index = from.paragraph; index <= to.paragraph; index++) {
|
|
508
|
-
const paragraph = doc.paragraphs[index];
|
|
509
|
-
const localStart = index === from.paragraph ? from.offset : 0;
|
|
510
|
-
const localEnd = index === to.paragraph ? to.offset : paragraph.runs.reduce((n, r) => n + r.text.length, 0);
|
|
511
|
-
let pos = 0;
|
|
512
|
-
for (const run of paragraph.runs) {
|
|
513
|
-
const runEnd = pos + run.text.length;
|
|
514
|
-
if (runEnd > localStart && pos < localEnd && run.text.length > 0) out.push(run.style ?? {});
|
|
515
|
-
pos = runEnd;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return out;
|
|
519
|
-
}
|
|
520
|
-
/** The values a range reads back for the range keys — resolved against the
|
|
521
|
-
* body, one value where every run agrees, `mixed` where they don't. */
|
|
522
|
-
function rangeProps(doc, range, fonts) {
|
|
523
|
-
const runs = runsInRange(doc, range);
|
|
524
|
-
const body = doc.body;
|
|
525
|
-
const resolve = (d) => ({
|
|
526
|
-
family: d.family ?? body.family,
|
|
527
|
-
weight: d.weight ?? body.weight,
|
|
528
|
-
italic: d.italic ?? body.italic,
|
|
529
|
-
size: d.size ?? body.size,
|
|
530
|
-
color: d.color ?? body.color,
|
|
531
|
-
decoration: d.decoration ?? body.decoration
|
|
532
|
-
});
|
|
533
|
-
const styles = (runs.length ? runs : [{}]).map(resolve);
|
|
534
|
-
const values = {};
|
|
535
|
-
const mixed = [];
|
|
536
|
-
const read = (key, of) => {
|
|
537
|
-
const first = of(styles[0]);
|
|
538
|
-
values[key] = first;
|
|
539
|
-
if (styles.some((s) => JSON.stringify(of(s)) !== JSON.stringify(first))) mixed.push(key);
|
|
540
|
-
};
|
|
541
|
-
read("fontFamily", (s) => fontForFace({
|
|
542
|
-
family: s.family,
|
|
543
|
-
weight: body.weight,
|
|
544
|
-
italic: body.italic
|
|
545
|
-
}, fonts));
|
|
546
|
-
read("fontSize", (s) => s.size);
|
|
547
|
-
read("fontColor", (s) => s.color.toLowerCase());
|
|
548
|
-
read("bold", (s) => (s.weight ?? 400) >= 600);
|
|
549
|
-
read("italic", (s) => !!s.italic);
|
|
550
|
-
read("underline", (s) => (s.decoration ?? []).includes("underline"));
|
|
551
|
-
return {
|
|
552
|
-
values,
|
|
553
|
-
mixed
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
//#endregion
|
|
557
|
-
//#region src/repository/kinds/freeText.ts
|
|
558
|
-
/** Free-text `/DA` fields → content {@link TextStyle}. An absent `fontColor`
|
|
559
|
-
* falls back to the `/DA` colour — the same rule the CPVT renderer applies. */
|
|
560
|
-
function textFromDTO(dto) {
|
|
561
|
-
const body = dto.richText?.body;
|
|
562
|
-
return {
|
|
563
|
-
fontFamily: dto.fontFamily,
|
|
564
|
-
fontSize: dto.fontSize,
|
|
565
|
-
fontColor: require_seam.colorToCss(dto.fontColor ?? dto.color),
|
|
566
|
-
textAlign: dto.textAlign,
|
|
567
|
-
...body && body.weight >= 600 ? { bold: true } : {},
|
|
568
|
-
...body?.italic ? { italic: true } : {},
|
|
569
|
-
...body?.decoration.includes("underline") ? { underline: true } : {}
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* A formatting toggle is a BODY change (runs are deltas over it), so it
|
|
574
|
-
* lowers as a rich write carrying the current paragraphs — and every toggle
|
|
575
|
-
* lowers the same COMPLETE body (the current one with the three toggles
|
|
576
|
-
* applied: a partial body means engine defaults, which would reset the
|
|
577
|
-
* size, face and colour), so a patch of several merges cleanly.
|
|
578
|
-
*/
|
|
579
|
-
const formattingBody = (a) => {
|
|
580
|
-
const doc = richDocOf(a);
|
|
581
|
-
const t = a.text;
|
|
582
|
-
return { richText: {
|
|
583
|
-
body: {
|
|
584
|
-
...doc.body,
|
|
585
|
-
weight: t?.bold ? 700 : 400,
|
|
586
|
-
italic: !!t?.italic,
|
|
587
|
-
decoration: t?.underline ? ["underline"] : []
|
|
588
|
-
},
|
|
589
|
-
paragraphs: doc.paragraphs
|
|
590
|
-
} };
|
|
591
|
-
};
|
|
592
|
-
/**
|
|
593
|
-
* The engine geometry for a callout: the overall `/Rect` (text box ∪ leader ∪
|
|
594
|
-
* arrow), the `/RD` inset that recovers the text box from it, the `/CL` leader
|
|
595
|
-
* (`[tip, knee, conn]` with the connection point derived), and the `/LE`
|
|
596
|
-
* ending. All in PDF user space (y-up), with every `/RD` inset clamped
|
|
597
|
-
* non-negative.
|
|
598
|
-
*
|
|
599
|
-
* A tilted box (the upright policy) additionally emits `rotation` +
|
|
600
|
-
* `unrotatedRect` (the logical text box) — the engine bakes the box + text
|
|
601
|
-
* under an INLINE rotation about the box centre while the leader stays
|
|
602
|
-
* page-space, so (unlike plain boxes) the /AP form `/Matrix` stays identity
|
|
603
|
-
* and the raster stays placed by `/Rect`. `/RD` then insets to the ROTATED
|
|
604
|
-
* box's AABB — the best axis-aligned text box a foreign viewer regenerating
|
|
605
|
-
* the AP can draw (spec-conformant degradation). The transform pair is TOTAL
|
|
606
|
-
* (nulls state the clears) like every box emission.
|
|
607
|
-
*/
|
|
608
|
-
function calloutFields(a, crop) {
|
|
609
|
-
const g = a.geom;
|
|
610
|
-
if (g.t !== "text" || !g.callout) return null;
|
|
611
|
-
const rot = (0, _embedpdf_core_annotation.geomRotation)(g);
|
|
612
|
-
const overall = (0, _embedpdf_core_annotation.geomPdfBounds)(g, a.style.strokeWidth, crop);
|
|
613
|
-
const tb = (0, _embedpdf_core_annotation.contentToPdfRect)(rot ? (0, _embedpdf_core_annotation.rotatedAabb)(g.rect, rot) : g.rect, crop);
|
|
614
|
-
const nn = (n) => Math.max(0, n);
|
|
615
|
-
const pts = (0, _embedpdf_core_annotation.calloutLinePoints)(g).map((p) => (0, _embedpdf_core_annotation.contentToPdfPoint)(p, crop));
|
|
616
|
-
const calloutLine = pts.length === 3 ? [
|
|
617
|
-
pts[0],
|
|
618
|
-
pts[1],
|
|
619
|
-
pts[2]
|
|
620
|
-
] : [pts[0], pts[1]];
|
|
621
|
-
return {
|
|
622
|
-
rect: overall,
|
|
623
|
-
rectDifferences: {
|
|
624
|
-
left: nn(tb.left - overall.left),
|
|
625
|
-
bottom: nn(tb.bottom - overall.bottom),
|
|
626
|
-
right: nn(overall.right - tb.right),
|
|
627
|
-
top: nn(overall.top - tb.top)
|
|
628
|
-
},
|
|
629
|
-
calloutLine,
|
|
630
|
-
lineEnding: g.callout.ending,
|
|
631
|
-
...rot ? {
|
|
632
|
-
rotation: require_seam.toPdfRotation(rot),
|
|
633
|
-
unrotatedRect: (0, _embedpdf_core_annotation.contentToPdfRect)(g.rect, crop)
|
|
634
|
-
} : {
|
|
635
|
-
rotation: null,
|
|
636
|
-
unrotatedRect: null
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
const freeText = {
|
|
641
|
-
ingest: (dto, crop) => {
|
|
642
|
-
const d = dto;
|
|
643
|
-
if (d.intent === "free-text-callout" && d.calloutLine && d.calloutLine.length >= 2) {
|
|
644
|
-
const cl = d.calloutLine;
|
|
645
|
-
const rot = d.rotation ? require_seam.fromPdfRotation(d.rotation) : 0;
|
|
646
|
-
return {
|
|
647
|
-
geom: {
|
|
648
|
-
t: "text",
|
|
649
|
-
rect: rot && d.unrotatedRect ? (0, _embedpdf_core_annotation.pdfToContentRect)(d.unrotatedRect, crop) : (0, _embedpdf_core_annotation.pdfToContentRect)(require_seam.insetPdfRectByRD(d.rect, d.rectDifferences), crop),
|
|
650
|
-
callout: {
|
|
651
|
-
tip: (0, _embedpdf_core_annotation.pdfToContentPoint)(cl[0], crop),
|
|
652
|
-
knee: cl.length === 3 ? (0, _embedpdf_core_annotation.pdfToContentPoint)(cl[1], crop) : void 0,
|
|
653
|
-
ending: d.lineEnding ?? "none"
|
|
654
|
-
},
|
|
655
|
-
...rot && d.unrotatedRect ? { rot } : {}
|
|
656
|
-
},
|
|
657
|
-
text: textFromDTO(d)
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
const rot = d.rotation ? require_seam.fromPdfRotation(d.rotation) : 0;
|
|
661
|
-
return {
|
|
662
|
-
geom: {
|
|
663
|
-
t: "text",
|
|
664
|
-
rect: (0, _embedpdf_core_annotation.pdfToContentRect)(rot && d.unrotatedRect ? d.unrotatedRect : d.rect, crop),
|
|
665
|
-
...rot ? { rot } : {}
|
|
666
|
-
},
|
|
667
|
-
text: textFromDTO(d)
|
|
668
|
-
};
|
|
669
|
-
},
|
|
670
|
-
geometry: (a, crop) => {
|
|
671
|
-
if (a.geom.t !== "text") return null;
|
|
672
|
-
const cf = calloutFields(a, crop);
|
|
673
|
-
if (cf) return { ...cf };
|
|
674
|
-
return boxEmit(a, crop);
|
|
675
|
-
},
|
|
676
|
-
prop: {
|
|
677
|
-
bold: formattingBody,
|
|
678
|
-
italic: formattingBody,
|
|
679
|
-
underline: formattingBody
|
|
680
|
-
},
|
|
681
|
-
draftExtras: (a, crop) => ({
|
|
682
|
-
intent: calloutFields(a, crop) ? "free-text-callout" : "free-text",
|
|
683
|
-
contents: a.data?.contents ?? ""
|
|
684
|
-
})
|
|
685
|
-
};
|
|
686
|
-
//#endregion
|
|
687
|
-
//#region src/repository/kinds/quads.ts
|
|
688
|
-
/**
|
|
689
|
-
* Imported `/QuadPoints` → semantic TextQuads. `normalizeQuad` is a
|
|
690
|
-
* NORMALIZER, not a cast: the de-facto zigzag order passes through, ring-order
|
|
691
|
-
* producers are repaired, and garbage gets a deterministic labeling — so
|
|
692
|
-
* drawing code can never put an underline on the wrong edge of a well-formed
|
|
693
|
-
* quad, rotated ones included.
|
|
694
|
-
*/
|
|
695
|
-
const quadsFromDTO = (quadPoints, crop) => quadPoints.map((q) => (0, _embedpdf_core_geometry.normalizeQuad)({
|
|
696
|
-
p1: (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p1, crop),
|
|
697
|
-
p2: (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p2, crop),
|
|
698
|
-
p3: (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p3, crop),
|
|
699
|
-
p4: (0, _embedpdf_core_annotation.pdfToContentPoint)(q.p4, crop)
|
|
700
|
-
}));
|
|
701
|
-
/** Content quads → engine `/QuadPoints` (PDF user space, zigzag slot order:
|
|
702
|
-
* p1..p4 = upper-start, upper-end, lower-start, lower-end — under the y-flip
|
|
703
|
-
* exactly PDFium's documented TL, TR, BL, BR); null off quads geom. */
|
|
704
|
-
function quadPointsFor(a, crop) {
|
|
705
|
-
if (a.geom.t !== "quads") return null;
|
|
706
|
-
return a.geom.quads.map((q) => ({
|
|
707
|
-
p1: (0, _embedpdf_core_annotation.contentToPdfPoint)(q.upperStart, crop),
|
|
708
|
-
p2: (0, _embedpdf_core_annotation.contentToPdfPoint)(q.upperEnd, crop),
|
|
709
|
-
p3: (0, _embedpdf_core_annotation.contentToPdfPoint)(q.lowerStart, crop),
|
|
710
|
-
p4: (0, _embedpdf_core_annotation.contentToPdfPoint)(q.lowerEnd, crop)
|
|
711
|
-
}));
|
|
712
|
-
}
|
|
713
|
-
/** PDF-space bounding box of a set of `/QuadPoints` quads — the `/Rect` a
|
|
714
|
-
* quad-bearing draft must carry alongside its quads. */
|
|
715
|
-
const pdfBoundsOfQuads = (quads) => {
|
|
716
|
-
let left = Infinity;
|
|
717
|
-
let bottom = Infinity;
|
|
718
|
-
let right = -Infinity;
|
|
719
|
-
let top = -Infinity;
|
|
720
|
-
for (const q of quads) for (const p of [
|
|
721
|
-
q.p1,
|
|
722
|
-
q.p2,
|
|
723
|
-
q.p3,
|
|
724
|
-
q.p4
|
|
725
|
-
]) {
|
|
726
|
-
if (p.x < left) left = p.x;
|
|
727
|
-
if (p.x > right) right = p.x;
|
|
728
|
-
if (p.y < bottom) bottom = p.y;
|
|
729
|
-
if (p.y > top) top = p.y;
|
|
730
|
-
}
|
|
731
|
-
return {
|
|
732
|
-
left,
|
|
733
|
-
bottom,
|
|
734
|
-
right,
|
|
735
|
-
top
|
|
736
|
-
};
|
|
737
|
-
};
|
|
738
|
-
const markupProjection = (subtype) => {
|
|
739
|
-
return {
|
|
740
|
-
ingest: (dto, crop) => {
|
|
741
|
-
const d = dto;
|
|
742
|
-
return {
|
|
743
|
-
geom: {
|
|
744
|
-
t: "quads",
|
|
745
|
-
quads: quadsFromDTO(d.quadPoints, crop)
|
|
746
|
-
},
|
|
747
|
-
...subtype === "strikeout" && "intent" in d && d.intent ? { intent: d.intent } : {}
|
|
748
|
-
};
|
|
749
|
-
},
|
|
750
|
-
geometry: () => null,
|
|
751
|
-
draftExtras: (a, crop) => {
|
|
752
|
-
const quads = quadPointsFor(a, crop);
|
|
753
|
-
if (!quads) return null;
|
|
754
|
-
return {
|
|
755
|
-
quadPoints: quads,
|
|
756
|
-
...subtype === "strikeout" && a.intent === "strikeout-text-edit" ? { intent: a.intent } : {}
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
};
|
|
760
|
-
};
|
|
761
|
-
const highlight = markupProjection("highlight");
|
|
762
|
-
const underline = markupProjection("underline");
|
|
763
|
-
const squiggly = markupProjection("squiggly");
|
|
764
|
-
const strikeout = markupProjection("strikeout");
|
|
765
|
-
const caret = {
|
|
766
|
-
ingest: (dto, crop) => {
|
|
767
|
-
const d = dto;
|
|
768
|
-
const rot = d.rotation ? require_seam.fromPdfRotation(d.rotation) : 0;
|
|
769
|
-
return {
|
|
770
|
-
geom: {
|
|
771
|
-
t: "caret",
|
|
772
|
-
rect: (0, _embedpdf_core_annotation.pdfToContentRect)(rot && d.unrotatedRect ? d.unrotatedRect : d.rect, crop),
|
|
773
|
-
...rot ? { rot } : {}
|
|
774
|
-
},
|
|
775
|
-
...d.intent ? { intent: d.intent } : {}
|
|
776
|
-
};
|
|
777
|
-
},
|
|
778
|
-
geometry: (a, crop) => a.geom.t === "caret" ? require_seam.boxGeomFields(a.geom.rect, a.geom.rot ?? 0, crop) : null,
|
|
779
|
-
draftExtras: (a) => ({
|
|
780
|
-
rectDifferences: {
|
|
781
|
-
left: .5,
|
|
782
|
-
top: .5,
|
|
783
|
-
right: .5,
|
|
784
|
-
bottom: .5
|
|
785
|
-
},
|
|
786
|
-
...a.intent === "replace" ? { intent: a.intent } : {},
|
|
787
|
-
...a.data?.contents != null ? { contents: a.data.contents } : {}
|
|
788
|
-
})
|
|
789
|
-
};
|
|
790
|
-
const redact = {
|
|
791
|
-
ingest: (dto, crop) => {
|
|
792
|
-
const d = dto;
|
|
793
|
-
return {
|
|
794
|
-
geom: d.quadPoints.length > 0 ? {
|
|
795
|
-
t: "quads",
|
|
796
|
-
quads: quadsFromDTO(d.quadPoints, crop)
|
|
797
|
-
} : {
|
|
798
|
-
t: "rect",
|
|
799
|
-
rect: (0, _embedpdf_core_annotation.pdfToContentRect)(d.rect, crop),
|
|
800
|
-
ellipse: false
|
|
801
|
-
},
|
|
802
|
-
text: {
|
|
803
|
-
fontFamily: d.fontFamily,
|
|
804
|
-
fontSize: d.fontSize,
|
|
805
|
-
fontColor: require_seam.colorToCss(d.fontColor),
|
|
806
|
-
textAlign: d.textAlign
|
|
807
|
-
},
|
|
808
|
-
...d.overlayText ? { label: {
|
|
809
|
-
text: d.overlayText,
|
|
810
|
-
repeat: d.repeat
|
|
811
|
-
} } : {}
|
|
812
|
-
};
|
|
813
|
-
},
|
|
814
|
-
geometry: (a, crop) => a.geom.t === "rect" ? { rect: (0, _embedpdf_core_annotation.contentToPdfRect)(a.geom.rect, crop) } : null,
|
|
815
|
-
draftExtras: (a, crop) => {
|
|
816
|
-
const quads = quadPointsFor(a, crop);
|
|
817
|
-
return quads ? {
|
|
818
|
-
quadPoints: quads,
|
|
819
|
-
rect: pdfBoundsOfQuads(quads)
|
|
820
|
-
} : {};
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
//#endregion
|
|
824
|
-
//#region src/repository/kinds/misc.ts
|
|
825
|
-
const rectGeometry = (a, crop) => "rect" in a.geom ? { rect: (0, _embedpdf_core_annotation.contentToPdfRect)(a.geom.rect, crop) } : null;
|
|
826
|
-
const iconProjection = (subtype) => ({
|
|
827
|
-
ingest: (dto, crop) => {
|
|
828
|
-
const d = dto;
|
|
829
|
-
return {
|
|
830
|
-
geom: {
|
|
831
|
-
t: "rect",
|
|
832
|
-
rect: (0, _embedpdf_core_annotation.pdfToContentRect)(d.rect, crop),
|
|
833
|
-
ellipse: false
|
|
834
|
-
},
|
|
835
|
-
icon: d.icon
|
|
836
|
-
};
|
|
837
|
-
},
|
|
838
|
-
geometry: rectGeometry,
|
|
839
|
-
createable: false
|
|
840
|
-
});
|
|
841
|
-
const textNote = iconProjection("text");
|
|
842
|
-
const fileAttachment = iconProjection("file-attachment");
|
|
843
|
-
const stamp = {
|
|
844
|
-
ingest: (dto, crop) => {
|
|
845
|
-
const d = dto;
|
|
846
|
-
return { geom: require_seam.boxGeomFromDTO(d, d.rotation, d.unrotatedRect, crop, false) };
|
|
847
|
-
},
|
|
848
|
-
geometry: (a, crop) => boxEmit(a, crop),
|
|
849
|
-
createable: false
|
|
850
|
-
};
|
|
851
|
-
const link = {
|
|
852
|
-
ingest: (dto, crop) => {
|
|
853
|
-
const d = dto;
|
|
854
|
-
return {
|
|
855
|
-
geom: {
|
|
856
|
-
t: "rect",
|
|
857
|
-
rect: (0, _embedpdf_core_annotation.pdfToContentRect)(d.rect, crop),
|
|
858
|
-
ellipse: false
|
|
859
|
-
},
|
|
860
|
-
link: d.target
|
|
861
|
-
};
|
|
862
|
-
},
|
|
863
|
-
geometry: rectGeometry,
|
|
864
|
-
prop: { link: (a) => {
|
|
865
|
-
const target = require_seam.writableTarget(a.link);
|
|
866
|
-
return target ? { target } : a.link == null ? { target: null } : {};
|
|
867
|
-
} },
|
|
868
|
-
draftExtras: (a) => ({ target: require_seam.writableTarget(a.link) })
|
|
869
|
-
};
|
|
870
|
-
/** One PDF `widget` subtype → per-family CLIENT kinds (radios have no font). */
|
|
871
|
-
const WIDGET_KIND_BY_FAMILY = {
|
|
872
|
-
text: "widget-text",
|
|
873
|
-
combobox: "widget-choice",
|
|
874
|
-
listbox: "widget-choice",
|
|
875
|
-
pushbutton: "widget-button",
|
|
876
|
-
checkbox: "widget-toggle",
|
|
877
|
-
radio: "widget-toggle"
|
|
878
|
-
};
|
|
879
|
-
const widgetKindOf = (family) => WIDGET_KIND_BY_FAMILY[family] ?? "widget-box";
|
|
880
|
-
const WIDGET_TEXT_KINDS = /* @__PURE__ */ new Set([
|
|
881
|
-
"widget-text",
|
|
882
|
-
"widget-choice",
|
|
883
|
-
"widget-button"
|
|
884
|
-
]);
|
|
885
|
-
function widgetTextFromDTO(dto) {
|
|
886
|
-
return {
|
|
887
|
-
fontFamily: dto.fontFamily ?? "helvetica",
|
|
888
|
-
fontSize: dto.fontSize ?? 0,
|
|
889
|
-
fontColor: dto.fontColor ? require_seam.colorToCss(dto.fontColor) : "#000000",
|
|
890
|
-
textAlign: dto.textAlign
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
const widget = {
|
|
894
|
-
ingest: (dto, crop) => {
|
|
895
|
-
const d = dto;
|
|
896
|
-
return {
|
|
897
|
-
geom: require_seam.boxGeomFromDTO(d, void 0, void 0, crop, false),
|
|
898
|
-
...WIDGET_TEXT_KINDS.has(widgetKindOf(d.fieldFamily)) ? { text: widgetTextFromDTO(d) } : {}
|
|
899
|
-
};
|
|
900
|
-
},
|
|
901
|
-
geometry: rectGeometry,
|
|
902
|
-
createable: false
|
|
903
|
-
};
|
|
904
|
-
const unsupported = {
|
|
905
|
-
ingest: (dto, crop) => ({ geom: {
|
|
906
|
-
t: "rect",
|
|
907
|
-
rect: (0, _embedpdf_core_annotation.pdfToContentRect)(dto.rect, crop),
|
|
908
|
-
ellipse: false
|
|
909
|
-
} }),
|
|
910
|
-
geometry: () => null,
|
|
911
|
-
createable: false
|
|
912
|
-
};
|
|
913
|
-
//#endregion
|
|
914
|
-
//#region src/repository/links.ts
|
|
915
|
-
/**
|
|
916
|
-
* Attached-link folding + desired-state derivation — RELATIONSHIP logic, not
|
|
917
|
-
* projection: a `/Link` child grouped under a linkable parent becomes the
|
|
918
|
-
* parent's `link` prop, and the reconciler derives the children it should
|
|
919
|
-
* have from the parent's committed geometry.
|
|
920
|
-
*/
|
|
921
|
-
/**
|
|
922
|
-
* The DESIRED hit rects (content space) of a parent's attached link
|
|
923
|
-
* children — derived fresh from the parent's committed geometry, never
|
|
924
|
-
* tracked: markup gets one child per quad (per-line hit areas, the v2
|
|
925
|
-
* segment behaviour), every other kind one child over its visual bounds.
|
|
926
|
-
* The reconciler and the navigation lens both read THIS, so the clickable
|
|
927
|
-
* area and the written `/Rect`s can never disagree.
|
|
928
|
-
*
|
|
929
|
-
* A link's `/Rect` is axis-aligned by spec (no rotation exists for links),
|
|
930
|
-
* so a ROTATED parent gets the AABB of its rotated footprint — the
|
|
931
|
-
* `selectionQuad` corners (rotation + stroke included), the same envelope
|
|
932
|
-
* the selection chrome outlines. Exact rotated hit regions need
|
|
933
|
-
* `/QuadPoints` (tier 2).
|
|
934
|
-
*/
|
|
935
|
-
function linkChildRects(a) {
|
|
936
|
-
if (a.geom.t === "quads") return a.geom.quads.map(_embedpdf_core_geometry.textQuadBounds);
|
|
937
|
-
return [(0, _embedpdf_core_annotation.unionRect)((0, _embedpdf_core_annotation.selectionQuad)(a.geom, a.style.strokeWidth, a.style.border))];
|
|
938
|
-
}
|
|
939
|
-
//#endregion
|
|
940
|
-
//#region src/repository/index.ts
|
|
941
|
-
/** Every wire subtype declares exactly one projection — a missing kind is a
|
|
942
|
-
* COMPILE error, not a silent fall-through. */
|
|
943
|
-
const KINDS = {
|
|
944
|
-
square,
|
|
945
|
-
circle,
|
|
946
|
-
line,
|
|
947
|
-
polygon,
|
|
948
|
-
polyline,
|
|
949
|
-
ink,
|
|
950
|
-
"free-text": freeText,
|
|
951
|
-
highlight,
|
|
952
|
-
underline,
|
|
953
|
-
squiggly,
|
|
954
|
-
strikeout,
|
|
955
|
-
caret,
|
|
956
|
-
redact,
|
|
957
|
-
text: textNote,
|
|
958
|
-
"file-attachment": fileAttachment,
|
|
959
|
-
stamp,
|
|
960
|
-
link,
|
|
961
|
-
widget,
|
|
962
|
-
unsupported
|
|
963
|
-
};
|
|
964
|
-
/** Model subtypes are wire subtypes except the widget CLIENT kinds
|
|
965
|
-
* (`widget-text`…), which all project through the widget family. */
|
|
966
|
-
const projectionOf = (subtype) => subtype.startsWith("widget") ? KINDS.widget : KINDS[subtype] ?? KINDS.unsupported;
|
|
967
|
-
const wireSubtypeOf = (a) => a.subtype.startsWith("widget") ? "widget" : a.subtype;
|
|
968
|
-
/**
|
|
969
|
-
* Engine DTO → content-space Annot. `source` decides how it renders: `'baked'`
|
|
970
|
-
* shows the engine's appearance raster (a page load, or a remote edit — trust
|
|
971
|
-
* the authored AP); `'vector'` renders live from geom/style (we authored or
|
|
972
|
-
* changed it). `apBox` is the raster's content-space box, used while baked.
|
|
973
|
-
*/
|
|
974
|
-
function fromDTO(dto, crop, source = "baked") {
|
|
975
|
-
const slice = projectionOf(dto.subtype).ingest(dto, crop);
|
|
976
|
-
const strippedRect = !(dto.subtype === "free-text" && dto.intent === "free-text-callout") && "unrotatedRect" in dto && "rotation" in dto && dto.rotation ? dto.unrotatedRect : void 0;
|
|
977
|
-
return {
|
|
978
|
-
id: require_seam.refKey(dto.ref),
|
|
979
|
-
ref: dto.ref,
|
|
980
|
-
pon: dto.pageObjectNumber,
|
|
981
|
-
subtype: dto.subtype === "widget" ? widgetKindOf(dto.fieldFamily) : dto.subtype,
|
|
982
|
-
flags: dto.flags,
|
|
983
|
-
source,
|
|
984
|
-
data: dto,
|
|
985
|
-
...dto.inReplyTo ? { irt: require_seam.refKey(dto.inReplyTo) } : {},
|
|
986
|
-
...dto.replyType === "group" && dto.inReplyTo ? { group: require_seam.refKey(dto.inReplyTo) } : {},
|
|
987
|
-
style: require_seam.styleFromDTO(dto),
|
|
988
|
-
...slice,
|
|
989
|
-
apBox: (0, _embedpdf_core_annotation.pdfToContentRect)(strippedRect ?? dto.rect, crop),
|
|
990
|
-
...strippedRect ? { apRot: (0, _embedpdf_core_annotation.geomRotation)(slice.geom) } : {}
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
/** Lower `keys` through the kind's overrides + the generic table. `null` =
|
|
994
|
-
* some key has no lowering — the caller degrades to the full projection. */
|
|
995
|
-
function emitProps(a, crop, keys) {
|
|
996
|
-
const kind = projectionOf(a.subtype);
|
|
997
|
-
const out = {};
|
|
998
|
-
for (const key of keys) {
|
|
999
|
-
const lower = kind.prop?.[key] ?? GENERIC_PROPS[key];
|
|
1000
|
-
if (!lower) return null;
|
|
1001
|
-
Object.assign(out, lower(a, crop));
|
|
1002
|
-
}
|
|
1003
|
-
return out;
|
|
1004
|
-
}
|
|
1005
|
-
const editableKeys = (subtype) => (0, _embedpdf_core_annotation.propsFor)(subtype).map((s) => s.key);
|
|
1006
|
-
/** Content Annot → the FULL engine patch: the kind's geometry group plus every
|
|
1007
|
-
* prop it declares editable. The reference statement — scoped emission and
|
|
1008
|
-
* drafts both build on it. */
|
|
1009
|
-
function toPatch(a, crop) {
|
|
1010
|
-
const geo = projectionOf(a.subtype).geometry(a, crop);
|
|
1011
|
-
const props = emitProps(a, crop, editableKeys(a.subtype)) ?? {};
|
|
1012
|
-
if (!geo && Object.keys(props).length === 0) return null;
|
|
1013
|
-
return {
|
|
1014
|
-
subtype: wireSubtypeOf(a),
|
|
1015
|
-
...geo,
|
|
1016
|
-
...props
|
|
1017
|
-
};
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Content Annot + a {@link PatchScope} → the SPARSE patch for exactly that
|
|
1021
|
-
* intent (the shell's `patch` effect emitter): the reducer says WHAT changed —
|
|
1022
|
-
* geometry, or the props keys verbatim — and this lowers only that. Kinds
|
|
1023
|
-
* without editable geometry (text markup) and unlowerable keys degrade to the
|
|
1024
|
-
* full patch: verbose, never a dropped write.
|
|
1025
|
-
*/
|
|
1026
|
-
function toScopedPatch(a, scope, crop) {
|
|
1027
|
-
const kind = projectionOf(a.subtype);
|
|
1028
|
-
if (scope.kind === "geometry") {
|
|
1029
|
-
const geo = kind.geometry(a, crop);
|
|
1030
|
-
return geo ? {
|
|
1031
|
-
subtype: wireSubtypeOf(a),
|
|
1032
|
-
...geo
|
|
1033
|
-
} : toPatch(a, crop);
|
|
1034
|
-
}
|
|
1035
|
-
const props = emitProps(a, crop, scope.keys);
|
|
1036
|
-
if (props === null) return toPatch(a, crop);
|
|
1037
|
-
if (Object.keys(props).length === 0) return null;
|
|
1038
|
-
return {
|
|
1039
|
-
subtype: wireSubtypeOf(a),
|
|
1040
|
-
...props
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
/** Content Annot → engine create draft: the full statement plus the kind's
|
|
1044
|
-
* create-only extras, with the model's `/F` emitted verbatim, once, for every
|
|
1045
|
-
* kind (a fresh draw carries DRAWN_FLAGS plus any tool seed). `null` for the
|
|
1046
|
-
* kinds whose creates travel their own path (stamps, widgets, icon place). */
|
|
1047
|
-
function toCreateDraft(a, crop) {
|
|
1048
|
-
const kind = projectionOf(a.subtype);
|
|
1049
|
-
if (kind.createable === false) return null;
|
|
1050
|
-
const base = toPatch(a, crop);
|
|
1051
|
-
if (!base) return null;
|
|
1052
|
-
const extras = kind.draftExtras?.(a, crop);
|
|
1053
|
-
if (kind.draftExtras && extras === null) return null;
|
|
1054
|
-
return {
|
|
1055
|
-
...base,
|
|
1056
|
-
...extras,
|
|
1057
|
-
nm: crypto.randomUUID(),
|
|
1058
|
-
flags: a.flags
|
|
1059
|
-
};
|
|
1060
|
-
}
|
|
1061
|
-
//#endregion
|
|
1062
|
-
//#region src/hover-feed.ts
|
|
1063
|
-
/**
|
|
1064
|
-
* The annotation plane's E/X trigger feed — a thin adapter from the ONE
|
|
1065
|
-
* pointer-driven hover seam (the `hoverAt` diff) onto the shared hover pump.
|
|
1066
|
-
*
|
|
1067
|
-
* Anti-cascade is structural and lives in the CALLER's seam placement:
|
|
1068
|
-
* reducer-side hover clears (session-hide, remove, reload) never pass
|
|
1069
|
-
* through `hoverAt`, so an effect-induced hover loss can never masquerade as
|
|
1070
|
-
* a cursor exit. This module only decides WHO is hoverable here:
|
|
1071
|
-
*
|
|
1072
|
-
* - Drafts (no engine ref) can't carry /AA — skipped.
|
|
1073
|
-
* - Widgets and links belong to their own event planes (fill controls /
|
|
1074
|
-
* LinkLayer anchors own their pixels). When they ARE hit-testable on this
|
|
1075
|
-
* plane, an authoring tool owns them — firing hover actions while editing
|
|
1076
|
-
* would be wrong — so they are skipped unconditionally.
|
|
1077
|
-
* - Tree-less annotations are FREE: no target, no dispatch, and per-event
|
|
1078
|
-
* flags keep a lone /E or /X from dispatching its inert twin.
|
|
1079
|
-
*/
|
|
1080
|
-
function createAnnotationHoverFeed(actions, annotOf) {
|
|
1081
|
-
const pump = (0, _embedpdf_plugin_actions_contract.createHoverPump)(actions.dispatch);
|
|
1082
|
-
const targetOf = (id) => {
|
|
1083
|
-
if (!id) return null;
|
|
1084
|
-
const annot = annotOf(id);
|
|
1085
|
-
if (!annot?.ref) return null;
|
|
1086
|
-
if (annot.subtype.startsWith("widget") || annot.subtype === "link") return null;
|
|
1087
|
-
const enter = Boolean(annot.data?.actions?.cursorEnter?.root);
|
|
1088
|
-
const exit = Boolean(annot.data?.actions?.cursorExit?.root);
|
|
1089
|
-
if (!enter && !exit) return null;
|
|
1090
|
-
return {
|
|
1091
|
-
ref: annot.ref,
|
|
1092
|
-
pon: annot.pon,
|
|
1093
|
-
events: {
|
|
1094
|
-
enter,
|
|
1095
|
-
exit
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
};
|
|
1099
|
-
return { hover: (id) => pump.hover(targetOf(id)) };
|
|
1100
|
-
}
|
|
1101
|
-
//#endregion
|
|
1102
|
-
//#region src/text-item.ts
|
|
1103
|
-
/**
|
|
1104
|
-
* The free-text presentation projection: the core's geometry-only `textBoxes`
|
|
1105
|
-
* joined with the DTO-derived font + CSS into render-ready {@link TextItem}s. This
|
|
1106
|
-
* is the text analogue of the core's `scene()` "paint" for shapes — it lives in
|
|
1107
|
-
* the plugin (not the portable core) because the font→CSS stack mapping and the
|
|
1108
|
-
* engine `Color`→CSS seam are web concerns, shared across every web framework.
|
|
1109
|
-
*/
|
|
1110
|
-
/** Project the model's free-text boxes into render-ready {@link TextItem}s — the
|
|
1111
|
-
* core geometry (`textBoxes`) joined with the DTO-derived CSS. Pure; memoized by
|
|
1112
|
-
* model identity at the call site so selectors get a stable reference. */
|
|
1113
|
-
function buildTextItems(m, pon, view) {
|
|
1114
|
-
return (0, _embedpdf_core_annotation.textBoxes)(m, pon, view).map((tb) => {
|
|
1115
|
-
const a = m.byId[tb.id];
|
|
1116
|
-
const t = a?.text ?? _embedpdf_core_annotation.initialTextStyle;
|
|
1117
|
-
const sw = a?.style.strokeWidth ?? 0;
|
|
1118
|
-
const doc = a ? richDocOf(a) : null;
|
|
1119
|
-
return {
|
|
1120
|
-
id: tb.id,
|
|
1121
|
-
ref: a?.ref ?? null,
|
|
1122
|
-
box: tb.box,
|
|
1123
|
-
contents: a?.data?.contents ?? "",
|
|
1124
|
-
richText: { paragraphs: doc ? stripBodyDefaults(doc.paragraphs, doc.body) : [{ runs: [{ text: "" }] }] },
|
|
1125
|
-
editing: tb.editing,
|
|
1126
|
-
...tb.rot ? { rot: tb.rot } : {},
|
|
1127
|
-
css: {
|
|
1128
|
-
fontFamily: cssFontFamilyForFont(t.fontFamily),
|
|
1129
|
-
fontSize: t.fontSize,
|
|
1130
|
-
color: t.fontColor,
|
|
1131
|
-
fontWeight: t.bold ? 700 : 400,
|
|
1132
|
-
fontStyle: t.italic ? "italic" : "normal",
|
|
1133
|
-
textDecoration: t.underline ? "underline" : "none",
|
|
1134
|
-
align: t.textAlign,
|
|
1135
|
-
padding: (0, _embedpdf_core_annotation.textPlateInset)(sw)
|
|
1136
|
-
}
|
|
1137
|
-
};
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
//#endregion
|
|
1141
|
-
//#region src/placement.ts
|
|
1142
|
-
/**
|
|
1143
|
-
* Per-kind CODE for the click-to-place ICON kinds (note / file attachment)
|
|
1144
|
-
* — the `props.ts`/`repository.ts` pattern: all tool CONFIG stays in the
|
|
1145
|
-
* tool table; this module only interprets it. The stamp keeps its own
|
|
1146
|
-
* sizing/sniffing path in the capability; everything funnels through the
|
|
1147
|
-
* one `placeAt` entry there.
|
|
1148
|
-
*/
|
|
1149
|
-
/**
|
|
1150
|
-
* Icon kinds place at the generator's fixed box: the engine forces the
|
|
1151
|
-
* `/Rect` to 20×20 from its bottom-left corner (the spec's note-icon rule),
|
|
1152
|
-
* so the footprint ghost and the placement use exactly this size.
|
|
1153
|
-
*/
|
|
1154
|
-
const ICON_PLACE_SIZE = {
|
|
1155
|
-
width: 20,
|
|
1156
|
-
height: 20
|
|
1157
|
-
};
|
|
1158
|
-
const isIconPlaceKind = (subtype) => subtype === "text" || subtype === "file-attachment";
|
|
1159
|
-
/**
|
|
1160
|
-
* Build the engine create draft for a placed icon annotation. `geom` is the
|
|
1161
|
-
* repository's `boxGeomFields` emit (the `/Rect` + upright rotation pair);
|
|
1162
|
-
* `defaults` is the tool's resolved flat props bag (`defaultsFor`) — the
|
|
1163
|
-
* colour seam is crossed here via the repository's `cssToColor`, and the
|
|
1164
|
-
* icon falls back to the kind's own default when the bag carries none.
|
|
1165
|
-
*/
|
|
1166
|
-
function iconPlacementDraft(subtype, geom, defaults, flags, file) {
|
|
1167
|
-
const shared = {
|
|
1168
|
-
...geom,
|
|
1169
|
-
color: require_seam.cssToColor(defaults.color),
|
|
1170
|
-
opacity: defaults.opacity,
|
|
1171
|
-
flags: {
|
|
1172
|
-
print: true,
|
|
1173
|
-
...flags
|
|
1174
|
-
}
|
|
1175
|
-
};
|
|
1176
|
-
if (subtype === "text") return {
|
|
1177
|
-
subtype: "text",
|
|
1178
|
-
icon: defaults.icon ?? "comment",
|
|
1179
|
-
...shared
|
|
1180
|
-
};
|
|
1181
|
-
if (!file) throw new Error("[annotation] a file-attachment placement requires a file payload");
|
|
1182
|
-
return {
|
|
1183
|
-
subtype: "file-attachment",
|
|
1184
|
-
icon: defaults.icon ?? "paperclip",
|
|
1185
|
-
file,
|
|
1186
|
-
...shared
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
//#endregion
|
|
1190
|
-
//#region src/capability.ts
|
|
1191
|
-
/** Fold `zoom`/`rotation` host args into the core's ViewEnv (or none).
|
|
1192
|
-
* `zoom` is the page's RELATIVE zoom (`transform.zoom`) — never the
|
|
1193
|
-
* px-per-point `scale` (that one only converts CSS-px chrome settings). */
|
|
1194
|
-
const viewEnv = (zoom, rotation) => zoom != null || rotation != null ? {
|
|
1195
|
-
zoom: zoom ?? 1,
|
|
1196
|
-
rotation: rotation ?? 0
|
|
1197
|
-
} : void 0;
|
|
1198
|
-
const TEXT_COMMIT_DEBOUNCE_MS = 250;
|
|
1199
|
-
/**
|
|
1200
|
-
* The annotation shell. The pure `update` runs HERE (so it can emit effects);
|
|
1201
|
-
* the resulting model is dispatched to the store, and each effect is performed
|
|
1202
|
-
* against the engine repository — optimistic create → reconcile to the durable
|
|
1203
|
-
* ref, patch/delete fire-and-forget.
|
|
1204
|
-
*/
|
|
1205
|
-
function createAnnotationCapability(ctx, config = {}) {
|
|
1206
|
-
const behaviors = [];
|
|
1207
|
-
/** Per-annotation debounce timer for the engine text write while typing. */
|
|
1208
|
-
const textTimers = /* @__PURE__ */ new Map();
|
|
1209
|
-
/** The registered fonts, for face ↔ key mapping (the local engine's list;
|
|
1210
|
-
* the cloud engine registers none). */
|
|
1211
|
-
const fonts = () => ctx.engine?.fonts?.list() ?? [];
|
|
1212
|
-
const configTools = config.tools ?? [];
|
|
1213
|
-
const registry = require_tools.buildToolRegistry(configTools);
|
|
1214
|
-
/** The installed file-picker port (a DOM file dialog, wired by the framework
|
|
1215
|
-
* adapter), or null — every click-then-pick tool resolves through this ONE
|
|
1216
|
-
* slot. See {@link FilePickerProvider}. */
|
|
1217
|
-
let filePickerProvider = null;
|
|
1218
|
-
const model = () => ctx.getState().model;
|
|
1219
|
-
const actionsForHover = ctx.tryGet(_embedpdf_plugin_actions_contract.ActionsToken);
|
|
1220
|
-
const hoverFeed = actionsForHover ? createAnnotationHoverFeed(actionsForHover, (id) => model().byId[id] ?? null) : null;
|
|
1221
|
-
const chromeSettings = () => ctx.getState().chrome;
|
|
1222
|
-
/**
|
|
1223
|
-
* Ids on a page whose Behavior is currently ENGAGED (form widgets under a
|
|
1224
|
-
* fill tool): they render their own DOM, so hit-test/marquee must not see
|
|
1225
|
-
* them. Resolved per event — engagement follows the active tool live.
|
|
1226
|
-
*/
|
|
1227
|
-
const inertIdsAt = (pon) => {
|
|
1228
|
-
if (!behaviors.length) return void 0;
|
|
1229
|
-
const m = model();
|
|
1230
|
-
let out;
|
|
1231
|
-
for (const id of m.order) {
|
|
1232
|
-
const a = m.byId[id];
|
|
1233
|
-
if (!a || a.pon !== pon) continue;
|
|
1234
|
-
if (behaviors.some((b) => b.matches({
|
|
1235
|
-
subtype: a.subtype,
|
|
1236
|
-
ref: a.ref
|
|
1237
|
-
}) && b.engaged())) (out ?? (out = /* @__PURE__ */ new Set())).add(id);
|
|
1238
|
-
}
|
|
1239
|
-
return out;
|
|
1240
|
-
};
|
|
1241
|
-
/** The CSS-px chrome settings converted to CONTENT units by the page's view
|
|
1242
|
-
* scale (px per content unit) — screen-constant grab zones + stalk at every
|
|
1243
|
-
* zoom. No scale → the values are read as content units (headless callers).
|
|
1244
|
-
* `boost` widens the GRAB tolerances only (never the drawn chrome or the
|
|
1245
|
-
* knob's position) — the touch path passes {@link TOUCH_GRAB_BOOST} so
|
|
1246
|
-
* handles present finger-sized targets. */
|
|
1247
|
-
const chromeGeomAt = (scale, boost = 1) => {
|
|
1248
|
-
const cs = chromeSettings();
|
|
1249
|
-
const s = scale || 1;
|
|
1250
|
-
return {
|
|
1251
|
-
handleTol: cs.handles.hitSize / 2 / s * boost,
|
|
1252
|
-
knobTol: cs.knob.hitSize / 2 / s * boost,
|
|
1253
|
-
knobOffset: cs.knob.offset / s
|
|
1254
|
-
};
|
|
1255
|
-
};
|
|
1256
|
-
/** Finger-sized grab zones: 2× the mouse tolerances lands a ~24px handle hit
|
|
1257
|
-
* box in Apple's ~44pt-target territory without moving any visuals. */
|
|
1258
|
-
const TOUCH_GRAB_BOOST = 2;
|
|
1259
|
-
const grabBoost = (touch) => touch ? TOUCH_GRAB_BOOST : 1;
|
|
1260
|
-
const itemsCache = /* @__PURE__ */ new Map();
|
|
1261
|
-
const chromeCache = /* @__PURE__ */ new Map();
|
|
1262
|
-
const memoItems = (pon, view) => {
|
|
1263
|
-
const m = model();
|
|
1264
|
-
const g = ctx.getState().toolGhost;
|
|
1265
|
-
const c = itemsCache.get(pon);
|
|
1266
|
-
if (c && c.model === m && c.ghost === g && c.zoom === view?.zoom && c.rotation === view?.rotation) return c.v;
|
|
1267
|
-
const v = (0, _embedpdf_core_annotation.pageItems)(m, pon, view);
|
|
1268
|
-
if (g && g.pon === pon && g.kind === "vector") {
|
|
1269
|
-
const tool = registry.get(g.toolId);
|
|
1270
|
-
const style = (0, _embedpdf_core_annotation.styleFromProps)((0, _embedpdf_core_annotation.defaultsFor)(m, tool?.preset ?? g.toolId));
|
|
1271
|
-
v.push({
|
|
1272
|
-
id: "tool-ghost",
|
|
1273
|
-
ref: null,
|
|
1274
|
-
subtype: tool?.subtype ?? "square",
|
|
1275
|
-
geom: g.geom,
|
|
1276
|
-
box: g.box,
|
|
1277
|
-
style,
|
|
1278
|
-
source: "ghost",
|
|
1279
|
-
selected: false
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
itemsCache.set(pon, {
|
|
1283
|
-
model: m,
|
|
1284
|
-
ghost: g,
|
|
1285
|
-
zoom: view?.zoom,
|
|
1286
|
-
rotation: view?.rotation,
|
|
1287
|
-
v
|
|
1288
|
-
});
|
|
1289
|
-
return v;
|
|
1290
|
-
};
|
|
1291
|
-
const memoChrome = (pon, scale, rotation, zoom) => {
|
|
1292
|
-
const m = model();
|
|
1293
|
-
const cs = chromeSettings();
|
|
1294
|
-
const c = chromeCache.get(pon);
|
|
1295
|
-
if (c && c.model === m && c.cs === cs && c.scale === scale && c.rotation === rotation && c.zoom === zoom) return c.v;
|
|
1296
|
-
let v = (0, _embedpdf_core_annotation.chrome)(m, pon, pageBoxOf(pon), chromeGeomAt(scale).knobOffset, viewEnv(zoom, rotation));
|
|
1297
|
-
if (!cs.guides.enabled) v = v.filter((n) => n.kind !== "rotate-guides");
|
|
1298
|
-
chromeCache.set(pon, {
|
|
1299
|
-
model: m,
|
|
1300
|
-
cs,
|
|
1301
|
-
scale,
|
|
1302
|
-
rotation,
|
|
1303
|
-
zoom,
|
|
1304
|
-
v
|
|
1305
|
-
});
|
|
1306
|
-
return v;
|
|
1307
|
-
};
|
|
1308
|
-
let anchorCache = null;
|
|
1309
|
-
const memoAnchor = (scale, rotation, zoom) => {
|
|
1310
|
-
const m = model();
|
|
1311
|
-
const cs = chromeSettings();
|
|
1312
|
-
if (anchorCache && anchorCache.model === m && anchorCache.cs === cs && anchorCache.scale === scale && anchorCache.rotation === rotation && anchorCache.zoom === zoom) return anchorCache.v;
|
|
1313
|
-
const v = (0, _embedpdf_core_annotation.selectionAnchor)(m, pageBoxOf, () => chromeGeomAt(scale).knobOffset, () => viewEnv(zoom, rotation));
|
|
1314
|
-
anchorCache = {
|
|
1315
|
-
model: m,
|
|
1316
|
-
cs,
|
|
1317
|
-
scale,
|
|
1318
|
-
rotation,
|
|
1319
|
-
zoom,
|
|
1320
|
-
v
|
|
1321
|
-
};
|
|
1322
|
-
return v;
|
|
1323
|
-
};
|
|
1324
|
-
let draftAnchorCache = null;
|
|
1325
|
-
const memoDraftAnchor = () => {
|
|
1326
|
-
const m = model();
|
|
1327
|
-
if (draftAnchorCache && draftAnchorCache.model === m) return draftAnchorCache.v;
|
|
1328
|
-
const v = (0, _embedpdf_core_annotation.creationDraftAnchor)(m);
|
|
1329
|
-
draftAnchorCache = {
|
|
1330
|
-
model: m,
|
|
1331
|
-
v
|
|
1332
|
-
};
|
|
1333
|
-
return v;
|
|
1334
|
-
};
|
|
1335
|
-
/** The editor's text RANGE inside the annotation being edited, or null
|
|
1336
|
-
* (no editor selection, a bare caret, or a selection left behind by a
|
|
1337
|
-
* previous edit). The one condition that routes the range keys to runs. */
|
|
1338
|
-
const activeTextRange = (m) => {
|
|
1339
|
-
const ts = ctx.getState().textSelection;
|
|
1340
|
-
return ts && m.editing === ts.id && ts.end > ts.start && m.byId[ts.id] ? ts : null;
|
|
1341
|
-
};
|
|
1342
|
-
let selPropsCache = null;
|
|
1343
|
-
const memoSelectionProps = () => {
|
|
1344
|
-
const m = model();
|
|
1345
|
-
const range = activeTextRange(m);
|
|
1346
|
-
if (selPropsCache && selPropsCache.model === m && selPropsCache.range === range) return selPropsCache.v;
|
|
1347
|
-
const members = m.selected.map((id) => m.byId[id]).filter((a) => !!a);
|
|
1348
|
-
const specs = (0, _embedpdf_core_annotation.sharedProps)(members.map((a) => a.subtype));
|
|
1349
|
-
const values = {};
|
|
1350
|
-
const mixed = [];
|
|
1351
|
-
const valueOf = (a, key) => key === "link" && a.subtype !== "link" ? (0, _embedpdf_core_annotation.linkOf)(m, a.id) : (0, _embedpdf_core_annotation.readProp)(a, key);
|
|
1352
|
-
for (const spec of specs) {
|
|
1353
|
-
const first = valueOf(members[0], spec.key);
|
|
1354
|
-
values[spec.key] = first;
|
|
1355
|
-
const firstJson = JSON.stringify(first);
|
|
1356
|
-
if (members.some((a) => JSON.stringify(valueOf(a, spec.key)) !== firstJson)) mixed.push(spec.key);
|
|
1357
|
-
}
|
|
1358
|
-
if (range && members.length === 1 && members[0].id === range.id) {
|
|
1359
|
-
const rp = rangeProps(richDocOf(members[0], fonts), range, fonts);
|
|
1360
|
-
for (const spec of specs) {
|
|
1361
|
-
if (!RANGE_KEYS.includes(spec.key)) continue;
|
|
1362
|
-
values[spec.key] = rp.values[spec.key];
|
|
1363
|
-
const i = mixed.indexOf(spec.key);
|
|
1364
|
-
if (i >= 0) mixed.splice(i, 1);
|
|
1365
|
-
if (rp.mixed.includes(spec.key)) mixed.push(spec.key);
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
const v = {
|
|
1369
|
-
specs,
|
|
1370
|
-
values,
|
|
1371
|
-
mixed
|
|
1372
|
-
};
|
|
1373
|
-
selPropsCache = {
|
|
1374
|
-
model: m,
|
|
1375
|
-
range,
|
|
1376
|
-
v
|
|
1377
|
-
};
|
|
1378
|
-
return v;
|
|
1379
|
-
};
|
|
1380
|
-
const linkItemsCache = /* @__PURE__ */ new Map();
|
|
1381
|
-
const memoLinkItems = (pon) => {
|
|
1382
|
-
const m = model();
|
|
1383
|
-
const c = linkItemsCache.get(pon);
|
|
1384
|
-
if (c && c.model === m) return c.v;
|
|
1385
|
-
const v = [];
|
|
1386
|
-
for (const id of m.order) {
|
|
1387
|
-
const a = m.byId[id];
|
|
1388
|
-
if (!a || a.pon !== pon || a.subtype !== "link") continue;
|
|
1389
|
-
if (!(0, _embedpdf_core_annotation.viewable)(a.flags, false)) continue;
|
|
1390
|
-
const target = a.link ?? (a.data?.subtype === "link" ? a.data.target ?? null : null);
|
|
1391
|
-
if (target == null || a.geom.t !== "rect") continue;
|
|
1392
|
-
const activate = a.data?.actions?.activate;
|
|
1393
|
-
const ref = a.ref ?? a.data?.ref ?? void 0;
|
|
1394
|
-
const hoverEnter = Boolean(a.data?.actions?.cursorEnter?.root);
|
|
1395
|
-
const hoverExit = Boolean(a.data?.actions?.cursorExit?.root);
|
|
1396
|
-
v.push({
|
|
1397
|
-
id,
|
|
1398
|
-
rect: a.geom.rect,
|
|
1399
|
-
target,
|
|
1400
|
-
attached: a.group !== void 0,
|
|
1401
|
-
...activate ? { activate } : {},
|
|
1402
|
-
...ref ? { ref } : {},
|
|
1403
|
-
...hoverEnter || hoverExit ? { hoverEvents: {
|
|
1404
|
-
enter: hoverEnter,
|
|
1405
|
-
exit: hoverExit
|
|
1406
|
-
} } : {}
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
linkItemsCache.set(pon, {
|
|
1410
|
-
model: m,
|
|
1411
|
-
v
|
|
1412
|
-
});
|
|
1413
|
-
return v;
|
|
1414
|
-
};
|
|
1415
|
-
const textsCache = /* @__PURE__ */ new Map();
|
|
1416
|
-
const memoTexts = (pon, view) => {
|
|
1417
|
-
const m = model();
|
|
1418
|
-
const c = textsCache.get(pon);
|
|
1419
|
-
if (c && c.model === m && c.zoom === view?.zoom && c.rotation === view?.rotation) return c.v;
|
|
1420
|
-
const v = buildTextItems(m, pon, view);
|
|
1421
|
-
textsCache.set(pon, {
|
|
1422
|
-
model: m,
|
|
1423
|
-
zoom: view?.zoom,
|
|
1424
|
-
rotation: view?.rotation,
|
|
1425
|
-
v
|
|
1426
|
-
});
|
|
1427
|
-
return v;
|
|
1428
|
-
};
|
|
1429
|
-
let selFlagsCache = null;
|
|
1430
|
-
const memoSelectionFlags = () => {
|
|
1431
|
-
const m = model();
|
|
1432
|
-
if (selFlagsCache && selFlagsCache.model === m) return selFlagsCache.v;
|
|
1433
|
-
const members = m.selected.map((id) => m.byId[id]).filter((a) => !!a);
|
|
1434
|
-
let v = null;
|
|
1435
|
-
if (members.length) {
|
|
1436
|
-
v = {};
|
|
1437
|
-
for (const key of _embedpdf_core_annotation.FLAG_KEYS) {
|
|
1438
|
-
const first = members[0].flags[key];
|
|
1439
|
-
v[key] = members.every((a) => a.flags[key] === first) ? first : null;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
selFlagsCache = {
|
|
1443
|
-
model: m,
|
|
1444
|
-
v
|
|
1445
|
-
};
|
|
1446
|
-
return v;
|
|
1447
|
-
};
|
|
1448
|
-
const cropOf = (pon) => ctx.document()?.pages.find((p) => p.pageObjectNumber === pon)?.boxes.crop ?? null;
|
|
1449
|
-
/** The page's box in content space (origin at the crop top-left) — the box
|
|
1450
|
-
* pointer gestures clamp to, so annotations stay page-bound. */
|
|
1451
|
-
const pageBoxOf = (pon) => {
|
|
1452
|
-
const crop = cropOf(pon);
|
|
1453
|
-
return crop ? {
|
|
1454
|
-
x: 0,
|
|
1455
|
-
y: 0,
|
|
1456
|
-
width: crop.right - crop.left,
|
|
1457
|
-
height: crop.top - crop.bottom
|
|
1458
|
-
} : void 0;
|
|
1459
|
-
};
|
|
1460
|
-
/**
|
|
1461
|
-
* The armed stamp-tool payload: the bytes the next click places, plus the
|
|
1462
|
-
* PDF-point placement size (derived from the sniffed intrinsic aspect).
|
|
1463
|
-
* Transient tool state — deliberately NOT in the model: it is never
|
|
1464
|
-
* rendered, never synced, and dies with the tool.
|
|
1465
|
-
*/
|
|
1466
|
-
let armedStamp = null;
|
|
1467
|
-
/** Fixed bytes as a provider: the same image at every size. */
|
|
1468
|
-
const fixedPreview = (bytes, mimeType) => {
|
|
1469
|
-
const preview = {
|
|
1470
|
-
bytes,
|
|
1471
|
-
...mimeType ? { mimeType } : {}
|
|
1472
|
-
};
|
|
1473
|
-
return async () => preview;
|
|
1474
|
-
};
|
|
1475
|
-
/**
|
|
1476
|
-
* The desired stamp size (PDF points) from sniffed bytes: the image's
|
|
1477
|
-
* INTRINSIC pixel dimensions taken 1:1 as points (the v2 rule — keep the
|
|
1478
|
-
* artwork's own size, then clamp to the page at placement). A `targetWidth`
|
|
1479
|
-
* override scales to that width, aspect preserved. Vector (PDF) stamps carry
|
|
1480
|
-
* no sniffable dimensions — a caller-supplied `intrinsic` override (a stamp
|
|
1481
|
-
* library knows its page size) keeps the true aspect; without one they fall
|
|
1482
|
-
* back to a square target.
|
|
1483
|
-
*/
|
|
1484
|
-
const desiredStampSize = (meta, targetWidth, intrinsicOverride) => {
|
|
1485
|
-
const intrinsic = intrinsicOverride && intrinsicOverride.width > 0 && intrinsicOverride.height > 0 ? intrinsicOverride : "width" in meta && meta.width > 0 ? {
|
|
1486
|
-
width: meta.width,
|
|
1487
|
-
height: meta.height
|
|
1488
|
-
} : {
|
|
1489
|
-
width: targetWidth ?? 150,
|
|
1490
|
-
height: targetWidth ?? 150
|
|
1491
|
-
};
|
|
1492
|
-
if (targetWidth === void 0) return intrinsic;
|
|
1493
|
-
return {
|
|
1494
|
-
width: targetWidth,
|
|
1495
|
-
height: targetWidth * (intrinsic.height / intrinsic.width)
|
|
1496
|
-
};
|
|
1497
|
-
};
|
|
1498
|
-
const armStamp = async (input) => {
|
|
1499
|
-
const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.source);
|
|
1500
|
-
const meta = (0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)(resolved.bytes);
|
|
1501
|
-
if (!meta) throw new Error("[annotation] stamp source must be PNG, JPEG, or single-page PDF bytes");
|
|
1502
|
-
let preview = null;
|
|
1503
|
-
if (typeof input.preview === "function") preview = input.preview;
|
|
1504
|
-
else if (input.preview) {
|
|
1505
|
-
const p = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.preview);
|
|
1506
|
-
preview = fixedPreview(new Uint8Array(p.bytes), p.mimeType);
|
|
1507
|
-
} else if (meta.mimeType !== "application/pdf") preview = fixedPreview(new Uint8Array(resolved.bytes), meta.mimeType);
|
|
1508
|
-
armedStamp = {
|
|
1509
|
-
source: input.source,
|
|
1510
|
-
...desiredStampSize(meta, input.targetWidth, input.intrinsicSize),
|
|
1511
|
-
preview,
|
|
1512
|
-
previewCache: /* @__PURE__ */ new Map(),
|
|
1513
|
-
...input.name !== void 0 ? { name: input.name } : {},
|
|
1514
|
-
...input.subject !== void 0 ? { subject: input.subject } : {}
|
|
1515
|
-
};
|
|
1516
|
-
ctx.dispatch({ type: "STAMP_ARM_CHANGED" });
|
|
1517
|
-
ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.activateTool(require_tools.ARMED_STAMP_TOOL_ID);
|
|
1518
|
-
};
|
|
1519
|
-
const disarmStamp = () => {
|
|
1520
|
-
if (!armedStamp) return;
|
|
1521
|
-
armedStamp = null;
|
|
1522
|
-
ctx.dispatch({ type: "STAMP_ARM_CHANGED" });
|
|
1523
|
-
};
|
|
1524
|
-
/** Move the hover FOOTPRINT ghost to a content point. The box/geometry is
|
|
1525
|
-
* computed by the SAME rules the click's placement uses ({@link createStampAt}
|
|
1526
|
-
* fit + clamp for an armed stamp; the click-create anchor + page clamp for a
|
|
1527
|
-
* draw tool), so the ghost is the placement, not an approximation of it. */
|
|
1528
|
-
const ghostHoverAt = (toolId, pon, point, displayRotation) => {
|
|
1529
|
-
const tool = registry.get(toolId);
|
|
1530
|
-
const crop = cropOf(pon);
|
|
1531
|
-
if (!tool || tool.ghost === false || !crop) {
|
|
1532
|
-
clearGhost();
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
const page = {
|
|
1536
|
-
width: crop.right - crop.left,
|
|
1537
|
-
height: crop.top - crop.bottom
|
|
1538
|
-
};
|
|
1539
|
-
if (armedStamp) {
|
|
1540
|
-
const rot = uprightRotFor(displayRotation);
|
|
1541
|
-
const box = (0, _embedpdf_core_annotation.fitStampBox)(point, {
|
|
1542
|
-
width: armedStamp.width,
|
|
1543
|
-
height: armedStamp.height
|
|
1544
|
-
}, page, rot);
|
|
1545
|
-
ctx.dispatch({
|
|
1546
|
-
type: "SET_TOOL_GHOST",
|
|
1547
|
-
ghost: {
|
|
1548
|
-
pon,
|
|
1549
|
-
box,
|
|
1550
|
-
rot,
|
|
1551
|
-
kind: "image"
|
|
1552
|
-
}
|
|
1553
|
-
});
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
if (isIconPlaceKind(tool.subtype)) {
|
|
1557
|
-
const box = (0, _embedpdf_core_annotation.fitStampBox)(point, ICON_PLACE_SIZE, page, uprightRotFor(displayRotation));
|
|
1558
|
-
dispatchVectorGhost(pon, toolId, {
|
|
1559
|
-
t: "rect",
|
|
1560
|
-
rect: box,
|
|
1561
|
-
ellipse: false
|
|
1562
|
-
});
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
if (!tool.clickCreate) {
|
|
1566
|
-
clearGhost();
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
const placement = (0, _embedpdf_core_annotation.resolveClickPlacement)(point, tool.clickCreate, {
|
|
1570
|
-
pageBox: {
|
|
1571
|
-
x: 0,
|
|
1572
|
-
y: 0,
|
|
1573
|
-
width: page.width,
|
|
1574
|
-
height: page.height
|
|
1575
|
-
},
|
|
1576
|
-
upright: tool.upright,
|
|
1577
|
-
displayRotation
|
|
1578
|
-
});
|
|
1579
|
-
const geom = (0, _embedpdf_core_annotation.clickCreateGeom)(tool.subtype, placement, (0, _embedpdf_core_annotation.defaultsFor)(model(), tool.preset));
|
|
1580
|
-
if (!geom) {
|
|
1581
|
-
clearGhost();
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
dispatchVectorGhost(pon, toolId, geom);
|
|
1585
|
-
};
|
|
1586
|
-
/** Paint a vector ghost item for a tool's would-be geometry — shared by the
|
|
1587
|
-
* hover footprint and the externally-driven placement preview. */
|
|
1588
|
-
const dispatchVectorGhost = (pon, toolId, geom) => {
|
|
1589
|
-
const tool = registry.get(toolId);
|
|
1590
|
-
const style = (0, _embedpdf_core_annotation.styleFromProps)((0, _embedpdf_core_annotation.defaultsFor)(model(), tool?.preset ?? toolId));
|
|
1591
|
-
ctx.dispatch({
|
|
1592
|
-
type: "SET_TOOL_GHOST",
|
|
1593
|
-
ghost: {
|
|
1594
|
-
pon,
|
|
1595
|
-
box: (0, _embedpdf_core_annotation.geomVisualBounds)(geom, style.strokeWidth, style.border),
|
|
1596
|
-
rot: 0,
|
|
1597
|
-
kind: "vector",
|
|
1598
|
-
toolId,
|
|
1599
|
-
geom
|
|
1600
|
-
}
|
|
1601
|
-
});
|
|
1602
|
-
};
|
|
1603
|
-
/**
|
|
1604
|
-
* Drive the placement preview during an EXTERNALLY-owned creation gesture
|
|
1605
|
-
* (the form plugin's drag-to-place): paint the box the commit would use,
|
|
1606
|
-
* styled from the TOOL's defaults, through the same ghost pipeline as every
|
|
1607
|
-
* footprint. The box is clamped to the page (a drag may overshoot).
|
|
1608
|
-
*/
|
|
1609
|
-
const setPlacementPreview = (toolId, pon, box) => {
|
|
1610
|
-
const crop = cropOf(pon);
|
|
1611
|
-
if (!crop) return;
|
|
1612
|
-
const page = {
|
|
1613
|
-
width: crop.right - crop.left,
|
|
1614
|
-
height: crop.top - crop.bottom
|
|
1615
|
-
};
|
|
1616
|
-
const x = Math.max(0, Math.min(box.x, page.width));
|
|
1617
|
-
const y = Math.max(0, Math.min(box.y, page.height));
|
|
1618
|
-
const rect = {
|
|
1619
|
-
x,
|
|
1620
|
-
y,
|
|
1621
|
-
width: Math.max(0, Math.min(box.x + box.width, page.width) - x),
|
|
1622
|
-
height: Math.max(0, Math.min(box.y + box.height, page.height) - y)
|
|
1623
|
-
};
|
|
1624
|
-
dispatchVectorGhost(pon, toolId, {
|
|
1625
|
-
t: "rect",
|
|
1626
|
-
rect,
|
|
1627
|
-
ellipse: false
|
|
1628
|
-
});
|
|
1629
|
-
};
|
|
1630
|
-
const clearGhost = () => {
|
|
1631
|
-
if (ctx.getState().toolGhost) ctx.dispatch({
|
|
1632
|
-
type: "SET_TOOL_GHOST",
|
|
1633
|
-
ghost: null
|
|
1634
|
-
});
|
|
1635
|
-
};
|
|
1636
|
-
/** Place a stamp of `desired` PDF-point size centred on a content point — the
|
|
1637
|
-
* one engine-write both the armed and click-to-place paths funnel through.
|
|
1638
|
-
* The size is fit to the page and clamped fully onto it (v2 rubber-stamp
|
|
1639
|
-
* rule: never larger than the page, aspect preserved), and never spills off
|
|
1640
|
-
* the edge. `rotCW` (the tool's upright counter-rotation, CW content degrees)
|
|
1641
|
-
* emits the repository's box rotation fields — the engine bakes the tilted
|
|
1642
|
-
* /AP exactly as an interactively rotated stamp round-trips, and the fit uses
|
|
1643
|
-
* the ROTATED footprint. Returns false when the page/document isn't ready. */
|
|
1644
|
-
const createStampAt = (pon, point, source, desired, rotCW = 0, identity = {}) => {
|
|
1645
|
-
const doc = ctx.doc;
|
|
1646
|
-
const crop = cropOf(pon);
|
|
1647
|
-
if (!doc || !crop) return null;
|
|
1648
|
-
const box = (0, _embedpdf_core_annotation.fitStampBox)(point, desired, {
|
|
1649
|
-
width: crop.right - crop.left,
|
|
1650
|
-
height: crop.top - crop.bottom
|
|
1651
|
-
}, rotCW);
|
|
1652
|
-
return doc.page(pon).annotations.create({
|
|
1653
|
-
subtype: "stamp",
|
|
1654
|
-
...require_seam.boxGeomFields(box, rotCW, crop),
|
|
1655
|
-
source,
|
|
1656
|
-
fit: "contain",
|
|
1657
|
-
...identity.name !== void 0 ? { name: identity.name } : {},
|
|
1658
|
-
...identity.subject !== void 0 ? { subject: identity.subject } : {}
|
|
1659
|
-
}).then((res) => {
|
|
1660
|
-
syncDTO(res.created, "baked");
|
|
1661
|
-
apply({
|
|
1662
|
-
t: "select",
|
|
1663
|
-
ids: [require_seam.refKey(res.created.ref)]
|
|
1664
|
-
});
|
|
1665
|
-
return res.created.ref;
|
|
1666
|
-
});
|
|
1667
|
-
};
|
|
1668
|
-
/** The click path's fire-and-forget wrapper: a rejected placement is logged,
|
|
1669
|
-
* never thrown into the gesture. */
|
|
1670
|
-
const createStampAtSync = (pon, point, source, desired, rotCW = 0, identity = {}) => {
|
|
1671
|
-
const placed = createStampAt(pon, point, source, desired, rotCW, identity);
|
|
1672
|
-
if (!placed) return false;
|
|
1673
|
-
placed.catch((err) => console.error("[annotation] stamp placement failed:", err));
|
|
1674
|
-
return true;
|
|
1675
|
-
};
|
|
1676
|
-
/** Programmatic placement — the same law as a click, awaited. */
|
|
1677
|
-
const placeStamp = async (input, placement) => {
|
|
1678
|
-
const meta = (0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)((await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.source)).bytes);
|
|
1679
|
-
if (!meta) throw new Error("[annotation] stamp source must be PNG, JPEG, or single-page PDF bytes");
|
|
1680
|
-
const placed = createStampAt(placement.pageObjectNumber, placement.at, input.source, desiredStampSize(meta, placement.targetWidth ?? input.targetWidth, input.intrinsicSize), placement.rotation ?? 0, {
|
|
1681
|
-
...input.name !== void 0 ? { name: input.name } : {},
|
|
1682
|
-
...input.subject !== void 0 ? { subject: input.subject } : {}
|
|
1683
|
-
});
|
|
1684
|
-
if (!placed) throw new Error(`[annotation] cannot place a stamp on page ${placement.pageObjectNumber}: document or page not ready`);
|
|
1685
|
-
return placed;
|
|
1686
|
-
};
|
|
1687
|
-
/** The active tool's upright counter-rotation for a click at `displayRotation`
|
|
1688
|
-
* (0 when the tool doesn't ask for upright, or the display isn't rotated). */
|
|
1689
|
-
const uprightRotFor = (displayRotation) => {
|
|
1690
|
-
if (!displayRotation) return 0;
|
|
1691
|
-
const ix = ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken);
|
|
1692
|
-
return (ix ? registry.get(ix.activeToolId()) : void 0)?.upright ? (0, _embedpdf_core_annotation.uprightRotation)(displayRotation) : 0;
|
|
1693
|
-
};
|
|
1694
|
-
const placeArmedStamp = (pon, point, displayRotation) => {
|
|
1695
|
-
const armed = armedStamp;
|
|
1696
|
-
if (!armed) return false;
|
|
1697
|
-
return createStampAtSync(pon, point, armed.source, {
|
|
1698
|
-
width: armed.width,
|
|
1699
|
-
height: armed.height
|
|
1700
|
-
}, uprightRotFor(displayRotation), {
|
|
1701
|
-
...armed.name !== void 0 ? { name: armed.name } : {},
|
|
1702
|
-
...armed.subject !== void 0 ? { subject: armed.subject } : {}
|
|
1703
|
-
});
|
|
1704
|
-
};
|
|
1705
|
-
/** Sniff a stamp source (rejecting non-image bytes) and place it at its
|
|
1706
|
-
* intrinsic size (fit to the page in {@link createStampAt}); `targetWidth`
|
|
1707
|
-
* overrides the width, aspect preserved. */
|
|
1708
|
-
const placeStampSource = async (pon, point, source, rotCW, targetWidth) => {
|
|
1709
|
-
const meta = (0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)((await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(source)).bytes);
|
|
1710
|
-
if (!meta) {
|
|
1711
|
-
console.error("[annotation] stamp source must be PNG, JPEG, or single-page PDF bytes");
|
|
1712
|
-
return;
|
|
1713
|
-
}
|
|
1714
|
-
createStampAtSync(pon, point, source, desiredStampSize(meta, targetWidth), rotCW);
|
|
1715
|
-
};
|
|
1716
|
-
/**
|
|
1717
|
-
* Click-to-place: resolve the ACTIVE tool's source spec. Fixed `bytes` place
|
|
1718
|
-
* immediately; a `'prompt'` source asks the installed provider, then places on
|
|
1719
|
-
* resolve — dropping the placement if it was cancelled, or if the tool or
|
|
1720
|
-
* document changed while the picker was open (the intent expired).
|
|
1721
|
-
*/
|
|
1722
|
-
const requestStampAt = (pon, point, displayRotation) => {
|
|
1723
|
-
const ix = ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken);
|
|
1724
|
-
const tool = ix ? registry.get(ix.activeToolId()) : void 0;
|
|
1725
|
-
const spec = tool?.source;
|
|
1726
|
-
if (!spec) return false;
|
|
1727
|
-
const rotCW = tool.upright && displayRotation ? (0, _embedpdf_core_annotation.uprightRotation)(displayRotation) : 0;
|
|
1728
|
-
if (spec.kind === "bytes") {
|
|
1729
|
-
placeStampSource(pon, point, spec.source, rotCW);
|
|
1730
|
-
return true;
|
|
1731
|
-
}
|
|
1732
|
-
return promptFileAt(tool, pon, point, (picked) => void placeStampSource(pon, point, picked.data, rotCW));
|
|
1733
|
-
};
|
|
1734
|
-
/**
|
|
1735
|
-
* Run the installed file-picker port for a click-then-pick tool, with the
|
|
1736
|
-
* shared expiry rule: the placement is dropped if the picker cancels, or if
|
|
1737
|
-
* the document or the active tool changed while it was open (the intent
|
|
1738
|
-
* expired with the click). No provider installed → decline the click (let a
|
|
1739
|
-
* lower-priority handler act) rather than swallow it.
|
|
1740
|
-
*/
|
|
1741
|
-
const promptFileAt = (tool, pon, point, place) => {
|
|
1742
|
-
const provider = filePickerProvider;
|
|
1743
|
-
if (!provider) return false;
|
|
1744
|
-
const spec = tool.source;
|
|
1745
|
-
const req = {
|
|
1746
|
-
toolId: tool.id,
|
|
1747
|
-
subtype: tool.subtype,
|
|
1748
|
-
accept: spec?.kind === "prompt" ? spec.accept : void 0,
|
|
1749
|
-
pon,
|
|
1750
|
-
point
|
|
1751
|
-
};
|
|
1752
|
-
const docAtClick = ctx.doc;
|
|
1753
|
-
provider(req).then((picked) => {
|
|
1754
|
-
if (!picked) return;
|
|
1755
|
-
if (ctx.doc !== docAtClick) return;
|
|
1756
|
-
if (ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.activeToolId() !== tool.id) return;
|
|
1757
|
-
place(picked);
|
|
1758
|
-
}, (err) => console.error("[annotation] file-picker provider failed:", err));
|
|
1759
|
-
return true;
|
|
1760
|
-
};
|
|
1761
|
-
/** Place a fixed-size ICON annotation (note / file attachment) centred on a
|
|
1762
|
-
* content point — the icon-kind sibling of {@link createStampAt}. The
|
|
1763
|
-
* engine bakes the 20×20 /AP from /C + /Name; a successful create selects
|
|
1764
|
-
* the new annotation (the anchor for its menu / future comment popup). */
|
|
1765
|
-
const createIconAt = (tool, pon, point, rotCW, file) => {
|
|
1766
|
-
const doc = ctx.doc;
|
|
1767
|
-
const crop = cropOf(pon);
|
|
1768
|
-
if (!doc || !crop || !isIconPlaceKind(tool.subtype)) return false;
|
|
1769
|
-
const box = (0, _embedpdf_core_annotation.fitStampBox)(point, ICON_PLACE_SIZE, {
|
|
1770
|
-
width: crop.right - crop.left,
|
|
1771
|
-
height: crop.top - crop.bottom
|
|
1772
|
-
}, rotCW);
|
|
1773
|
-
const draft = iconPlacementDraft(tool.subtype, require_seam.boxGeomFields(box, rotCW, crop), (0, _embedpdf_core_annotation.defaultsFor)(model(), tool.preset), tool.flags, file);
|
|
1774
|
-
doc.page(pon).annotations.create(draft).then((res) => {
|
|
1775
|
-
syncDTO(res.created, "baked");
|
|
1776
|
-
apply({
|
|
1777
|
-
t: "select",
|
|
1778
|
-
ids: [require_seam.refKey(res.created.ref)]
|
|
1779
|
-
});
|
|
1780
|
-
}, (err) => console.error("[annotation] icon placement failed:", err));
|
|
1781
|
-
return true;
|
|
1782
|
-
};
|
|
1783
|
-
/**
|
|
1784
|
-
* The ONE click-to-place entry the place handler forwards every down to —
|
|
1785
|
-
* armed payload first, then the active tool's kind decides:
|
|
1786
|
-
* - stamp → the source spec (fixed bytes, or the file-picker port)
|
|
1787
|
-
* - note (text) → place immediately (no payload)
|
|
1788
|
-
* - attachment → spot first, file second: the file-picker port opens
|
|
1789
|
-
* inside the click gesture, and placement lands when it
|
|
1790
|
-
* resolves (dropped on cancel, or if the tool/document
|
|
1791
|
-
* changed while the picker was open).
|
|
1792
|
-
* Returns whether the click was consumed.
|
|
1793
|
-
*/
|
|
1794
|
-
const placeAt = (pon, point, displayRotation) => {
|
|
1795
|
-
if (placeArmedStamp(pon, point, displayRotation)) return true;
|
|
1796
|
-
const ix = ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken);
|
|
1797
|
-
const tool = ix ? registry.get(ix.activeToolId()) : void 0;
|
|
1798
|
-
if (!tool) return false;
|
|
1799
|
-
if (tool.subtype === "stamp") return requestStampAt(pon, point, displayRotation);
|
|
1800
|
-
if (!isIconPlaceKind(tool.subtype)) return false;
|
|
1801
|
-
const rotCW = tool.upright && displayRotation ? (0, _embedpdf_core_annotation.uprightRotation)(displayRotation) : 0;
|
|
1802
|
-
if (tool.subtype === "text") return createIconAt(tool, pon, point, rotCW, null);
|
|
1803
|
-
return promptFileAt(tool, pon, point, (picked) => createIconAt(tool, pon, point, rotCW, picked));
|
|
1804
|
-
};
|
|
1805
|
-
/** The page a ref lives on: from the loaded model first (covers obj/nm refs
|
|
1806
|
-
* that don't carry a pon), else the ref itself (index refs do). */
|
|
1807
|
-
const ponForRef = (ref) => model().byId[require_seam.refKey(ref)]?.pon ?? (ref.kind === "index" ? ref.pageObjectNumber : null);
|
|
1808
|
-
/**
|
|
1809
|
-
* Re-sync one annotation into the model from the authoritative engine DTO,
|
|
1810
|
-
* with the render `source` the caller decides: `'vector'` when WE authored or
|
|
1811
|
-
* changed the appearance (create / restyle / resize), `'baked'` when the AP is
|
|
1812
|
-
* still authoritative (a move, which preserves it, or a remote edit).
|
|
1813
|
-
* `bumpAp` marks the upsert as confirming an engine /AP re-bake with new
|
|
1814
|
-
* content, so the annotation's `apVersion` — and with it the page's
|
|
1815
|
-
* appearance epoch — advances and the shell fetches the fresh raster.
|
|
1816
|
-
*/
|
|
1817
|
-
const syncDTO = (dto, source, bumpAp = false) => {
|
|
1818
|
-
const crop = cropOf(dto.pageObjectNumber);
|
|
1819
|
-
if (crop) apply({
|
|
1820
|
-
t: "upsert",
|
|
1821
|
-
annots: [ingestDTO(dto, crop, source)],
|
|
1822
|
-
bumpAp
|
|
1823
|
-
});
|
|
1824
|
-
};
|
|
1825
|
-
/** The one engine-update path, shared by `update` and `updateSelection`. A
|
|
1826
|
-
* programmatic patch changes the appearance → render live (vector). */
|
|
1827
|
-
const updateOne = async (ref, patch) => {
|
|
1828
|
-
const doc = ctx.doc;
|
|
1829
|
-
if (!doc) throw new Error("[annotation] no document bound");
|
|
1830
|
-
const pon = ponForRef(ref);
|
|
1831
|
-
if (pon == null) throw new Error("[annotation] cannot resolve page for ref");
|
|
1832
|
-
const res = await doc.page(pon).annotations.update(ref, patch);
|
|
1833
|
-
syncDTO(res.updated, "vector");
|
|
1834
|
-
};
|
|
1835
|
-
const commitText = (ref) => {
|
|
1836
|
-
const key = require_seam.refKey(ref);
|
|
1837
|
-
clearTimeout(textTimers.get(key));
|
|
1838
|
-
textTimers.delete(key);
|
|
1839
|
-
const a = model().byId[key];
|
|
1840
|
-
const pon = ponForRef(ref);
|
|
1841
|
-
if (!a || pon == null) return;
|
|
1842
|
-
const patch = textCommitPatch(a, richDocOf(a, fonts).paragraphs, fonts);
|
|
1843
|
-
ctx.doc?.page(pon).annotations.update(ref, {
|
|
1844
|
-
subtype: "free-text",
|
|
1845
|
-
...patch
|
|
1846
|
-
}).then(() => {}, () => {});
|
|
1847
|
-
};
|
|
1848
|
-
const scheduleTextCommit = (ref) => {
|
|
1849
|
-
const key = require_seam.refKey(ref);
|
|
1850
|
-
clearTimeout(textTimers.get(key));
|
|
1851
|
-
textTimers.set(key, setTimeout(() => commitText(ref), TEXT_COMMIT_DEBOUNCE_MS));
|
|
1852
|
-
};
|
|
1853
|
-
const flushTextCommits = () => {
|
|
1854
|
-
for (const key of [...textTimers.keys()]) {
|
|
1855
|
-
const a = model().byId[key];
|
|
1856
|
-
if (a?.ref) commitText(a.ref);
|
|
1857
|
-
else {
|
|
1858
|
-
clearTimeout(textTimers.get(key));
|
|
1859
|
-
textTimers.delete(key);
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
};
|
|
1863
|
-
function apply(msg) {
|
|
1864
|
-
const [next, effects] = (0, _embedpdf_core_annotation.update)(model(), msg);
|
|
1865
|
-
ctx.dispatch({
|
|
1866
|
-
type: "SET_MODEL",
|
|
1867
|
-
model: next
|
|
1868
|
-
});
|
|
1869
|
-
for (const fx of effects) perform(fx, next);
|
|
1870
|
-
}
|
|
1871
|
-
let hydrationStarted = false;
|
|
1872
|
-
let hydrationWindow = false;
|
|
1873
|
-
let hydrationRunning = null;
|
|
1874
|
-
let hydrateAgain = false;
|
|
1875
|
-
const pendingRemote = [];
|
|
1876
|
-
const setHydration = (hydration) => ctx.dispatch({
|
|
1877
|
-
type: "SET_HYDRATION",
|
|
1878
|
-
hydration
|
|
1879
|
-
});
|
|
1880
|
-
const ingestSnapshot = (snap, bumpAp) => {
|
|
1881
|
-
const annots = [];
|
|
1882
|
-
for (const page of snap.pages) {
|
|
1883
|
-
const crop = cropOf(page.pageState.pageObjectNumber);
|
|
1884
|
-
if (!crop) continue;
|
|
1885
|
-
annots.push(...page.annotations.map((d) => ingestDTO(d, crop)));
|
|
1886
|
-
}
|
|
1887
|
-
apply({
|
|
1888
|
-
t: "hydrated",
|
|
1889
|
-
annots,
|
|
1890
|
-
bumpAp
|
|
1891
|
-
});
|
|
1892
|
-
};
|
|
1893
|
-
const canRead = () => ctx.doc?.security.allows("doc.annotate.read") ?? true;
|
|
1894
|
-
const runHydration = (bumpAp) => {
|
|
1895
|
-
const doc = ctx.doc;
|
|
1896
|
-
if (!doc) return Promise.resolve();
|
|
1897
|
-
if (!canRead()) {
|
|
1898
|
-
setHydration({ status: "forbidden" });
|
|
1899
|
-
return Promise.resolve();
|
|
1900
|
-
}
|
|
1901
|
-
hydrationWindow = true;
|
|
1902
|
-
setHydration({ status: "loading" });
|
|
1903
|
-
const run = doc.annotations.listRawAll().then((snap) => {
|
|
1904
|
-
ingestSnapshot(snap, bumpAp);
|
|
1905
|
-
const head = snap.auditHead;
|
|
1906
|
-
const queued = pendingRemote.splice(0);
|
|
1907
|
-
hydrationWindow = false;
|
|
1908
|
-
for (const event of queued) {
|
|
1909
|
-
const serverId = "origin" in event ? event.origin.serverId : null;
|
|
1910
|
-
if (head !== void 0 && serverId !== null && serverId <= head) continue;
|
|
1911
|
-
applyRemoteAnnotationEvent(event);
|
|
1912
|
-
}
|
|
1913
|
-
setHydration({ status: "complete" });
|
|
1914
|
-
}).catch((error) => {
|
|
1915
|
-
hydrationWindow = false;
|
|
1916
|
-
for (const event of pendingRemote.splice(0)) applyRemoteAnnotationEvent(event);
|
|
1917
|
-
setHydration({
|
|
1918
|
-
status: "error",
|
|
1919
|
-
error
|
|
1920
|
-
});
|
|
1921
|
-
}).finally(() => {
|
|
1922
|
-
hydrationRunning = null;
|
|
1923
|
-
if (hydrateAgain) {
|
|
1924
|
-
hydrateAgain = false;
|
|
1925
|
-
hydrationRunning = runHydration(true);
|
|
1926
|
-
}
|
|
1927
|
-
});
|
|
1928
|
-
hydrationRunning = run;
|
|
1929
|
-
return run;
|
|
1930
|
-
};
|
|
1931
|
-
const rehydrate = () => {
|
|
1932
|
-
if (hydrationRunning) {
|
|
1933
|
-
hydrateAgain = true;
|
|
1934
|
-
return hydrationRunning;
|
|
1935
|
-
}
|
|
1936
|
-
hydrationRunning = runHydration(true);
|
|
1937
|
-
return hydrationRunning;
|
|
1938
|
-
};
|
|
1939
|
-
/**
|
|
1940
|
-
* Fold one REMOTE annotation event into the model (transplanted from the
|
|
1941
|
-
* effects layer so hydration can queue + replay through the same code
|
|
1942
|
-
* path). `bump` re-fetches rasters, `keep` doesn't — driven by the
|
|
1943
|
-
* ENGINE'S appearance echo riding the event, so a remote MOVE costs
|
|
1944
|
-
* peers zero re-renders while a remote restyle refreshes exactly once.
|
|
1945
|
-
*/
|
|
1946
|
-
const upsertRemote = (dtos, bumpAp) => {
|
|
1947
|
-
const bump = [];
|
|
1948
|
-
const keep = [];
|
|
1949
|
-
for (const dto of dtos) {
|
|
1950
|
-
const crop = cropOf(dto.pageObjectNumber);
|
|
1951
|
-
if (!crop) continue;
|
|
1952
|
-
const a = ingestDTO(dto, crop, "baked");
|
|
1953
|
-
(bumpAp && !(0, _embedpdf_core_annotation.isSubstrateOnly)(a) ? bump : keep).push(a);
|
|
1954
|
-
}
|
|
1955
|
-
if (bump.length) apply({
|
|
1956
|
-
t: "upsert",
|
|
1957
|
-
annots: bump,
|
|
1958
|
-
bumpAp: true
|
|
1959
|
-
});
|
|
1960
|
-
if (keep.length) apply({
|
|
1961
|
-
t: "upsert",
|
|
1962
|
-
annots: keep
|
|
1963
|
-
});
|
|
1964
|
-
};
|
|
1965
|
-
const applyRemoteAnnotationEvent = (event) => {
|
|
1966
|
-
switch (event.type) {
|
|
1967
|
-
case "annotation.created":
|
|
1968
|
-
upsertRemote([event.created], true);
|
|
1969
|
-
break;
|
|
1970
|
-
case "annotation.updated":
|
|
1971
|
-
upsertRemote([event.updated], event.appearance.changed);
|
|
1972
|
-
break;
|
|
1973
|
-
case "annotation.moved":
|
|
1974
|
-
upsertRemote(event.moved, false);
|
|
1975
|
-
break;
|
|
1976
|
-
case "annotation.deleted":
|
|
1977
|
-
if (event.deleted) {
|
|
1978
|
-
const key = (0, _embedpdf_engine_core_runtime.encodeStableIdKey)(event.deleted);
|
|
1979
|
-
if (model().byId[key]) apply({
|
|
1980
|
-
t: "remove",
|
|
1981
|
-
ids: [key]
|
|
1982
|
-
});
|
|
1983
|
-
}
|
|
1984
|
-
break;
|
|
1985
|
-
default: break;
|
|
1986
|
-
}
|
|
1987
|
-
};
|
|
1988
|
-
const currentUserId = () => ctx.doc?.security.identity?.user_id;
|
|
1989
|
-
let threadsMemo = null;
|
|
1990
|
-
/** Value-stable layout key: display order is all the sort consumes, so
|
|
1991
|
-
* the memo must survive hosts that rebuild the pages array per read. */
|
|
1992
|
-
const layoutSignature = () => (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber).join(",");
|
|
1993
|
-
const computeThreadsIndex = () => {
|
|
1994
|
-
const m = model();
|
|
1995
|
-
const dtos = [];
|
|
1996
|
-
for (const id of m.order) {
|
|
1997
|
-
const data = m.byId[id]?.data;
|
|
1998
|
-
if (data) dtos.push(data);
|
|
1999
|
-
}
|
|
2000
|
-
const threads = (0, _embedpdf_engine_core_runtime.buildCommentThreads)(dtos, { currentUserId: currentUserId() });
|
|
2001
|
-
const pages = ctx.document()?.pages ?? [];
|
|
2002
|
-
const displayIndex = new Map(pages.map((p, i) => [p.pageObjectNumber, i]));
|
|
2003
|
-
threads.sort((a, b) => {
|
|
2004
|
-
const pa = displayIndex.get(a.pageObjectNumber) ?? Number.MAX_SAFE_INTEGER;
|
|
2005
|
-
const pb = displayIndex.get(b.pageObjectNumber) ?? Number.MAX_SAFE_INTEGER;
|
|
2006
|
-
if (pa !== pb) return pa - pb;
|
|
2007
|
-
if (a.root.rect.top !== b.root.rect.top) return b.root.rect.top - a.root.rect.top;
|
|
2008
|
-
const ca = a.root.created ?? "";
|
|
2009
|
-
const cb = b.root.created ?? "";
|
|
2010
|
-
return ca < cb ? -1 : ca > cb ? 1 : 0;
|
|
2011
|
-
});
|
|
2012
|
-
const byMember = /* @__PURE__ */ new Map();
|
|
2013
|
-
for (const t of threads) {
|
|
2014
|
-
byMember.set(require_seam.refKey(t.root.ref), t);
|
|
2015
|
-
for (const r of t.replies) byMember.set(require_seam.refKey(r.ref), t);
|
|
2016
|
-
for (const g of t.groupedParts) byMember.set(require_seam.refKey(g.ref), t);
|
|
2017
|
-
for (const s of t.review.statusRefs) byMember.set(require_seam.refKey(s), t);
|
|
2018
|
-
}
|
|
2019
|
-
return {
|
|
2020
|
-
threads,
|
|
2021
|
-
byMember
|
|
2022
|
-
};
|
|
2023
|
-
};
|
|
2024
|
-
const threadsIndex = () => {
|
|
2025
|
-
const m = model();
|
|
2026
|
-
const layout = layoutSignature();
|
|
2027
|
-
const userId = currentUserId();
|
|
2028
|
-
if (threadsMemo && threadsMemo.byId === m.byId && threadsMemo.order === m.order && threadsMemo.layout === layout && threadsMemo.userId === userId) return threadsMemo;
|
|
2029
|
-
threadsMemo = {
|
|
2030
|
-
...computeThreadsIndex(),
|
|
2031
|
-
byId: m.byId,
|
|
2032
|
-
order: m.order,
|
|
2033
|
-
layout,
|
|
2034
|
-
userId
|
|
2035
|
-
};
|
|
2036
|
-
return threadsMemo;
|
|
2037
|
-
};
|
|
2038
|
-
const threadOf = (ref) => {
|
|
2039
|
-
const t = threadsIndex().byMember.get(require_seam.refKey(ref));
|
|
2040
|
-
if (!t) throw new Error("[annotation] no comment thread contains this ref");
|
|
2041
|
-
return t;
|
|
2042
|
-
};
|
|
2043
|
-
/** Engine create + model sync for conversation-plane annotations (they
|
|
2044
|
-
* never paint, so the render source is immaterial — 'baked' avoids any
|
|
2045
|
-
* vector-scene work). */
|
|
2046
|
-
const createConversationAnnot = async (pon, draft) => {
|
|
2047
|
-
const doc = ctx.doc;
|
|
2048
|
-
if (!doc) throw new Error("[annotation] no document bound");
|
|
2049
|
-
const res = await doc.page(pon).annotations.create(draft);
|
|
2050
|
-
syncDTO(res.created, "baked");
|
|
2051
|
-
return res.created;
|
|
2052
|
-
};
|
|
2053
|
-
const deleteOne = async (ref) => {
|
|
2054
|
-
const doc = ctx.doc;
|
|
2055
|
-
if (!doc) throw new Error("[annotation] no document bound");
|
|
2056
|
-
await doc.page(ref.pageObjectNumber).annotations.delete(ref);
|
|
2057
|
-
apply({
|
|
2058
|
-
t: "remove",
|
|
2059
|
-
ids: [require_seam.refKey(ref)]
|
|
2060
|
-
});
|
|
2061
|
-
};
|
|
2062
|
-
const REPLY_FLAGS = {
|
|
2063
|
-
print: true,
|
|
2064
|
-
noZoom: true,
|
|
2065
|
-
noRotate: true
|
|
2066
|
-
};
|
|
2067
|
-
const STATUS_FLAGS = {
|
|
2068
|
-
hidden: true,
|
|
2069
|
-
noZoom: true,
|
|
2070
|
-
noRotate: true
|
|
2071
|
-
};
|
|
2072
|
-
/** Coarse authority mirror (doc.annotate.modify) — B3 replaces this with
|
|
2073
|
-
* the collab-scope mirrors; the FLAG split is already the real rule. */
|
|
2074
|
-
const mutationTarget = (ref) => {
|
|
2075
|
-
const d = model().byId[require_seam.refKey(ref)]?.data;
|
|
2076
|
-
return {
|
|
2077
|
-
...d?.userId !== void 0 ? { userId: d.userId } : {},
|
|
2078
|
-
...d?.groupId !== void 0 ? { groupId: d.groupId } : {}
|
|
2079
|
-
};
|
|
2080
|
-
};
|
|
2081
|
-
const allowsCreate = () => ctx.doc?.security.allowsAnnotationCreate() ?? false;
|
|
2082
|
-
/**
|
|
2083
|
-
* Ingest an engine DTO with this session's per-record AUTHORITY projected
|
|
2084
|
-
* onto it (permissions.md) — asked of the SAME mirrors the server enforces
|
|
2085
|
-
* with, against the record's stamped owner. Fused into
|
|
2086
|
-
* `annotTransformable`/`annotDeletable`, so hit-test, chrome, props and
|
|
2087
|
-
* delete all agree with the engine by construction. No security context
|
|
2088
|
-
* (bare local engines, tests) → unstamped → allowed; the engine enforces.
|
|
2089
|
-
*/
|
|
2090
|
-
const ingestDTO = (dto, crop, source) => {
|
|
2091
|
-
const a = fromDTO(dto, crop, source);
|
|
2092
|
-
const sec = ctx.doc?.security;
|
|
2093
|
-
if (!sec) return a;
|
|
2094
|
-
const target = {
|
|
2095
|
-
...dto.userId !== void 0 ? { userId: dto.userId } : {},
|
|
2096
|
-
...dto.groupId !== void 0 ? { groupId: dto.groupId } : {}
|
|
2097
|
-
};
|
|
2098
|
-
return {
|
|
2099
|
-
...a,
|
|
2100
|
-
authority: {
|
|
2101
|
-
update: sec.allowsAnnotationMutation("update", target),
|
|
2102
|
-
delete: sec.allowsAnnotationMutation("delete", target)
|
|
2103
|
-
}
|
|
2104
|
-
};
|
|
2105
|
-
};
|
|
2106
|
-
const allowsMutation = (action, ref) => ctx.doc?.security.allowsAnnotationMutation(action, mutationTarget(ref)) ?? false;
|
|
2107
|
-
const deletableOne = (ref) => {
|
|
2108
|
-
const a = model().byId[require_seam.refKey(ref)];
|
|
2109
|
-
return !!a && (0, _embedpdf_core_annotation.annotDeletable)(a);
|
|
2110
|
-
};
|
|
2111
|
-
const threadMemberRefs = (t) => [
|
|
2112
|
-
...t.replies.map((r) => r.ref),
|
|
2113
|
-
...t.groupedParts.map((g) => g.ref),
|
|
2114
|
-
...t.review.statusRefs,
|
|
2115
|
-
t.root.ref
|
|
2116
|
-
];
|
|
2117
|
-
const comments = {
|
|
2118
|
-
threads: () => threadsIndex().threads,
|
|
2119
|
-
thread: (ref) => threadsIndex().byMember.get(require_seam.refKey(ref)) ?? null,
|
|
2120
|
-
hydration: () => ctx.getState().hydration,
|
|
2121
|
-
rehydrate,
|
|
2122
|
-
reply: async (ref, text) => {
|
|
2123
|
-
const t = threadOf(ref);
|
|
2124
|
-
return (await createConversationAnnot(t.pageObjectNumber, {
|
|
2125
|
-
subtype: "text",
|
|
2126
|
-
rect: t.root.rect,
|
|
2127
|
-
icon: "comment",
|
|
2128
|
-
contents: text,
|
|
2129
|
-
inReplyTo: t.root.ref,
|
|
2130
|
-
flags: { ...REPLY_FLAGS }
|
|
2131
|
-
})).ref;
|
|
2132
|
-
},
|
|
2133
|
-
edit: async (ref, text) => {
|
|
2134
|
-
const subtype = model().byId[require_seam.refKey(ref)]?.data?.subtype;
|
|
2135
|
-
if (!subtype) throw new Error("[annotation] cannot edit an uncommitted annotation");
|
|
2136
|
-
await updateOne(ref, {
|
|
2137
|
-
subtype,
|
|
2138
|
-
contents: text
|
|
2139
|
-
});
|
|
2140
|
-
},
|
|
2141
|
-
setStatus: async (ref, state) => {
|
|
2142
|
-
const t = threadOf(ref);
|
|
2143
|
-
const userId = currentUserId();
|
|
2144
|
-
const previous = userId ? t.review.byReviewer[userId] : void 0;
|
|
2145
|
-
await createConversationAnnot(t.pageObjectNumber, {
|
|
2146
|
-
subtype: "text",
|
|
2147
|
-
rect: t.root.rect,
|
|
2148
|
-
inReplyTo: previous?.ref ?? t.root.ref,
|
|
2149
|
-
state,
|
|
2150
|
-
stateModel: "review",
|
|
2151
|
-
flags: { ...STATUS_FLAGS }
|
|
2152
|
-
});
|
|
2153
|
-
},
|
|
2154
|
-
setMarked: async (ref, marked) => {
|
|
2155
|
-
const t = threadOf(ref);
|
|
2156
|
-
await createConversationAnnot(t.pageObjectNumber, {
|
|
2157
|
-
subtype: "text",
|
|
2158
|
-
rect: t.root.rect,
|
|
2159
|
-
inReplyTo: t.root.ref,
|
|
2160
|
-
state: marked ? "marked" : "unmarked",
|
|
2161
|
-
stateModel: "marked",
|
|
2162
|
-
flags: { ...STATUS_FLAGS }
|
|
2163
|
-
});
|
|
2164
|
-
},
|
|
2165
|
-
remove: deleteOne,
|
|
2166
|
-
removeThread: async (ref) => {
|
|
2167
|
-
const t = threadOf(ref);
|
|
2168
|
-
const members = threadMemberRefs(t);
|
|
2169
|
-
const blocked = members.filter((r) => !deletableOne(r));
|
|
2170
|
-
if (blocked.length > 0) return {
|
|
2171
|
-
deleted: [],
|
|
2172
|
-
failed: blocked.map((r) => ({
|
|
2173
|
-
ref: r,
|
|
2174
|
-
error: /* @__PURE__ */ new Error("delete not permitted")
|
|
2175
|
-
}))
|
|
2176
|
-
};
|
|
2177
|
-
const deleted = [];
|
|
2178
|
-
const failed = [];
|
|
2179
|
-
for (const member of members) {
|
|
2180
|
-
if (failed.length > 0) break;
|
|
2181
|
-
try {
|
|
2182
|
-
await deleteOne(member);
|
|
2183
|
-
deleted.push(member);
|
|
2184
|
-
} catch (error) {
|
|
2185
|
-
failed.push({
|
|
2186
|
-
ref: member,
|
|
2187
|
-
error
|
|
2188
|
-
});
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
return {
|
|
2192
|
-
deleted,
|
|
2193
|
-
failed
|
|
2194
|
-
};
|
|
2195
|
-
},
|
|
2196
|
-
permissionsFor: (ref) => {
|
|
2197
|
-
model().byId[require_seam.refKey(ref)]?.flags;
|
|
2198
|
-
const t = threadsIndex().byMember.get(require_seam.refKey(ref)) ?? null;
|
|
2199
|
-
return {
|
|
2200
|
-
canReply: allowsCreate(),
|
|
2201
|
-
canSetStatus: allowsCreate(),
|
|
2202
|
-
canEditText: (() => {
|
|
2203
|
-
const a = model().byId[require_seam.refKey(ref)];
|
|
2204
|
-
return !!a && (0, _embedpdf_core_annotation.annotContentsEditable)(a);
|
|
2205
|
-
})(),
|
|
2206
|
-
canDelete: deletableOne(ref),
|
|
2207
|
-
canDeleteThread: t !== null && threadMemberRefs(t).every(deletableOne)
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
};
|
|
2211
|
-
/**
|
|
2212
|
-
* Per-parent serialization of attached-link reconciles: rapid edits chain
|
|
2213
|
-
* instead of interleaving (two overlapping runs could double-create
|
|
2214
|
-
* children). Each run reads the CURRENT model at execution time, so a
|
|
2215
|
-
* chained run converges on the latest desired state.
|
|
2216
|
-
*/
|
|
2217
|
-
const linkSyncChains = /* @__PURE__ */ new Map();
|
|
2218
|
-
/**
|
|
2219
|
-
* Queue a reconcile. `intent` is either an explicit target (a set/clear —
|
|
2220
|
-
* captured by THIS run's closure, so chained sets stay latest-wins) or
|
|
2221
|
-
* `'keep'` (a geometry follow: re-rect the children toward whatever target
|
|
2222
|
-
* the substrate holds AT RUN TIME — so a remote retarget is never undone
|
|
2223
|
-
* by a local move). Returns the chain, so `links.set()` can await commit.
|
|
2224
|
-
*/
|
|
2225
|
-
const scheduleLinkSync = (id, intent) => {
|
|
2226
|
-
const next = (linkSyncChains.get(id) ?? Promise.resolve()).then(() => reconcileLinkChildren(id, intent === "keep" ? (0, _embedpdf_core_annotation.linkOf)(model(), id) : intent.target));
|
|
2227
|
-
linkSyncChains.set(id, next);
|
|
2228
|
-
next.finally(() => {
|
|
2229
|
-
if (linkSyncChains.get(id) === next) linkSyncChains.delete(id);
|
|
2230
|
-
});
|
|
2231
|
-
return next;
|
|
2232
|
-
};
|
|
2233
|
-
/**
|
|
2234
|
-
* THE one place attached link children are created, retargeted, re-rected,
|
|
2235
|
-
* or deleted. Declarative: desired state = `desired` target + the parent's
|
|
2236
|
-
* committed geometry (`linkChildRects`); CURRENT state is read straight
|
|
2237
|
-
* from the substrate (`linkChildrenOf`) — no join-key ledger. Results land
|
|
2238
|
-
* as ordinary substrate upserts/removes, so the `linkOf` lens converges
|
|
2239
|
-
* immediately locally and via events everywhere else. Idempotent — foreign
|
|
2240
|
-
* inconsistencies heal on the next local edit.
|
|
2241
|
-
*/
|
|
2242
|
-
const reconcileLinkChildren = async (id, desired) => {
|
|
2243
|
-
const doc = ctx.doc;
|
|
2244
|
-
const a = model().byId[id];
|
|
2245
|
-
if (!doc || !a || !a.ref || a.subtype === "link") return;
|
|
2246
|
-
const crop = cropOf(a.pon);
|
|
2247
|
-
if (!crop) return;
|
|
2248
|
-
const page = doc.page(a.pon);
|
|
2249
|
-
const target = require_seam.writableTarget(desired);
|
|
2250
|
-
const rects = desired == null ? [] : linkChildRects(a).map((r) => (0, _embedpdf_core_annotation.contentToPdfRect)(r, crop));
|
|
2251
|
-
const current = (0, _embedpdf_core_annotation.linkChildrenOf)(model(), id);
|
|
2252
|
-
try {
|
|
2253
|
-
const paired = Math.min(current.length, rects.length);
|
|
2254
|
-
for (let i = 0; i < paired; i++) {
|
|
2255
|
-
const ref = current[i].ref;
|
|
2256
|
-
if (!ref) continue;
|
|
2257
|
-
const res = await page.annotations.update(ref, {
|
|
2258
|
-
subtype: "link",
|
|
2259
|
-
rect: rects[i],
|
|
2260
|
-
...target ? { target } : {}
|
|
2261
|
-
});
|
|
2262
|
-
apply({
|
|
2263
|
-
t: "upsert",
|
|
2264
|
-
annots: [ingestDTO(res.updated, crop, "baked")]
|
|
2265
|
-
});
|
|
2266
|
-
}
|
|
2267
|
-
for (let i = current.length; i < rects.length; i++) {
|
|
2268
|
-
const res = await page.annotations.create({
|
|
2269
|
-
subtype: "link",
|
|
2270
|
-
rect: rects[i],
|
|
2271
|
-
target,
|
|
2272
|
-
inReplyTo: a.ref,
|
|
2273
|
-
replyType: "group"
|
|
2274
|
-
});
|
|
2275
|
-
apply({
|
|
2276
|
-
t: "upsert",
|
|
2277
|
-
annots: [ingestDTO(res.created, crop, "baked")]
|
|
2278
|
-
});
|
|
2279
|
-
}
|
|
2280
|
-
for (let i = rects.length; i < current.length; i++) {
|
|
2281
|
-
const child = current[i];
|
|
2282
|
-
if (child.ref) await page.annotations.delete(child.ref);
|
|
2283
|
-
apply({
|
|
2284
|
-
t: "remove",
|
|
2285
|
-
ids: [child.id]
|
|
2286
|
-
});
|
|
2287
|
-
}
|
|
2288
|
-
} catch (err) {
|
|
2289
|
-
console.error("[annotation] attached-link sync failed:", err);
|
|
2290
|
-
}
|
|
2291
|
-
};
|
|
2292
|
-
/** A relationship-only engine patch (sets/clears `/IRT` + `/RT`) — geometry and
|
|
2293
|
-
* style are left untouched, so grouping never re-bakes an appearance. */
|
|
2294
|
-
const relationshipPatch = (subtype, rel) => ({
|
|
2295
|
-
subtype,
|
|
2296
|
-
...rel
|
|
2297
|
-
});
|
|
2298
|
-
/** Write a relationship change to one committed annotation and re-sync it from
|
|
2299
|
-
* the authoritative DTO (preserving its render source — relationships don't
|
|
2300
|
-
* change the appearance). */
|
|
2301
|
-
const writeRelationship = async (a, rel) => {
|
|
2302
|
-
const doc = ctx.doc;
|
|
2303
|
-
if (!doc || !a.ref || !a.data) return;
|
|
2304
|
-
const res = await doc.page(a.pon).annotations.update(a.ref, relationshipPatch(a.data.subtype, rel));
|
|
2305
|
-
syncDTO(res.updated, a.source);
|
|
2306
|
-
};
|
|
2307
|
-
/** Committed, data-backed annotations in the current selection. */
|
|
2308
|
-
const selectedCommitted = () => {
|
|
2309
|
-
const m = model();
|
|
2310
|
-
return m.selected.map((id) => m.byId[id]).filter((a) => !!a && !!a.ref && !!a.data);
|
|
2311
|
-
};
|
|
2312
|
-
function perform(fx, m) {
|
|
2313
|
-
const doc = ctx.doc;
|
|
2314
|
-
if (!doc) return;
|
|
2315
|
-
if (fx.fx === "createGroup") {
|
|
2316
|
-
const ids = [fx.primary, ...fx.members];
|
|
2317
|
-
const annots = ids.map((id) => m.byId[id]);
|
|
2318
|
-
const primary = annots[0];
|
|
2319
|
-
if (!primary || annots.some((a) => !a || a.pon !== primary.pon)) {
|
|
2320
|
-
apply({
|
|
2321
|
-
t: "remove",
|
|
2322
|
-
ids
|
|
2323
|
-
});
|
|
2324
|
-
return;
|
|
2325
|
-
}
|
|
2326
|
-
const crop = cropOf(primary.pon);
|
|
2327
|
-
const drafts = crop ? annots.map((a) => a ? toCreateDraft(a, crop) : null) : annots.map(() => null);
|
|
2328
|
-
if (drafts.some((d) => !d)) {
|
|
2329
|
-
apply({
|
|
2330
|
-
t: "remove",
|
|
2331
|
-
ids
|
|
2332
|
-
});
|
|
2333
|
-
return;
|
|
2334
|
-
}
|
|
2335
|
-
(async () => {
|
|
2336
|
-
const committed = [];
|
|
2337
|
-
try {
|
|
2338
|
-
const page = doc.page(primary.pon);
|
|
2339
|
-
const primaryResult = await page.annotations.create(drafts[0]);
|
|
2340
|
-
committed.push({
|
|
2341
|
-
tempId: fx.primary,
|
|
2342
|
-
ref: primaryResult.created.ref
|
|
2343
|
-
});
|
|
2344
|
-
apply({
|
|
2345
|
-
t: "created",
|
|
2346
|
-
tempId: fx.primary,
|
|
2347
|
-
id: require_seam.refKey(primaryResult.created.ref),
|
|
2348
|
-
ref: primaryResult.created.ref
|
|
2349
|
-
});
|
|
2350
|
-
syncDTO(primaryResult.created, "vector");
|
|
2351
|
-
for (let i = 0; i < fx.members.length; i++) {
|
|
2352
|
-
const tempId = fx.members[i];
|
|
2353
|
-
const draft = {
|
|
2354
|
-
...drafts[i + 1],
|
|
2355
|
-
inReplyTo: primaryResult.created.ref,
|
|
2356
|
-
replyType: "group"
|
|
2357
|
-
};
|
|
2358
|
-
const result = await page.annotations.create(draft);
|
|
2359
|
-
committed.push({
|
|
2360
|
-
tempId,
|
|
2361
|
-
ref: result.created.ref
|
|
2362
|
-
});
|
|
2363
|
-
apply({
|
|
2364
|
-
t: "created",
|
|
2365
|
-
tempId,
|
|
2366
|
-
id: require_seam.refKey(result.created.ref),
|
|
2367
|
-
ref: result.created.ref
|
|
2368
|
-
});
|
|
2369
|
-
syncDTO(result.created, "vector");
|
|
2370
|
-
}
|
|
2371
|
-
} catch (error) {
|
|
2372
|
-
const removeIds = ids.filter((id) => !committed.some((c) => c.tempId === id));
|
|
2373
|
-
for (const part of [...committed].reverse()) try {
|
|
2374
|
-
await doc.page(primary.pon).annotations.delete(part.ref);
|
|
2375
|
-
removeIds.push(require_seam.refKey(part.ref));
|
|
2376
|
-
} catch {}
|
|
2377
|
-
if (removeIds.length) apply({
|
|
2378
|
-
t: "remove",
|
|
2379
|
-
ids: removeIds
|
|
2380
|
-
});
|
|
2381
|
-
console.error("[annotation] grouped annotation creation failed:", error);
|
|
2382
|
-
}
|
|
2383
|
-
})();
|
|
2384
|
-
} else if (fx.fx === "create") {
|
|
2385
|
-
const a = m.byId[fx.id];
|
|
2386
|
-
const crop = a && cropOf(a.pon);
|
|
2387
|
-
const draft = a && crop ? toCreateDraft(a, crop) : null;
|
|
2388
|
-
if (!a || !draft) return;
|
|
2389
|
-
doc.page(a.pon).annotations.create(draft).then((res) => {
|
|
2390
|
-
apply({
|
|
2391
|
-
t: "created",
|
|
2392
|
-
tempId: fx.id,
|
|
2393
|
-
id: require_seam.refKey(res.created.ref),
|
|
2394
|
-
ref: res.created.ref
|
|
2395
|
-
});
|
|
2396
|
-
syncDTO(res.created, "vector");
|
|
2397
|
-
}, () => apply({
|
|
2398
|
-
t: "createFailed",
|
|
2399
|
-
tempId: fx.id
|
|
2400
|
-
}));
|
|
2401
|
-
} else if (fx.fx === "flags") {
|
|
2402
|
-
const a = m.byId[fx.id];
|
|
2403
|
-
if (!a || !a.ref || !a.data) return;
|
|
2404
|
-
doc.page(a.pon).annotations.update(a.ref, {
|
|
2405
|
-
subtype: a.data.subtype,
|
|
2406
|
-
flags: a.flags
|
|
2407
|
-
}).then((res) => syncDTO(res.updated, a.source), (err) => console.error("[annotation] flags write failed:", err));
|
|
2408
|
-
} else if (fx.fx === "patch") {
|
|
2409
|
-
const a = m.byId[fx.id];
|
|
2410
|
-
const crop = a && cropOf(a.pon);
|
|
2411
|
-
const patch = a && a.ref && crop ? toScopedPatch(a, fx.scope, crop) : null;
|
|
2412
|
-
if (!a || !a.ref || !patch) return;
|
|
2413
|
-
doc.page(a.pon).annotations.update(a.ref, patch).then((res) => {
|
|
2414
|
-
syncDTO(res.updated, a.source, res.appearance.changed);
|
|
2415
|
-
if ((0, _embedpdf_core_annotation.linkChildrenOf)(model(), fx.id).length) scheduleLinkSync(fx.id, "keep");
|
|
2416
|
-
}, () => {
|
|
2417
|
-
if (a.data && crop) apply({
|
|
2418
|
-
t: "upsert",
|
|
2419
|
-
annots: [ingestDTO(a.data, crop, a.source)]
|
|
2420
|
-
});
|
|
2421
|
-
});
|
|
2422
|
-
} else if (fx.fx === "syncLink") scheduleLinkSync(fx.id, { target: fx.target });
|
|
2423
|
-
else {
|
|
2424
|
-
require_seam.refKey(fx.ref);
|
|
2425
|
-
doc.page(fx.ref.pageObjectNumber).annotations.delete(fx.ref).then(() => {}, () => {});
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
const reloadPageNow = async (pon) => {
|
|
2429
|
-
const doc = ctx.doc;
|
|
2430
|
-
const crop = cropOf(pon);
|
|
2431
|
-
if (!doc || !crop) return;
|
|
2432
|
-
try {
|
|
2433
|
-
const snap = await doc.page(pon).annotations.list();
|
|
2434
|
-
const m = model();
|
|
2435
|
-
const stale = m.order.filter((id) => m.byId[id]?.pon === pon);
|
|
2436
|
-
if (stale.length) apply({
|
|
2437
|
-
t: "remove",
|
|
2438
|
-
ids: stale
|
|
2439
|
-
});
|
|
2440
|
-
apply({
|
|
2441
|
-
t: "loaded",
|
|
2442
|
-
annots: snap.annotations.map((d) => ingestDTO(d, crop))
|
|
2443
|
-
});
|
|
2444
|
-
} catch {}
|
|
2445
|
-
};
|
|
2446
|
-
/** Script patch → the engine's per-kind patch vocabulary. Colors cross the
|
|
2447
|
-
* Acrobat-array → engine {r,g,b}/255 boundary here; everything else maps
|
|
2448
|
-
* one-to-one (the VM's validity matrix already scoped keys per kind). */
|
|
2449
|
-
const engineScriptPatch = (subtype, patch) => {
|
|
2450
|
-
const out = { subtype };
|
|
2451
|
-
const toEngineColor = (color) => {
|
|
2452
|
-
const rgb = (0, _embedpdf_core_acrojs.scriptColorToRgb)(color);
|
|
2453
|
-
return rgb ? {
|
|
2454
|
-
r: Math.round(rgb.r * 255),
|
|
2455
|
-
g: Math.round(rgb.g * 255),
|
|
2456
|
-
b: Math.round(rgb.b * 255)
|
|
2457
|
-
} : null;
|
|
2458
|
-
};
|
|
2459
|
-
if (patch.strokeColor) {
|
|
2460
|
-
const color = toEngineColor(patch.strokeColor);
|
|
2461
|
-
if (!color) return /* @__PURE__ */ new Error("transparent stroke colours are not supported");
|
|
2462
|
-
out.color = color;
|
|
2463
|
-
}
|
|
2464
|
-
if (patch.fillColor) out.interiorColor = toEngineColor(patch.fillColor);
|
|
2465
|
-
if (patch.opacity !== void 0) out.opacity = patch.opacity;
|
|
2466
|
-
if (patch.width !== void 0) out.strokeWidth = patch.width;
|
|
2467
|
-
if (patch.borderStyle) out.borderStyle = patch.borderStyle === "D" ? "dashed" : "solid";
|
|
2468
|
-
if (patch.dash) out.dashArray = patch.dash;
|
|
2469
|
-
if (patch.rect) out.rect = {
|
|
2470
|
-
left: patch.rect[0],
|
|
2471
|
-
bottom: patch.rect[1],
|
|
2472
|
-
right: patch.rect[2],
|
|
2473
|
-
top: patch.rect[3]
|
|
2474
|
-
};
|
|
2475
|
-
if (patch.contents !== void 0) out.contents = patch.contents;
|
|
2476
|
-
if (patch.flags) out.flags = patch.flags;
|
|
2477
|
-
return out;
|
|
2478
|
-
};
|
|
2479
|
-
const updateSelection = (patch) => {
|
|
2480
|
-
const m = model();
|
|
2481
|
-
const range = activeTextRange(m);
|
|
2482
|
-
if (range) {
|
|
2483
|
-
const a = m.byId[range.id];
|
|
2484
|
-
const { delta, rest } = runDeltaForProps(patch, fonts);
|
|
2485
|
-
if (Object.keys(delta).length) {
|
|
2486
|
-
const next = (0, _embedpdf_core_annotation.applyStyleToRange)({ paragraphs: richDocOf(a, fonts).paragraphs }, range, delta);
|
|
2487
|
-
apply({
|
|
2488
|
-
t: "setRichText",
|
|
2489
|
-
id: range.id,
|
|
2490
|
-
doc: next
|
|
2491
|
-
});
|
|
2492
|
-
if (a.ref) scheduleTextCommit(a.ref);
|
|
2493
|
-
}
|
|
2494
|
-
if (Object.keys(rest).length) {
|
|
2495
|
-
flushTextCommits();
|
|
2496
|
-
apply({
|
|
2497
|
-
t: "setProps",
|
|
2498
|
-
patch: rest
|
|
2499
|
-
});
|
|
2500
|
-
}
|
|
2501
|
-
return;
|
|
2502
|
-
}
|
|
2503
|
-
if (m.editing) flushTextCommits();
|
|
2504
|
-
apply({
|
|
2505
|
-
t: "setProps",
|
|
2506
|
-
patch
|
|
2507
|
-
});
|
|
2508
|
-
};
|
|
2509
|
-
return {
|
|
2510
|
-
create: async (pon, draft) => {
|
|
2511
|
-
const doc = ctx.doc;
|
|
2512
|
-
if (!doc) throw new Error("[annotation] no document bound");
|
|
2513
|
-
const withFlags = draft.flags ? draft : {
|
|
2514
|
-
...draft,
|
|
2515
|
-
flags: { print: true }
|
|
2516
|
-
};
|
|
2517
|
-
const res = await doc.page(pon).annotations.create(withFlags);
|
|
2518
|
-
syncDTO(res.created, res.created.subtype === "stamp" ? "baked" : "vector");
|
|
2519
|
-
return res.created.ref;
|
|
2520
|
-
},
|
|
2521
|
-
armStamp,
|
|
2522
|
-
placeStamp,
|
|
2523
|
-
disarmStamp,
|
|
2524
|
-
placeArmedStamp,
|
|
2525
|
-
requestStampAt,
|
|
2526
|
-
placeAt,
|
|
2527
|
-
hasArmedStamp: () => armedStamp != null,
|
|
2528
|
-
ghostHoverAt,
|
|
2529
|
-
clearGhost,
|
|
2530
|
-
setPlacementPreview,
|
|
2531
|
-
clearPlacementPreview: clearGhost,
|
|
2532
|
-
toolGhost: (pon) => {
|
|
2533
|
-
const g = ctx.getState().toolGhost;
|
|
2534
|
-
return g && g.pon === pon ? g : null;
|
|
2535
|
-
},
|
|
2536
|
-
armedStampPreview: (devicePixelWidth) => {
|
|
2537
|
-
const armed = armedStamp;
|
|
2538
|
-
if (!armed?.preview) return Promise.resolve(null);
|
|
2539
|
-
const bucket = require_types.previewBucket(devicePixelWidth ?? 0);
|
|
2540
|
-
let pending = armed.previewCache.get(bucket);
|
|
2541
|
-
if (!pending) {
|
|
2542
|
-
pending = armed.preview(bucket).catch((error) => {
|
|
2543
|
-
console.error("[annotation] stamp ghost preview failed:", error);
|
|
2544
|
-
return null;
|
|
2545
|
-
});
|
|
2546
|
-
armed.previewCache.set(bucket, pending);
|
|
2547
|
-
}
|
|
2548
|
-
return pending;
|
|
2549
|
-
},
|
|
2550
|
-
stampArmEpoch: () => ctx.getState().stampArmEpoch,
|
|
2551
|
-
setFilePickerProvider: (provider) => {
|
|
2552
|
-
filePickerProvider = provider;
|
|
2553
|
-
},
|
|
2554
|
-
downloadAttachment: async (ref) => {
|
|
2555
|
-
const doc = ctx.doc;
|
|
2556
|
-
if (!doc) throw new Error("[annotation] no document bound");
|
|
2557
|
-
const pon = ponForRef(ref);
|
|
2558
|
-
if (pon == null) throw new Error("[annotation] cannot resolve page for ref");
|
|
2559
|
-
const annotations = doc.page(pon).annotations;
|
|
2560
|
-
if (!annotations.downloadFile) throw new Error("[annotation] this engine does not support attachment download");
|
|
2561
|
-
return annotations.downloadFile(ref);
|
|
2562
|
-
},
|
|
2563
|
-
tools: () => [...registry.values()],
|
|
2564
|
-
tool: (id) => registry.get(id) ?? null,
|
|
2565
|
-
toolSubtype: (id) => registry.get(id)?.subtype ?? id,
|
|
2566
|
-
registerTool: (def) => {
|
|
2567
|
-
const resolved = require_tools.buildToolRegistry([...configTools, def]).get(def.id);
|
|
2568
|
-
if (!resolved) throw new Error(`[annotation] could not resolve tool '${def.id}'`);
|
|
2569
|
-
registry.set(resolved.id, resolved);
|
|
2570
|
-
const un = ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.registerTool({
|
|
2571
|
-
id: resolved.id,
|
|
2572
|
-
cursor: resolved.cursor,
|
|
2573
|
-
enables: resolved.enables,
|
|
2574
|
-
touchDirect: require_tools.isTouchDirect(resolved.enables)
|
|
2575
|
-
});
|
|
2576
|
-
if (resolved.defaults) apply({
|
|
2577
|
-
t: "setDefaults",
|
|
2578
|
-
subtype: resolved.preset,
|
|
2579
|
-
patch: resolved.defaults
|
|
2580
|
-
});
|
|
2581
|
-
return () => {
|
|
2582
|
-
registry.delete(resolved.id);
|
|
2583
|
-
un?.();
|
|
2584
|
-
};
|
|
2585
|
-
},
|
|
2586
|
-
update: (ref, patch) => updateOne(ref, patch),
|
|
2587
|
-
updateSelection,
|
|
2588
|
-
toggleTextFormat: (format) => {
|
|
2589
|
-
const current = memoSelectionProps().values[format];
|
|
2590
|
-
updateSelection({ [format]: !current });
|
|
2591
|
-
},
|
|
2592
|
-
updateSelectionFlags: (patch) => apply({
|
|
2593
|
-
t: "setFlags",
|
|
2594
|
-
patch
|
|
2595
|
-
}),
|
|
2596
|
-
getSelectionFlags: () => memoSelectionFlags(),
|
|
2597
|
-
delete: async (ref) => {
|
|
2598
|
-
const doc = ctx.doc;
|
|
2599
|
-
if (!doc) throw new Error("[annotation] no document bound");
|
|
2600
|
-
const pon = ponForRef(ref);
|
|
2601
|
-
if (pon == null) throw new Error("[annotation] cannot resolve page for ref");
|
|
2602
|
-
await doc.page(pon).annotations.delete(ref);
|
|
2603
|
-
apply({
|
|
2604
|
-
t: "remove",
|
|
2605
|
-
ids: [require_seam.refKey(ref)]
|
|
2606
|
-
});
|
|
2607
|
-
},
|
|
2608
|
-
canRead: () => canRead(),
|
|
2609
|
-
canCreate: () => allowsCreate(),
|
|
2610
|
-
canEdit: (ref) => {
|
|
2611
|
-
const a = model().byId[require_seam.refKey(ref)];
|
|
2612
|
-
return !!a && (0, _embedpdf_core_annotation.annotTransformable)(a);
|
|
2613
|
-
},
|
|
2614
|
-
canDelete: (ref) => {
|
|
2615
|
-
const a = model().byId[require_seam.refKey(ref)];
|
|
2616
|
-
return !!a && (0, _embedpdf_core_annotation.annotDeletable)(a);
|
|
2617
|
-
},
|
|
2618
|
-
comments,
|
|
2619
|
-
links: {
|
|
2620
|
-
of: (ref) => {
|
|
2621
|
-
const a = model().byId[require_seam.refKey(ref)];
|
|
2622
|
-
if (!a) return null;
|
|
2623
|
-
return a.subtype === "link" ? a.link ?? null : (0, _embedpdf_core_annotation.linkOf)(model(), a.id);
|
|
2624
|
-
},
|
|
2625
|
-
set: (ref, target) => scheduleLinkSync(require_seam.refKey(ref), { target }),
|
|
2626
|
-
clear: (ref) => scheduleLinkSync(require_seam.refKey(ref), { target: null })
|
|
2627
|
-
},
|
|
2628
|
-
getSelection: () => {
|
|
2629
|
-
const m = model();
|
|
2630
|
-
return m.selected.map((id) => m.byId[id]?.ref).filter((r) => r != null);
|
|
2631
|
-
},
|
|
2632
|
-
get: (ref) => model().byId[require_seam.refKey(ref)]?.data ?? null,
|
|
2633
|
-
list: (pon) => {
|
|
2634
|
-
const m = model();
|
|
2635
|
-
return m.order.map((id) => m.byId[id]).filter((a) => a?.pon === pon).map((a) => a?.data).filter((d) => d != null);
|
|
2636
|
-
},
|
|
2637
|
-
getSelected: () => {
|
|
2638
|
-
const m = model();
|
|
2639
|
-
return m.selected.map((id) => m.byId[id]?.data).filter((d) => d != null);
|
|
2640
|
-
},
|
|
2641
|
-
getSelectionProps: () => memoSelectionProps(),
|
|
2642
|
-
propsForTool: (toolId) => (0, _embedpdf_core_annotation.propsFor)(registry.get(toolId)?.propsKind ?? toolId),
|
|
2643
|
-
pageItems: (pon, view) => memoItems(pon, view),
|
|
2644
|
-
chrome: (pon, scale, rotation, zoom) => memoChrome(pon, scale, rotation, zoom),
|
|
2645
|
-
selectionAnchor: (scale, rotation, zoom) => memoAnchor(scale, rotation, zoom),
|
|
2646
|
-
creationDraftAnchor: () => memoDraftAnchor(),
|
|
2647
|
-
selection: () => model().selected,
|
|
2648
|
-
hitKind: (pon, point, scale, rotation, zoom, touch) => (0, _embedpdf_core_annotation.hitTest)(model(), pon, point, chromeGeomAt(scale, grabBoost(touch)), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)).t,
|
|
2649
|
-
claimsTouchAt: (pon, point, scale, rotation, zoom) => {
|
|
2650
|
-
const m = model();
|
|
2651
|
-
const t = (0, _embedpdf_core_annotation.hitTest)(m, pon, point, chromeGeomAt(scale, TOUCH_GRAB_BOOST), m.hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation));
|
|
2652
|
-
if (t.t === "handle" || t.t === "rotate" || t.t === "group-handle") return true;
|
|
2653
|
-
if (t.t === "annot") return m.selected.includes(t.id) && m.selected.every((id) => (0, _embedpdf_core_annotation.canMove)(m, id));
|
|
2654
|
-
return false;
|
|
2655
|
-
},
|
|
2656
|
-
cursorAt: (pon, point, scale, rotation, zoom) => (0, _embedpdf_core_annotation.cursorAt)(model(), pon, point, chromeGeomAt(scale), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)),
|
|
2657
|
-
hoverAt: (at) => {
|
|
2658
|
-
const m = model();
|
|
2659
|
-
let id = null;
|
|
2660
|
-
if (at) {
|
|
2661
|
-
const h = (0, _embedpdf_core_annotation.hitTest)(m, at.pon, at.point, chromeGeomAt(at.scale), m.hitMargin, pageBoxOf(at.pon), inertIdsAt(at.pon), viewEnv(at.zoom, at.rotation));
|
|
2662
|
-
if (h.t === "annot") id = h.id;
|
|
2663
|
-
}
|
|
2664
|
-
if (m.hovered !== id) {
|
|
2665
|
-
hoverFeed?.hover(id);
|
|
2666
|
-
apply({
|
|
2667
|
-
t: "hover",
|
|
2668
|
-
id
|
|
2669
|
-
});
|
|
2670
|
-
}
|
|
2671
|
-
},
|
|
2672
|
-
behaviorFor: (a) => behaviors.find((b) => b.matches(a) && b.engaged()) ?? null,
|
|
2673
|
-
linkItemsOn: (pon) => memoLinkItems(pon),
|
|
2674
|
-
appearanceEpoch: (pon) => {
|
|
2675
|
-
const m = model();
|
|
2676
|
-
const parts = [];
|
|
2677
|
-
for (const id of m.order) {
|
|
2678
|
-
const a = m.byId[id];
|
|
2679
|
-
if (!a || a.pon !== pon || a.source !== "baked" || !a.ref) continue;
|
|
2680
|
-
if ((0, _embedpdf_core_annotation.isSubstrateOnly)(a)) continue;
|
|
2681
|
-
parts.push(`${id}@${a.apVersion ?? 0}`);
|
|
2682
|
-
}
|
|
2683
|
-
return parts.sort().join("|");
|
|
2684
|
-
},
|
|
2685
|
-
bakeScale: (renderScale) => (0, _embedpdf_core.snapAppearanceScale)(ctx.document()?.renderPolicy ?? _embedpdf_core.CONTINUOUS_RENDER_POLICY, renderScale),
|
|
2686
|
-
appearances: (pon, scale, signal) => {
|
|
2687
|
-
const doc = ctx.doc;
|
|
2688
|
-
if (!doc) return Promise.resolve([]);
|
|
2689
|
-
const task = doc.page(pon).annotations.renderAppearanceImages({ scale });
|
|
2690
|
-
if (signal) if (signal.aborted) task.abort(signal.reason);
|
|
2691
|
-
else signal.addEventListener("abort", () => task.abort(signal.reason), { once: true });
|
|
2692
|
-
return task.then((r) => r.appearances, () => []);
|
|
2693
|
-
},
|
|
2694
|
-
toContentBox: (pon, rect) => {
|
|
2695
|
-
const crop = cropOf(pon);
|
|
2696
|
-
return crop ? (0, _embedpdf_core_annotation.pdfToContentRect)(rect, crop) : null;
|
|
2697
|
-
},
|
|
2698
|
-
editPointer: (phase, pon, point, shift, scale, rotation, zoom, touch) => apply({
|
|
2699
|
-
t: "editPointer",
|
|
2700
|
-
phase,
|
|
2701
|
-
in: {
|
|
2702
|
-
pon,
|
|
2703
|
-
point,
|
|
2704
|
-
shift,
|
|
2705
|
-
pageBox: pageBoxOf(pon),
|
|
2706
|
-
chrome: chromeGeomAt(scale, grabBoost(touch)),
|
|
2707
|
-
inert: inertIdsAt(pon),
|
|
2708
|
-
...zoom != null ? { zoom } : {},
|
|
2709
|
-
...rotation != null ? { displayRotation: rotation } : {}
|
|
2710
|
-
}
|
|
2711
|
-
}),
|
|
2712
|
-
marqueePointer: (phase, pon, point, shift, scale, rotation, zoom) => apply({
|
|
2713
|
-
t: "marqueePointer",
|
|
2714
|
-
phase,
|
|
2715
|
-
in: {
|
|
2716
|
-
pon,
|
|
2717
|
-
point,
|
|
2718
|
-
shift,
|
|
2719
|
-
pageBox: pageBoxOf(pon),
|
|
2720
|
-
inert: inertIdsAt(pon),
|
|
2721
|
-
...zoom != null ? { zoom } : {},
|
|
2722
|
-
...rotation != null ? { displayRotation: rotation } : {}
|
|
2723
|
-
}
|
|
2724
|
-
}),
|
|
2725
|
-
createPointer: (tool, phase, pon, point, finish = false, displayRotation) => {
|
|
2726
|
-
if (!allowsCreate()) return;
|
|
2727
|
-
const t = registry.get(tool);
|
|
2728
|
-
apply({
|
|
2729
|
-
t: "createPointer",
|
|
2730
|
-
phase,
|
|
2731
|
-
subtype: t?.subtype ?? tool,
|
|
2732
|
-
preset: t?.preset ?? tool,
|
|
2733
|
-
intent: t?.intent,
|
|
2734
|
-
clickCreate: t?.clickCreate,
|
|
2735
|
-
flags: t?.flags,
|
|
2736
|
-
deferInkCommit: (t?.ink?.groupStrokesMs ?? 0) > 0,
|
|
2737
|
-
straightenInk: t?.ink?.straighten,
|
|
2738
|
-
in: {
|
|
2739
|
-
pon,
|
|
2740
|
-
point,
|
|
2741
|
-
shift: false,
|
|
2742
|
-
finish,
|
|
2743
|
-
pageBox: pageBoxOf(pon),
|
|
2744
|
-
displayRotation,
|
|
2745
|
-
upright: t?.upright
|
|
2746
|
-
}
|
|
2747
|
-
});
|
|
2748
|
-
},
|
|
2749
|
-
finishCreationDraft: () => apply({ t: "finishCreationDraft" }),
|
|
2750
|
-
finishInkDraft: () => apply({ t: "finishInkDraft" }),
|
|
2751
|
-
cancelCreationDraft: () => apply({ t: "cancel" }),
|
|
2752
|
-
markupFromSelection: (subtype, preset) => {
|
|
2753
|
-
if (!allowsCreate()) return false;
|
|
2754
|
-
const selection = ctx.tryGet(_embedpdf_plugin_selection_contract.SelectionToken);
|
|
2755
|
-
if (!selection || !selection.hasSelection()) return false;
|
|
2756
|
-
const snapshot = selection.snapshot();
|
|
2757
|
-
let created = false;
|
|
2758
|
-
for (const page of snapshot.pages) {
|
|
2759
|
-
if (!page.segments.length) continue;
|
|
2760
|
-
apply({
|
|
2761
|
-
t: "createMarkup",
|
|
2762
|
-
subtype,
|
|
2763
|
-
pon: page.pon,
|
|
2764
|
-
quads: page.segments.map((s) => s.quad),
|
|
2765
|
-
preset,
|
|
2766
|
-
flags: preset ? registry.get(preset)?.flags : void 0
|
|
2767
|
-
});
|
|
2768
|
-
created = true;
|
|
2769
|
-
}
|
|
2770
|
-
if (created) selection.clear();
|
|
2771
|
-
return created;
|
|
2772
|
-
},
|
|
2773
|
-
createMarkup: (subtype, pon, quads, preset) => {
|
|
2774
|
-
if (!allowsCreate()) return;
|
|
2775
|
-
apply({
|
|
2776
|
-
t: "createMarkup",
|
|
2777
|
-
subtype,
|
|
2778
|
-
pon,
|
|
2779
|
-
quads,
|
|
2780
|
-
preset,
|
|
2781
|
-
flags: preset ? registry.get(preset)?.flags : void 0
|
|
2782
|
-
});
|
|
2783
|
-
},
|
|
2784
|
-
createCaret: (pon, anchor) => {
|
|
2785
|
-
if (!allowsCreate()) return;
|
|
2786
|
-
apply({
|
|
2787
|
-
t: "createCaret",
|
|
2788
|
-
pon,
|
|
2789
|
-
anchor
|
|
2790
|
-
});
|
|
2791
|
-
},
|
|
2792
|
-
createReplaceText: (pon, quads, anchor, preset) => {
|
|
2793
|
-
if (!allowsCreate()) return;
|
|
2794
|
-
apply({
|
|
2795
|
-
t: "createReplaceText",
|
|
2796
|
-
pon,
|
|
2797
|
-
quads,
|
|
2798
|
-
anchor,
|
|
2799
|
-
preset
|
|
2800
|
-
});
|
|
2801
|
-
},
|
|
2802
|
-
previewMarkup: (subtype, quadsByPage, preset) => apply({
|
|
2803
|
-
t: "setMarkupPreview",
|
|
2804
|
-
subtype,
|
|
2805
|
-
quadsByPage,
|
|
2806
|
-
preset
|
|
2807
|
-
}),
|
|
2808
|
-
clearMarkupPreview: () => apply({ t: "clearMarkupPreview" }),
|
|
2809
|
-
setDefaults: (subtype, patch) => apply({
|
|
2810
|
-
t: "setDefaults",
|
|
2811
|
-
subtype,
|
|
2812
|
-
patch
|
|
2813
|
-
}),
|
|
2814
|
-
currentDefaults: (toolId) => (0, _embedpdf_core_annotation.defaultsFor)(model(), registry.get(toolId)?.preset ?? toolId),
|
|
2815
|
-
setSnap: (patch) => apply({
|
|
2816
|
-
t: "setSnap",
|
|
2817
|
-
patch
|
|
2818
|
-
}),
|
|
2819
|
-
snapSettings: () => model().snap,
|
|
2820
|
-
setChrome: (patch) => ctx.dispatch({
|
|
2821
|
-
type: "SET_CHROME",
|
|
2822
|
-
patch
|
|
2823
|
-
}),
|
|
2824
|
-
chromeSettings: () => chromeSettings(),
|
|
2825
|
-
deleteSelection: () => apply({ t: "delete" }),
|
|
2826
|
-
deselect: () => apply({ t: "deselect" }),
|
|
2827
|
-
select: (ref, options) => {
|
|
2828
|
-
apply({
|
|
2829
|
-
t: "select",
|
|
2830
|
-
ids: [require_seam.refKey(ref)],
|
|
2831
|
-
add: options?.add
|
|
2832
|
-
});
|
|
2833
|
-
},
|
|
2834
|
-
pruneEngagedSelection: () => {
|
|
2835
|
-
const m = model();
|
|
2836
|
-
const drop = m.selected.filter((id) => {
|
|
2837
|
-
const a = m.byId[id];
|
|
2838
|
-
return a && behaviors.some((b) => b.matches({
|
|
2839
|
-
subtype: a.subtype,
|
|
2840
|
-
ref: a.ref
|
|
2841
|
-
}) && b.engaged());
|
|
2842
|
-
});
|
|
2843
|
-
if (drop.length) apply({
|
|
2844
|
-
t: "deselect",
|
|
2845
|
-
ids: drop
|
|
2846
|
-
});
|
|
2847
|
-
},
|
|
2848
|
-
cancel: () => apply({ t: "cancel" }),
|
|
2849
|
-
rotateSelection90: () => apply({ t: "rotate90" }),
|
|
2850
|
-
resetSelectionRotation: () => apply({ t: "resetRotation" }),
|
|
2851
|
-
/** Group the current selection into one unit: the bottom-most member becomes
|
|
2852
|
-
* the primary, every other member becomes a `/RT /Group` subordinate of it.
|
|
2853
|
-
* No-op unless 2+ committed annotations on a single page are selected. */
|
|
2854
|
-
group: async () => {
|
|
2855
|
-
const m = model();
|
|
2856
|
-
const members = selectedCommitted();
|
|
2857
|
-
if (members.length < 2) return;
|
|
2858
|
-
const pon = members[0].pon;
|
|
2859
|
-
if (members.some((a) => a.pon !== pon)) return;
|
|
2860
|
-
const [primary, ...rest] = [...members].sort((a, b) => m.order.indexOf(a.id) - m.order.indexOf(b.id));
|
|
2861
|
-
if (!primary.ref) return;
|
|
2862
|
-
await Promise.all(rest.map((a) => writeRelationship(a, {
|
|
2863
|
-
inReplyTo: primary.ref,
|
|
2864
|
-
replyType: "group"
|
|
2865
|
-
})));
|
|
2866
|
-
},
|
|
2867
|
-
/** Ungroup: clear `/IRT` (+ `/RT`) on every subordinate in the group(s) the
|
|
2868
|
-
* selection touches, so each member becomes top-level again. */
|
|
2869
|
-
ungroup: async () => {
|
|
2870
|
-
const m = model();
|
|
2871
|
-
const subs = (0, _embedpdf_core_annotation.expandGroups)(m, m.selected).map((id) => m.byId[id]).filter((a) => !!a && !!a.ref && !!a.data && !!a.group);
|
|
2872
|
-
await Promise.all(subs.map((a) => writeRelationship(a, { inReplyTo: null })));
|
|
2873
|
-
},
|
|
2874
|
-
canGroup: () => {
|
|
2875
|
-
const m = model();
|
|
2876
|
-
const members = selectedCommitted();
|
|
2877
|
-
if (members.length < 2) return false;
|
|
2878
|
-
if (members.some((a) => a.pon !== members[0].pon)) return false;
|
|
2879
|
-
if (!members.every((a) => a.ref != null && allowsMutation("update", a.ref))) return false;
|
|
2880
|
-
const keys = new Set(m.selected.map((id) => (0, _embedpdf_core_annotation.groupKeyOf)(m, id)));
|
|
2881
|
-
if (keys.size === 1 && !keys.has(null)) return false;
|
|
2882
|
-
return true;
|
|
2883
|
-
},
|
|
2884
|
-
canUngroup: () => {
|
|
2885
|
-
const m = model();
|
|
2886
|
-
const subs = (0, _embedpdf_core_annotation.expandGroups)(m, m.selected).map((id) => m.byId[id]).filter((a) => !!a && !!a.group);
|
|
2887
|
-
return subs.length > 0 && subs.every((a) => a.ref != null && allowsMutation("update", a.ref));
|
|
2888
|
-
},
|
|
2889
|
-
ensurePage: () => {},
|
|
2890
|
-
reloadPage: (pon) => reloadPageNow(pon),
|
|
2891
|
-
hydration: () => ctx.getState().hydration,
|
|
2892
|
-
ensureHydrated: () => {
|
|
2893
|
-
if (hydrationStarted) return;
|
|
2894
|
-
hydrationStarted = true;
|
|
2895
|
-
runHydration(false);
|
|
2896
|
-
},
|
|
2897
|
-
rehydrate,
|
|
2898
|
-
deliverRemoteAnnotationEvent: (event) => {
|
|
2899
|
-
if (hydrationWindow) {
|
|
2900
|
-
pendingRemote.push(event);
|
|
2901
|
-
return;
|
|
2902
|
-
}
|
|
2903
|
-
applyRemoteAnnotationEvent(event);
|
|
2904
|
-
},
|
|
2905
|
-
textItems: (pon, view) => memoTexts(pon, view),
|
|
2906
|
-
currentEditing: () => model().editing,
|
|
2907
|
-
beginTextEdit: (ref) => apply({
|
|
2908
|
-
t: "beginTextEdit",
|
|
2909
|
-
id: require_seam.refKey(ref)
|
|
2910
|
-
}),
|
|
2911
|
-
beginTextEditAt: (pon, point, scale, rotation, zoom) => {
|
|
2912
|
-
const m = model();
|
|
2913
|
-
const h = (0, _embedpdf_core_annotation.hitTest)(m, pon, point, chromeGeomAt(scale), m.hitMargin, pageBoxOf(pon), void 0, viewEnv(zoom, rotation));
|
|
2914
|
-
const id = h.t === "annot" || h.t === "handle" ? h.id : null;
|
|
2915
|
-
if (id != null && m.byId[id]?.geom.t === "text") {
|
|
2916
|
-
apply({
|
|
2917
|
-
t: "beginTextEdit",
|
|
2918
|
-
id
|
|
2919
|
-
});
|
|
2920
|
-
return true;
|
|
2921
|
-
}
|
|
2922
|
-
return false;
|
|
2923
|
-
},
|
|
2924
|
-
setContents: (ref, text) => {
|
|
2925
|
-
apply({
|
|
2926
|
-
t: "setText",
|
|
2927
|
-
id: require_seam.refKey(ref),
|
|
2928
|
-
text
|
|
2929
|
-
});
|
|
2930
|
-
scheduleTextCommit(ref);
|
|
2931
|
-
},
|
|
2932
|
-
setRichText: (ref, doc) => {
|
|
2933
|
-
apply({
|
|
2934
|
-
t: "setRichText",
|
|
2935
|
-
id: require_seam.refKey(ref),
|
|
2936
|
-
doc: { paragraphs: doc.paragraphs }
|
|
2937
|
-
});
|
|
2938
|
-
scheduleTextCommit(ref);
|
|
2939
|
-
},
|
|
2940
|
-
setTextSelection: (ref, range) => {
|
|
2941
|
-
const id = require_seam.refKey(ref);
|
|
2942
|
-
const prev = ctx.getState().textSelection;
|
|
2943
|
-
const next = range ? {
|
|
2944
|
-
id,
|
|
2945
|
-
start: range.start,
|
|
2946
|
-
end: range.end
|
|
2947
|
-
} : null;
|
|
2948
|
-
if (prev === null === (next === null) && (!prev || !next || prev.id === next.id && prev.start === next.start && prev.end === next.end)) return;
|
|
2949
|
-
ctx.dispatch({
|
|
2950
|
-
type: "SET_TEXT_SELECTION",
|
|
2951
|
-
selection: next
|
|
2952
|
-
});
|
|
2953
|
-
},
|
|
2954
|
-
cssFontFamily: (family) => cssFontFamilyForFace(family, fonts),
|
|
2955
|
-
endTextEdit: () => {
|
|
2956
|
-
flushTextCommits();
|
|
2957
|
-
if (ctx.getState().textSelection) ctx.dispatch({
|
|
2958
|
-
type: "SET_TEXT_SELECTION",
|
|
2959
|
-
selection: null
|
|
2960
|
-
});
|
|
2961
|
-
apply({ t: "endTextEdit" });
|
|
2962
|
-
},
|
|
2963
|
-
commitScriptEffects: async (entries) => {
|
|
2964
|
-
const doc = ctx.doc;
|
|
2965
|
-
if (!doc) return { results: entries.map((entry) => ({
|
|
2966
|
-
annotObjectNumber: entry.annotObjectNumber,
|
|
2967
|
-
status: "failed",
|
|
2968
|
-
error: "no document"
|
|
2969
|
-
})) };
|
|
2970
|
-
const results = [];
|
|
2971
|
-
const touchedPons = /* @__PURE__ */ new Set();
|
|
2972
|
-
let failed = false;
|
|
2973
|
-
for (const entry of entries) {
|
|
2974
|
-
if (failed) {
|
|
2975
|
-
results.push({
|
|
2976
|
-
annotObjectNumber: entry.annotObjectNumber,
|
|
2977
|
-
status: "skipped"
|
|
2978
|
-
});
|
|
2979
|
-
continue;
|
|
2980
|
-
}
|
|
2981
|
-
const loaded = model().byId[`obj:${entry.annotObjectNumber}`];
|
|
2982
|
-
const pon = loaded?.pon ?? entry.pageObjectNumber;
|
|
2983
|
-
let ref = loaded?.ref ?? null;
|
|
2984
|
-
let subtype = loaded?.subtype;
|
|
2985
|
-
if ((!ref || !subtype) && pon !== void 0) try {
|
|
2986
|
-
const { annotations } = await doc.page(pon).annotations.list();
|
|
2987
|
-
const dto = annotations.find((candidate) => candidate.ref.kind === "objectNumber" && candidate.ref.annotObjectNumber === entry.annotObjectNumber);
|
|
2988
|
-
if (dto) {
|
|
2989
|
-
ref = dto.ref;
|
|
2990
|
-
subtype = dto.subtype;
|
|
2991
|
-
}
|
|
2992
|
-
} catch {}
|
|
2993
|
-
if (pon === void 0 || !ref || !subtype) {
|
|
2994
|
-
results.push({
|
|
2995
|
-
annotObjectNumber: entry.annotObjectNumber,
|
|
2996
|
-
status: "failed",
|
|
2997
|
-
error: "annotation not resolved (page not loaded)"
|
|
2998
|
-
});
|
|
2999
|
-
continue;
|
|
3000
|
-
}
|
|
3001
|
-
const patch = engineScriptPatch(subtype, entry.patch);
|
|
3002
|
-
if (patch instanceof Error) {
|
|
3003
|
-
results.push({
|
|
3004
|
-
annotObjectNumber: entry.annotObjectNumber,
|
|
3005
|
-
status: "failed",
|
|
3006
|
-
error: patch.message
|
|
3007
|
-
});
|
|
3008
|
-
continue;
|
|
3009
|
-
}
|
|
3010
|
-
try {
|
|
3011
|
-
await doc.page(pon).annotations.update(ref, patch);
|
|
3012
|
-
touchedPons.add(pon);
|
|
3013
|
-
results.push({
|
|
3014
|
-
annotObjectNumber: entry.annotObjectNumber,
|
|
3015
|
-
status: "applied"
|
|
3016
|
-
});
|
|
3017
|
-
} catch (error) {
|
|
3018
|
-
failed = true;
|
|
3019
|
-
results.push({
|
|
3020
|
-
annotObjectNumber: entry.annotObjectNumber,
|
|
3021
|
-
status: "failed",
|
|
3022
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3023
|
-
});
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
for (const touched of touchedPons) await reloadPageNow(touched);
|
|
3027
|
-
return { results };
|
|
3028
|
-
},
|
|
3029
|
-
registerBehavior: (b) => {
|
|
3030
|
-
behaviors.push(b);
|
|
3031
|
-
return () => {
|
|
3032
|
-
const i = behaviors.indexOf(b);
|
|
3033
|
-
if (i >= 0) behaviors.splice(i, 1);
|
|
3034
|
-
};
|
|
3035
|
-
}
|
|
3036
|
-
};
|
|
3037
|
-
}
|
|
3038
|
-
//#endregion
|
|
3039
|
-
Object.defineProperty(exports, "createAnnotationCapability", {
|
|
3040
|
-
enumerable: true,
|
|
3041
|
-
get: function() {
|
|
3042
|
-
return createAnnotationCapability;
|
|
3043
|
-
}
|
|
3044
|
-
});
|
|
3045
|
-
Object.defineProperty(exports, "fromDTO", {
|
|
3046
|
-
enumerable: true,
|
|
3047
|
-
get: function() {
|
|
3048
|
-
return fromDTO;
|
|
3049
|
-
}
|
|
3050
|
-
});
|
|
3051
|
-
Object.defineProperty(exports, "toCreateDraft", {
|
|
3052
|
-
enumerable: true,
|
|
3053
|
-
get: function() {
|
|
3054
|
-
return toCreateDraft;
|
|
3055
|
-
}
|
|
3056
|
-
});
|
|
3057
|
-
Object.defineProperty(exports, "toPatch", {
|
|
3058
|
-
enumerable: true,
|
|
3059
|
-
get: function() {
|
|
3060
|
-
return toPatch;
|
|
3061
|
-
}
|
|
3062
|
-
});
|
|
3063
|
-
|
|
3064
|
-
//# sourceMappingURL=capability-DVx1NmTe.cjs.map
|