@embedpdf/plugin-annotation 3.0.0-next.12 → 3.0.0-next.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{capability-R7dgogJs.cjs → capability-Bt320iJP.cjs} +713 -597
- package/dist/capability-Bt320iJP.cjs.map +1 -0
- package/dist/{capability-Bh_1PL5v.js → capability-CoL4na7g.js} +712 -572
- package/dist/capability-CoL4na7g.js.map +1 -0
- package/dist/contract/host.d.cts +2 -2
- package/dist/contract/host.d.ts +2 -2
- package/dist/contract-C7rcuNp_.d.cts +34 -0
- package/dist/contract-CM2Ovdq_.d.ts +34 -0
- package/dist/contract-DtXE1hjY.js +32 -0
- package/dist/contract-DtXE1hjY.js.map +1 -0
- package/dist/contract-a4eaQWga.cjs +67 -0
- package/dist/contract-a4eaQWga.cjs.map +1 -0
- package/dist/contract.cjs +9 -7
- package/dist/contract.d.cts +3 -10
- package/dist/contract.d.ts +3 -10
- package/dist/contract.js +4 -8
- package/dist/index.cjs +58 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +51 -6
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +3 -2
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +2 -1
- package/dist/tools-B3i1gjRs.cjs +589 -0
- package/dist/tools-B3i1gjRs.cjs.map +1 -0
- package/dist/tools-DsfqBn_a.js +566 -0
- package/dist/tools-DsfqBn_a.js.map +1 -0
- package/dist/types-BvB1vUKb.js.map +1 -1
- package/dist/{types-BgetZ4BK.d.ts → types-ChRnKozg.d.cts} +131 -14
- package/dist/types-Dlhdw2k_.cjs.map +1 -1
- package/dist/{types-BAPdbfPT.d.cts → types-Dmz3eRSG.d.ts} +131 -14
- package/package.json +9 -9
- package/dist/capability-Bh_1PL5v.js.map +0 -1
- package/dist/capability-R7dgogJs.cjs.map +0 -1
- package/dist/contract.cjs.map +0 -1
- package/dist/contract.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_seam = require("./seam-6pJeKOOJ.cjs");
|
|
2
|
+
const require_tools = require("./tools-B3i1gjRs.cjs");
|
|
2
3
|
const require_types = require("./types-Dlhdw2k_.cjs");
|
|
3
4
|
let _embedpdf_core = require("@embedpdf/core");
|
|
4
5
|
let _embedpdf_plugin_actions_contract = require("@embedpdf/plugin-actions/contract");
|
|
@@ -106,6 +107,15 @@ const polyCloudy = (a) => a.geom.t === "poly" && a.geom.closed ? { cloudyIntensi
|
|
|
106
107
|
/** The visual-bounds `/Rect` (stroke + border included) of a stroke geom. */
|
|
107
108
|
const visualRect = (a, crop) => {
|
|
108
109
|
const g = a.geom;
|
|
110
|
+
if (a.measure) {
|
|
111
|
+
const bounds = (0, _embedpdf_core_annotation.measurementLayout)(g, a.measure, a.style)?.visualBounds;
|
|
112
|
+
if (bounds) return { rect: {
|
|
113
|
+
left: crop.left + bounds.x,
|
|
114
|
+
right: crop.left + bounds.x + bounds.width,
|
|
115
|
+
top: crop.top - bounds.y,
|
|
116
|
+
bottom: crop.top - bounds.y - bounds.height
|
|
117
|
+
} };
|
|
118
|
+
}
|
|
109
119
|
if (g.t === "line" || g.t === "ink") return { rect: (0, _embedpdf_core_annotation.geomPdfBounds)(g, a.style.strokeWidth, crop) };
|
|
110
120
|
if (g.t === "poly") return { rect: (0, _embedpdf_core_annotation.geomPdfBounds)(g, a.style.strokeWidth, crop, a.style.border) };
|
|
111
121
|
return {};
|
|
@@ -137,18 +147,29 @@ const strokeProps = {
|
|
|
137
147
|
const line = {
|
|
138
148
|
ingest: (dto, crop) => {
|
|
139
149
|
const d = dto;
|
|
140
|
-
return {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
return {
|
|
151
|
+
...d.intent === "LineDimension" ? { measure: {
|
|
152
|
+
intent: d.intent,
|
|
153
|
+
measure: d.measure ?? null,
|
|
154
|
+
caption: d.caption ?? { enabled: false },
|
|
155
|
+
leader: d.leader ?? void 0,
|
|
156
|
+
crop,
|
|
157
|
+
text: d.contents ?? ""
|
|
158
|
+
} } : {},
|
|
159
|
+
geom: {
|
|
160
|
+
t: "line",
|
|
161
|
+
a: (0, _embedpdf_core_annotation.pdfToContentPoint)(d.linePoints.start, crop),
|
|
162
|
+
b: (0, _embedpdf_core_annotation.pdfToContentPoint)(d.linePoints.end, crop),
|
|
163
|
+
ends: d.lineEndings,
|
|
164
|
+
...require_seam.rotFromDTO(d.rotation)
|
|
165
|
+
}
|
|
166
|
+
};
|
|
147
167
|
},
|
|
148
168
|
geometry: (a, crop) => {
|
|
149
169
|
const g = a.geom;
|
|
150
170
|
if (g.t !== "line") return null;
|
|
151
171
|
return {
|
|
172
|
+
...a.measure?.intent === "LineDimension" ? { contents: (0, _embedpdf_core_annotation.distanceLabel)(g, a.measure) } : {},
|
|
152
173
|
linePoints: {
|
|
153
174
|
start: (0, _embedpdf_core_annotation.contentToPdfPoint)(g.a, crop),
|
|
154
175
|
end: (0, _embedpdf_core_annotation.contentToPdfPoint)(g.b, crop)
|
|
@@ -157,29 +178,55 @@ const line = {
|
|
|
157
178
|
...advisoryRotation(g)
|
|
158
179
|
};
|
|
159
180
|
},
|
|
160
|
-
prop: strokeProps
|
|
181
|
+
prop: strokeProps,
|
|
182
|
+
draftExtras: (a) => a.measure?.intent === "LineDimension" ? {
|
|
183
|
+
intent: a.measure.intent,
|
|
184
|
+
measure: a.measure.measure?.subtype === "RL" ? a.measure.measure : null,
|
|
185
|
+
caption: a.measure.caption,
|
|
186
|
+
leader: a.measure.leader,
|
|
187
|
+
subject: "Distance"
|
|
188
|
+
} : {}
|
|
161
189
|
};
|
|
162
190
|
const polyProjection = (closed) => ({
|
|
163
191
|
ingest: (dto, crop) => {
|
|
164
192
|
const d = dto;
|
|
165
|
-
return {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
193
|
+
return {
|
|
194
|
+
...d.intent === "PolygonDimension" || d.intent === "PolyLineDimension" ? { measure: {
|
|
195
|
+
intent: d.intent,
|
|
196
|
+
measure: d.measure ?? null,
|
|
197
|
+
caption: d.caption ?? { enabled: false },
|
|
198
|
+
crop,
|
|
199
|
+
text: d.contents ?? ""
|
|
200
|
+
} } : {},
|
|
201
|
+
geom: {
|
|
202
|
+
t: "poly",
|
|
203
|
+
points: d.vertices.map((p) => (0, _embedpdf_core_annotation.pdfToContentPoint)(p, crop)),
|
|
204
|
+
closed,
|
|
205
|
+
..."lineEndings" in d ? { ends: d.lineEndings } : {},
|
|
206
|
+
...require_seam.rotFromDTO(d.rotation)
|
|
207
|
+
}
|
|
208
|
+
};
|
|
172
209
|
},
|
|
173
210
|
geometry: (a, crop) => {
|
|
174
211
|
const g = a.geom;
|
|
175
212
|
if (g.t !== "poly") return null;
|
|
176
213
|
return {
|
|
214
|
+
...a.measure && a.measure.intent !== "LineDimension" ? {
|
|
215
|
+
contents: (0, _embedpdf_core_annotation.shapeMeasurementLabel)(g, a.measure),
|
|
216
|
+
caption: a.measure.caption
|
|
217
|
+
} : {},
|
|
177
218
|
vertices: g.points.map((p) => (0, _embedpdf_core_annotation.contentToPdfPoint)(p, crop)),
|
|
178
219
|
...visualRect(a, crop),
|
|
179
220
|
...advisoryRotation(g)
|
|
180
221
|
};
|
|
181
222
|
},
|
|
182
|
-
prop: strokeProps
|
|
223
|
+
prop: strokeProps,
|
|
224
|
+
draftExtras: (a) => a.measure && a.measure.intent !== "LineDimension" ? {
|
|
225
|
+
intent: a.measure.intent,
|
|
226
|
+
measure: a.measure.measure?.subtype === "RL" ? a.measure.measure : null,
|
|
227
|
+
caption: a.measure.caption,
|
|
228
|
+
subject: closed ? "Area" : "Perimeter"
|
|
229
|
+
} : {}
|
|
183
230
|
});
|
|
184
231
|
const polygon = polyProjection(true);
|
|
185
232
|
const polyline = polyProjection(false);
|
|
@@ -208,18 +255,387 @@ const ink = {
|
|
|
208
255
|
draftExtras: (a) => a.intent === "ink-highlight" ? { intent: a.intent } : {}
|
|
209
256
|
};
|
|
210
257
|
//#endregion
|
|
258
|
+
//#region src/rich-text.ts
|
|
259
|
+
/**
|
|
260
|
+
* Rich text POLICY — what the annotation plugin decides about a free-text
|
|
261
|
+
* annotation's rich document, kept pure so every framework's editor glue
|
|
262
|
+
* behaves identically (the mechanics live in `@embedpdf/web`'s binding, the
|
|
263
|
+
* run algebra in the core):
|
|
264
|
+
*
|
|
265
|
+
* • the rich document of any annot (the DTO's, or one synthesised from the
|
|
266
|
+
* plain text + the `/DA` text style for a draft that has no DTO yet)
|
|
267
|
+
* • a flat props patch → the run delta it means for a text RANGE (font,
|
|
268
|
+
* size, colour, bold/italic/underline) and the keys left for the body
|
|
269
|
+
* • what a range reads back for those keys (agree → the value, else mixed)
|
|
270
|
+
* • faces: a DTO font (standard kebab name / registered key) ↔ the face a
|
|
271
|
+
* run names (family + weight + italic) ↔ a CSS family list
|
|
272
|
+
* • the commit rule: a document nothing overrides on a plain annotation
|
|
273
|
+
* commits as `contents`; anything else as `richText`
|
|
274
|
+
*/
|
|
275
|
+
/** The props keys a text range takes as run deltas. */
|
|
276
|
+
const RANGE_KEYS = [
|
|
277
|
+
"fontFamily",
|
|
278
|
+
"fontSize",
|
|
279
|
+
"fontColor",
|
|
280
|
+
"bold",
|
|
281
|
+
"italic",
|
|
282
|
+
"underline"
|
|
283
|
+
];
|
|
284
|
+
/** The engine's standard-14 vocabulary (the `StandardFont` kebab names). */
|
|
285
|
+
const STANDARD_FACES = {
|
|
286
|
+
courier: {
|
|
287
|
+
family: "Courier",
|
|
288
|
+
weight: 400,
|
|
289
|
+
italic: false
|
|
290
|
+
},
|
|
291
|
+
"courier-bold": {
|
|
292
|
+
family: "Courier",
|
|
293
|
+
weight: 700,
|
|
294
|
+
italic: false
|
|
295
|
+
},
|
|
296
|
+
"courier-bold-oblique": {
|
|
297
|
+
family: "Courier",
|
|
298
|
+
weight: 700,
|
|
299
|
+
italic: true
|
|
300
|
+
},
|
|
301
|
+
"courier-oblique": {
|
|
302
|
+
family: "Courier",
|
|
303
|
+
weight: 400,
|
|
304
|
+
italic: true
|
|
305
|
+
},
|
|
306
|
+
helvetica: {
|
|
307
|
+
family: "Helvetica",
|
|
308
|
+
weight: 400,
|
|
309
|
+
italic: false
|
|
310
|
+
},
|
|
311
|
+
"helvetica-bold": {
|
|
312
|
+
family: "Helvetica",
|
|
313
|
+
weight: 700,
|
|
314
|
+
italic: false
|
|
315
|
+
},
|
|
316
|
+
"helvetica-bold-oblique": {
|
|
317
|
+
family: "Helvetica",
|
|
318
|
+
weight: 700,
|
|
319
|
+
italic: true
|
|
320
|
+
},
|
|
321
|
+
"helvetica-oblique": {
|
|
322
|
+
family: "Helvetica",
|
|
323
|
+
weight: 400,
|
|
324
|
+
italic: true
|
|
325
|
+
},
|
|
326
|
+
"times-roman": {
|
|
327
|
+
family: "Times",
|
|
328
|
+
weight: 400,
|
|
329
|
+
italic: false
|
|
330
|
+
},
|
|
331
|
+
"times-bold": {
|
|
332
|
+
family: "Times",
|
|
333
|
+
weight: 700,
|
|
334
|
+
italic: false
|
|
335
|
+
},
|
|
336
|
+
"times-bold-italic": {
|
|
337
|
+
family: "Times",
|
|
338
|
+
weight: 700,
|
|
339
|
+
italic: true
|
|
340
|
+
},
|
|
341
|
+
"times-italic": {
|
|
342
|
+
family: "Times",
|
|
343
|
+
weight: 400,
|
|
344
|
+
italic: true
|
|
345
|
+
},
|
|
346
|
+
symbol: {
|
|
347
|
+
family: "Symbol",
|
|
348
|
+
weight: 400,
|
|
349
|
+
italic: false
|
|
350
|
+
},
|
|
351
|
+
"zapf-dingbats": {
|
|
352
|
+
family: "ZapfDingbats",
|
|
353
|
+
weight: 400,
|
|
354
|
+
italic: false
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
/** Family names compare without case, spaces, hyphens, underscores and
|
|
358
|
+
* quotes — the engine's own rule. */
|
|
359
|
+
function familyKey(family) {
|
|
360
|
+
return family.toLowerCase().replace(/[\s\-_'"]/g, "");
|
|
361
|
+
}
|
|
362
|
+
const STANDARD_FAMILY_KEYS = {
|
|
363
|
+
helvetica: "Helvetica",
|
|
364
|
+
arial: "Helvetica",
|
|
365
|
+
arialmt: "Helvetica",
|
|
366
|
+
sansserif: "Helvetica",
|
|
367
|
+
times: "Times",
|
|
368
|
+
timesroman: "Times",
|
|
369
|
+
timesnewroman: "Times",
|
|
370
|
+
timesnewromanpsmt: "Times",
|
|
371
|
+
serif: "Times",
|
|
372
|
+
courier: "Courier",
|
|
373
|
+
couriernew: "Courier",
|
|
374
|
+
couriernewpsmt: "Courier",
|
|
375
|
+
monospace: "Courier",
|
|
376
|
+
symbol: "Symbol",
|
|
377
|
+
zapfdingbats: "ZapfDingbats",
|
|
378
|
+
dingbats: "ZapfDingbats"
|
|
379
|
+
};
|
|
380
|
+
/** Web stacks with the standard families' metrics (what the DOM editor and
|
|
381
|
+
* the vector renderer show for text the engine sets in a standard font). */
|
|
382
|
+
const STANDARD_STACKS = {
|
|
383
|
+
Helvetica: "Helvetica, Arial, sans-serif",
|
|
384
|
+
Times: "\"Times New Roman\", Times, serif",
|
|
385
|
+
Courier: "\"Courier New\", Courier, monospace",
|
|
386
|
+
Symbol: "serif",
|
|
387
|
+
ZapfDingbats: "serif"
|
|
388
|
+
};
|
|
389
|
+
/** The face a DTO font names: a standard font's family/weight/italic, a
|
|
390
|
+
* registered key's identity, else the string itself as a family. */
|
|
391
|
+
function faceForFont(font, fonts) {
|
|
392
|
+
const standard = STANDARD_FACES[font];
|
|
393
|
+
if (standard) return { ...standard };
|
|
394
|
+
const registered = fonts?.().find((f) => f.key === font);
|
|
395
|
+
if (registered) return {
|
|
396
|
+
family: registered.familyName,
|
|
397
|
+
weight: registered.weight,
|
|
398
|
+
italic: registered.italic
|
|
399
|
+
};
|
|
400
|
+
return { family: font };
|
|
401
|
+
}
|
|
402
|
+
/** The DTO font for a face: the registered key whose identity matches (the
|
|
403
|
+
* closest weight, italic first), else the standard kebab name, else the
|
|
404
|
+
* family itself. The inverse of {@link faceForFont}. */
|
|
405
|
+
function fontForFace(face, fonts) {
|
|
406
|
+
const wanted = familyKey(face.family);
|
|
407
|
+
const weight = face.weight ?? 400;
|
|
408
|
+
const italic = face.italic ?? false;
|
|
409
|
+
let best;
|
|
410
|
+
for (const f of fonts?.() ?? []) {
|
|
411
|
+
if (familyKey(f.familyName) !== wanted) continue;
|
|
412
|
+
const score = Math.abs(f.weight - weight) + (f.italic === italic ? 0 : 1e3);
|
|
413
|
+
if (!best || score < best.score) best = {
|
|
414
|
+
key: f.key,
|
|
415
|
+
score
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (best) return best.key;
|
|
419
|
+
const standard = STANDARD_FAMILY_KEYS[wanted];
|
|
420
|
+
if (standard) {
|
|
421
|
+
const bold = weight >= 600;
|
|
422
|
+
for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard && spec.weight >= 600 === bold && spec.italic === italic) return name;
|
|
423
|
+
for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard) return name;
|
|
424
|
+
}
|
|
425
|
+
return face.family;
|
|
426
|
+
}
|
|
427
|
+
/** The CSS family list for a face family: a standard family's web stack, a
|
|
428
|
+
* registered family's `@font-face` name (its key — see `mountWebFont`),
|
|
429
|
+
* else the family itself with a generic fallback. */
|
|
430
|
+
function cssFontFamilyForFace(family, fonts) {
|
|
431
|
+
const standard = STANDARD_FAMILY_KEYS[familyKey(family)];
|
|
432
|
+
if (standard) return STANDARD_STACKS[standard];
|
|
433
|
+
const wanted = familyKey(family);
|
|
434
|
+
const registered = fonts?.().find((f) => familyKey(f.familyName) === wanted);
|
|
435
|
+
if (registered) return `"${registered.key}"`;
|
|
436
|
+
return `"${family}", sans-serif`;
|
|
437
|
+
}
|
|
438
|
+
/** The CSS family list for a DTO font (a standard kebab name or a key). */
|
|
439
|
+
function cssFontFamilyForFont(font, fonts) {
|
|
440
|
+
const standard = STANDARD_FACES[font];
|
|
441
|
+
if (standard) return STANDARD_STACKS[standard.family];
|
|
442
|
+
return `"${font}", sans-serif`;
|
|
443
|
+
}
|
|
444
|
+
const hex = (css) => css.trim().toUpperCase();
|
|
445
|
+
/** The rich body the `/DA` text style describes (a draft's body before its
|
|
446
|
+
* DTO exists; also the fallback for a DTO without `richText`). */
|
|
447
|
+
function bodyFromTextStyle(t, fonts) {
|
|
448
|
+
const face = faceForFont(t.fontFamily, fonts);
|
|
449
|
+
return {
|
|
450
|
+
family: face.family,
|
|
451
|
+
weight: t.bold ? 700 : face.weight ?? 400,
|
|
452
|
+
italic: t.italic ?? face.italic ?? false,
|
|
453
|
+
size: t.fontSize,
|
|
454
|
+
color: hex(t.fontColor),
|
|
455
|
+
decoration: t.underline ? ["underline"] : [],
|
|
456
|
+
script: "normal",
|
|
457
|
+
letterSpacing: 0,
|
|
458
|
+
horizontalScale: 1,
|
|
459
|
+
align: t.textAlign,
|
|
460
|
+
dir: "ltr"
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/** The annotation's rich document: the DTO's, else one synthesised from
|
|
464
|
+
* its plain text and text style (a draft the engine has not echoed yet). */
|
|
465
|
+
function richDocOf(a, fonts) {
|
|
466
|
+
if (a.data?.subtype === "free-text" && a.data.richText) return a.data.richText;
|
|
467
|
+
return {
|
|
468
|
+
body: bodyFromTextStyle(a.text ?? {
|
|
469
|
+
fontFamily: "helvetica",
|
|
470
|
+
fontSize: 12,
|
|
471
|
+
fontColor: "#000000",
|
|
472
|
+
textAlign: "left"
|
|
473
|
+
}, fonts),
|
|
474
|
+
paragraphs: (0, _embedpdf_core_annotation.paragraphsFromPlainText)(a.data?.contents ?? "")
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The write a text edit commits: the rich paragraphs, with paragraph
|
|
479
|
+
* properties equal to the body's stripped (they are inherited, not
|
|
480
|
+
* overrides) and the body omitted — it stays what the props path last
|
|
481
|
+
* wrote. One engine, one path: plain and formatted text alike.
|
|
482
|
+
*/
|
|
483
|
+
function textCommitPatch(a, paragraphs, fonts) {
|
|
484
|
+
return { richText: { paragraphs: stripBodyDefaults(paragraphs, richDocOf(a, fonts).body) } };
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Paragraph alignment/direction equal to the body's are not overrides: the
|
|
488
|
+
* engine echoes them resolved, the editor renders them as inline styles and
|
|
489
|
+
* serialises them back, and the commit rule must see through that (a plain
|
|
490
|
+
* `/Contents` annotation must not grow an `/RC` from typing).
|
|
491
|
+
*/
|
|
492
|
+
function stripBodyDefaults(paragraphs, body) {
|
|
493
|
+
return paragraphs.map((p) => {
|
|
494
|
+
if ((p.align === void 0 || p.align !== body.align) && (p.dir === void 0 || p.dir !== body.dir)) return p;
|
|
495
|
+
const { align, dir, ...rest } = p;
|
|
496
|
+
return {
|
|
497
|
+
...rest,
|
|
498
|
+
...align !== void 0 && align !== body.align ? { align } : {},
|
|
499
|
+
...dir !== void 0 && dir !== body.dir ? { dir } : {}
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Split a props patch for an annotation whose editor holds a text RANGE:
|
|
505
|
+
* the run delta the range takes (font → face, size, colour, bold → weight,
|
|
506
|
+
* italic, underline → decoration) and the keys that still go to the body.
|
|
507
|
+
*/
|
|
508
|
+
function runDeltaForProps(patch, fonts) {
|
|
509
|
+
const delta = {};
|
|
510
|
+
const rest = {};
|
|
511
|
+
for (const key of Object.keys(patch)) {
|
|
512
|
+
const value = patch[key];
|
|
513
|
+
if (value === void 0) continue;
|
|
514
|
+
switch (key) {
|
|
515
|
+
case "fontFamily": {
|
|
516
|
+
const face = faceForFont(patch.fontFamily, fonts);
|
|
517
|
+
delta.family = face.family;
|
|
518
|
+
if (face.weight !== void 0) delta.weight = face.weight;
|
|
519
|
+
if (face.italic !== void 0) delta.italic = face.italic;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
case "fontSize":
|
|
523
|
+
delta.size = patch.fontSize;
|
|
524
|
+
break;
|
|
525
|
+
case "fontColor":
|
|
526
|
+
delta.color = hex(patch.fontColor);
|
|
527
|
+
break;
|
|
528
|
+
case "bold":
|
|
529
|
+
delta.weight = patch.bold ? 700 : 400;
|
|
530
|
+
break;
|
|
531
|
+
case "italic":
|
|
532
|
+
delta.italic = patch.italic;
|
|
533
|
+
break;
|
|
534
|
+
case "underline":
|
|
535
|
+
delta.decoration = patch.underline ? ["underline"] : [];
|
|
536
|
+
break;
|
|
537
|
+
default: rest[key] = value;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
delta,
|
|
542
|
+
rest
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/** The style deltas of every non-empty run a range overlaps. */
|
|
546
|
+
function runsInRange(doc, range) {
|
|
547
|
+
const start = Math.min(range.start, range.end);
|
|
548
|
+
const end = Math.max(range.start, range.end);
|
|
549
|
+
if (end <= start) return [];
|
|
550
|
+
const from = (0, _embedpdf_core_annotation.locateOffset)(doc, start);
|
|
551
|
+
const to = (0, _embedpdf_core_annotation.locateOffset)(doc, end);
|
|
552
|
+
const out = [];
|
|
553
|
+
for (let index = from.paragraph; index <= to.paragraph; index++) {
|
|
554
|
+
const paragraph = doc.paragraphs[index];
|
|
555
|
+
const localStart = index === from.paragraph ? from.offset : 0;
|
|
556
|
+
const localEnd = index === to.paragraph ? to.offset : paragraph.runs.reduce((n, r) => n + r.text.length, 0);
|
|
557
|
+
let pos = 0;
|
|
558
|
+
for (const run of paragraph.runs) {
|
|
559
|
+
const runEnd = pos + run.text.length;
|
|
560
|
+
if (runEnd > localStart && pos < localEnd && run.text.length > 0) out.push(run.style ?? {});
|
|
561
|
+
pos = runEnd;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
/** The values a range reads back for the range keys — resolved against the
|
|
567
|
+
* body, one value where every run agrees, `mixed` where they don't. */
|
|
568
|
+
function rangeProps(doc, range, fonts) {
|
|
569
|
+
const runs = runsInRange(doc, range);
|
|
570
|
+
const body = doc.body;
|
|
571
|
+
const resolve = (d) => ({
|
|
572
|
+
family: d.family ?? body.family,
|
|
573
|
+
weight: d.weight ?? body.weight,
|
|
574
|
+
italic: d.italic ?? body.italic,
|
|
575
|
+
size: d.size ?? body.size,
|
|
576
|
+
color: d.color ?? body.color,
|
|
577
|
+
decoration: d.decoration ?? body.decoration
|
|
578
|
+
});
|
|
579
|
+
const styles = (runs.length ? runs : [{}]).map(resolve);
|
|
580
|
+
const values = {};
|
|
581
|
+
const mixed = [];
|
|
582
|
+
const read = (key, of) => {
|
|
583
|
+
const first = of(styles[0]);
|
|
584
|
+
values[key] = first;
|
|
585
|
+
if (styles.some((s) => JSON.stringify(of(s)) !== JSON.stringify(first))) mixed.push(key);
|
|
586
|
+
};
|
|
587
|
+
read("fontFamily", (s) => fontForFace({
|
|
588
|
+
family: s.family,
|
|
589
|
+
weight: body.weight,
|
|
590
|
+
italic: body.italic
|
|
591
|
+
}, fonts));
|
|
592
|
+
read("fontSize", (s) => s.size);
|
|
593
|
+
read("fontColor", (s) => s.color.toLowerCase());
|
|
594
|
+
read("bold", (s) => (s.weight ?? 400) >= 600);
|
|
595
|
+
read("italic", (s) => !!s.italic);
|
|
596
|
+
read("underline", (s) => (s.decoration ?? []).includes("underline"));
|
|
597
|
+
return {
|
|
598
|
+
values,
|
|
599
|
+
mixed
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
//#endregion
|
|
211
603
|
//#region src/repository/kinds/freeText.ts
|
|
212
604
|
/** Free-text `/DA` fields → content {@link TextStyle}. An absent `fontColor`
|
|
213
605
|
* falls back to the `/DA` colour — the same rule the CPVT renderer applies. */
|
|
214
606
|
function textFromDTO(dto) {
|
|
607
|
+
const body = dto.richText?.body;
|
|
215
608
|
return {
|
|
216
609
|
fontFamily: dto.fontFamily,
|
|
217
610
|
fontSize: dto.fontSize,
|
|
218
611
|
fontColor: require_seam.colorToCss(dto.fontColor ?? dto.color),
|
|
219
|
-
textAlign: dto.textAlign
|
|
612
|
+
textAlign: dto.textAlign,
|
|
613
|
+
...body && body.weight >= 600 ? { bold: true } : {},
|
|
614
|
+
...body?.italic ? { italic: true } : {},
|
|
615
|
+
...body?.decoration.includes("underline") ? { underline: true } : {}
|
|
220
616
|
};
|
|
221
617
|
}
|
|
222
618
|
/**
|
|
619
|
+
* A formatting toggle is a BODY change (runs are deltas over it), so it
|
|
620
|
+
* lowers as a rich write carrying the current paragraphs — and every toggle
|
|
621
|
+
* lowers the same COMPLETE body (the current one with the three toggles
|
|
622
|
+
* applied: a partial body means engine defaults, which would reset the
|
|
623
|
+
* size, face and colour), so a patch of several merges cleanly.
|
|
624
|
+
*/
|
|
625
|
+
const formattingBody = (a) => {
|
|
626
|
+
const doc = richDocOf(a);
|
|
627
|
+
const t = a.text;
|
|
628
|
+
return { richText: {
|
|
629
|
+
body: {
|
|
630
|
+
...doc.body,
|
|
631
|
+
weight: t?.bold ? 700 : 400,
|
|
632
|
+
italic: !!t?.italic,
|
|
633
|
+
decoration: t?.underline ? ["underline"] : []
|
|
634
|
+
},
|
|
635
|
+
paragraphs: doc.paragraphs
|
|
636
|
+
} };
|
|
637
|
+
};
|
|
638
|
+
/**
|
|
223
639
|
* The engine geometry for a callout: the overall `/Rect` (text box ∪ leader ∪
|
|
224
640
|
* arrow), the `/RD` inset that recovers the text box from it, the `/CL` leader
|
|
225
641
|
* (`[tip, knee, conn]` with the connection point derived), and the `/LE`
|
|
@@ -303,6 +719,11 @@ const freeText = {
|
|
|
303
719
|
if (cf) return { ...cf };
|
|
304
720
|
return boxEmit(a, crop);
|
|
305
721
|
},
|
|
722
|
+
prop: {
|
|
723
|
+
bold: formattingBody,
|
|
724
|
+
italic: formattingBody,
|
|
725
|
+
underline: formattingBody
|
|
726
|
+
},
|
|
306
727
|
draftExtras: (a, crop) => ({
|
|
307
728
|
intent: calloutFields(a, crop) ? "free-text-callout" : "free-text",
|
|
308
729
|
contents: a.data?.contents ?? ""
|
|
@@ -650,6 +1071,14 @@ function toPatch(a, crop) {
|
|
|
650
1071
|
*/
|
|
651
1072
|
function toScopedPatch(a, scope, crop) {
|
|
652
1073
|
const kind = projectionOf(a.subtype);
|
|
1074
|
+
if (scope.kind === "caption") return a.measure ? {
|
|
1075
|
+
subtype: wireSubtypeOf(a),
|
|
1076
|
+
caption: a.measure.caption
|
|
1077
|
+
} : null;
|
|
1078
|
+
if (scope.kind === "leader") return a.measure?.intent === "LineDimension" ? {
|
|
1079
|
+
subtype: "line",
|
|
1080
|
+
leader: a.measure.leader
|
|
1081
|
+
} : null;
|
|
653
1082
|
if (scope.kind === "geometry") {
|
|
654
1083
|
const geo = kind.geometry(a, crop);
|
|
655
1084
|
return geo ? {
|
|
@@ -732,18 +1161,6 @@ function createAnnotationHoverFeed(actions, annotOf) {
|
|
|
732
1161
|
* the plugin (not the portable core) because the font→CSS stack mapping and the
|
|
733
1162
|
* engine `Color`→CSS seam are web concerns, shared across every web framework.
|
|
734
1163
|
*/
|
|
735
|
-
/** Map a free-text `/DA` font to a CSS font-family. A standard PDF font → a web
|
|
736
|
-
* stack with the same metrics; anything else is a registered font key → mount it
|
|
737
|
-
* as a `@font-face` of that family (see the framework `mountWebFont` helper). */
|
|
738
|
-
const STANDARD_FONT_CSS = {
|
|
739
|
-
helvetica: "Helvetica, Arial, sans-serif",
|
|
740
|
-
"helvetica-bold": "Helvetica, Arial, sans-serif",
|
|
741
|
-
courier: "\"Courier New\", Courier, monospace",
|
|
742
|
-
"times-roman": "\"Times New Roman\", Times, serif",
|
|
743
|
-
symbol: "serif",
|
|
744
|
-
"zapf-dingbats": "serif"
|
|
745
|
-
};
|
|
746
|
-
const cssFontFor = (font) => STANDARD_FONT_CSS[font] ?? `"${font}", sans-serif`;
|
|
747
1164
|
/** Project the model's free-text boxes into render-ready {@link TextItem}s — the
|
|
748
1165
|
* core geometry (`textBoxes`) joined with the DTO-derived CSS. Pure; memoized by
|
|
749
1166
|
* model identity at the call site so selectors get a stable reference. */
|
|
@@ -752,525 +1169,29 @@ function buildTextItems(m, pon, view) {
|
|
|
752
1169
|
const a = m.byId[tb.id];
|
|
753
1170
|
const t = a?.text ?? _embedpdf_core_annotation.initialTextStyle;
|
|
754
1171
|
const sw = a?.style.strokeWidth ?? 0;
|
|
755
|
-
const
|
|
1172
|
+
const doc = a ? richDocOf(a) : null;
|
|
756
1173
|
return {
|
|
757
1174
|
id: tb.id,
|
|
758
1175
|
ref: a?.ref ?? null,
|
|
759
1176
|
box: tb.box,
|
|
760
1177
|
contents: a?.data?.contents ?? "",
|
|
1178
|
+
richText: { paragraphs: doc ? stripBodyDefaults(doc.paragraphs, doc.body) : [{ runs: [{ text: "" }] }] },
|
|
761
1179
|
editing: tb.editing,
|
|
762
1180
|
...tb.rot ? { rot: tb.rot } : {},
|
|
763
1181
|
css: {
|
|
764
|
-
fontFamily:
|
|
1182
|
+
fontFamily: cssFontFamilyForFont(t.fontFamily),
|
|
765
1183
|
fontSize: t.fontSize,
|
|
766
|
-
lineHeight: t.fontSize,
|
|
767
1184
|
color: t.fontColor,
|
|
1185
|
+
fontWeight: t.bold ? 700 : 400,
|
|
1186
|
+
fontStyle: t.italic ? "italic" : "normal",
|
|
1187
|
+
textDecoration: t.underline ? "underline" : "none",
|
|
768
1188
|
align: t.textAlign,
|
|
769
|
-
padding:
|
|
770
|
-
background: a?.style.interiorColor ?? null
|
|
1189
|
+
padding: (0, _embedpdf_core_annotation.textPlateInset)(sw)
|
|
771
1190
|
}
|
|
772
1191
|
};
|
|
773
1192
|
});
|
|
774
1193
|
}
|
|
775
1194
|
//#endregion
|
|
776
|
-
//#region src/tools.ts
|
|
777
|
-
/**
|
|
778
|
-
* The tool `armStamp` activates — and the only tool an armed payload survives
|
|
779
|
-
* onto (plus the legacy `annotation-stamp` tag, honoured for embedder configs
|
|
780
|
-
* written before the tags were unified). Placement consults the armed payload
|
|
781
|
-
* BEFORE the active tool's own `source` spec, so a payload must not outlive
|
|
782
|
-
* its tool onto a sibling stamp preset: an `image` preset's click has to open
|
|
783
|
-
* its own picker, not silently place whatever the stamp panel last armed.
|
|
784
|
-
*/
|
|
785
|
-
const ARMED_STAMP_TOOL_ID = "stamp";
|
|
786
|
-
/**
|
|
787
|
-
* The creation properties each known authoring kind actually consumes. This is
|
|
788
|
-
* deliberately separate from the flat internal patch vocabulary: it gives tool
|
|
789
|
-
* configuration precise compile-time and runtime validation while mixed-selection
|
|
790
|
-
* edits can keep using `AnnotationPropsPatch`.
|
|
791
|
-
*/
|
|
792
|
-
const TOOL_DEFAULT_KEYS = {
|
|
793
|
-
square: [
|
|
794
|
-
"color",
|
|
795
|
-
"interiorColor",
|
|
796
|
-
"opacity",
|
|
797
|
-
"strokeWidth",
|
|
798
|
-
"border"
|
|
799
|
-
],
|
|
800
|
-
circle: [
|
|
801
|
-
"color",
|
|
802
|
-
"interiorColor",
|
|
803
|
-
"opacity",
|
|
804
|
-
"strokeWidth",
|
|
805
|
-
"border"
|
|
806
|
-
],
|
|
807
|
-
line: [
|
|
808
|
-
"color",
|
|
809
|
-
"interiorColor",
|
|
810
|
-
"opacity",
|
|
811
|
-
"strokeWidth",
|
|
812
|
-
"border",
|
|
813
|
-
"lineEndings"
|
|
814
|
-
],
|
|
815
|
-
polygon: [
|
|
816
|
-
"color",
|
|
817
|
-
"interiorColor",
|
|
818
|
-
"opacity",
|
|
819
|
-
"strokeWidth",
|
|
820
|
-
"border"
|
|
821
|
-
],
|
|
822
|
-
polyline: [
|
|
823
|
-
"color",
|
|
824
|
-
"interiorColor",
|
|
825
|
-
"opacity",
|
|
826
|
-
"strokeWidth",
|
|
827
|
-
"border",
|
|
828
|
-
"lineEndings"
|
|
829
|
-
],
|
|
830
|
-
ink: [
|
|
831
|
-
"color",
|
|
832
|
-
"opacity",
|
|
833
|
-
"strokeWidth",
|
|
834
|
-
"blendMode"
|
|
835
|
-
],
|
|
836
|
-
"free-text": [
|
|
837
|
-
"fontFamily",
|
|
838
|
-
"fontSize",
|
|
839
|
-
"fontColor",
|
|
840
|
-
"textAlign",
|
|
841
|
-
"opacity",
|
|
842
|
-
"interiorColor",
|
|
843
|
-
"color",
|
|
844
|
-
"strokeWidth",
|
|
845
|
-
"border"
|
|
846
|
-
],
|
|
847
|
-
"free-text-callout": [
|
|
848
|
-
"fontFamily",
|
|
849
|
-
"fontSize",
|
|
850
|
-
"fontColor",
|
|
851
|
-
"textAlign",
|
|
852
|
-
"opacity",
|
|
853
|
-
"interiorColor",
|
|
854
|
-
"color",
|
|
855
|
-
"strokeWidth",
|
|
856
|
-
"border",
|
|
857
|
-
"lineEndings"
|
|
858
|
-
],
|
|
859
|
-
highlight: [
|
|
860
|
-
"color",
|
|
861
|
-
"opacity",
|
|
862
|
-
"blendMode"
|
|
863
|
-
],
|
|
864
|
-
underline: [
|
|
865
|
-
"color",
|
|
866
|
-
"opacity",
|
|
867
|
-
"blendMode"
|
|
868
|
-
],
|
|
869
|
-
strikeout: [
|
|
870
|
-
"color",
|
|
871
|
-
"opacity",
|
|
872
|
-
"blendMode"
|
|
873
|
-
],
|
|
874
|
-
squiggly: [
|
|
875
|
-
"color",
|
|
876
|
-
"opacity",
|
|
877
|
-
"blendMode"
|
|
878
|
-
],
|
|
879
|
-
caret: ["color", "opacity"],
|
|
880
|
-
redact: [
|
|
881
|
-
"color",
|
|
882
|
-
"interiorColor",
|
|
883
|
-
"opacity",
|
|
884
|
-
"fontFamily",
|
|
885
|
-
"fontSize",
|
|
886
|
-
"fontColor",
|
|
887
|
-
"textAlign"
|
|
888
|
-
],
|
|
889
|
-
stamp: [],
|
|
890
|
-
link: ["link"],
|
|
891
|
-
text: [
|
|
892
|
-
"icon",
|
|
893
|
-
"color",
|
|
894
|
-
"opacity"
|
|
895
|
-
],
|
|
896
|
-
"file-attachment": [
|
|
897
|
-
"icon",
|
|
898
|
-
"color",
|
|
899
|
-
"opacity"
|
|
900
|
-
],
|
|
901
|
-
"widget-text": [
|
|
902
|
-
"color",
|
|
903
|
-
"interiorColor",
|
|
904
|
-
"strokeWidth",
|
|
905
|
-
"border",
|
|
906
|
-
"fontFamily",
|
|
907
|
-
"fontSize",
|
|
908
|
-
"fontColor",
|
|
909
|
-
"textAlign"
|
|
910
|
-
],
|
|
911
|
-
"widget-choice": [
|
|
912
|
-
"color",
|
|
913
|
-
"interiorColor",
|
|
914
|
-
"strokeWidth",
|
|
915
|
-
"border",
|
|
916
|
-
"fontFamily",
|
|
917
|
-
"fontSize",
|
|
918
|
-
"fontColor",
|
|
919
|
-
"textAlign"
|
|
920
|
-
],
|
|
921
|
-
"widget-toggle": [
|
|
922
|
-
"color",
|
|
923
|
-
"interiorColor",
|
|
924
|
-
"strokeWidth",
|
|
925
|
-
"border"
|
|
926
|
-
]
|
|
927
|
-
};
|
|
928
|
-
const DRAW_TAGS = ["annotation-draw", "annotation-edit"];
|
|
929
|
-
const MARKUP_TAGS = ["text-select", "annotation-edit"];
|
|
930
|
-
/**
|
|
931
|
-
* Whether arming this ANNOTATION tool is touch consent (`Tool.touchDirect`):
|
|
932
|
-
* drag-CREATE tools — shape/ink draw and text-markup select — take
|
|
933
|
-
* single-finger touch wholesale (finger draws/marks, two fingers navigate:
|
|
934
|
-
* the drawing-app convention). Click-to-PLACE tools (stamp, note) do not: a
|
|
935
|
-
* tap places without any claim, and drags keep scrolling. Derived from the
|
|
936
|
-
* gesture tags, so custom registered tools inherit the right behavior. Only
|
|
937
|
-
* annotation-owned tools run through this — the built-in `pointer` tool also
|
|
938
|
-
* enables `text-select`, but it is registered by the hub, not here.
|
|
939
|
-
*/
|
|
940
|
-
const isTouchDirect = (enables) => enables.has("annotation-draw") || enables.has("text-select");
|
|
941
|
-
/**
|
|
942
|
-
* The built-in tools — a data mirror of v2's registrations (shapes + lines + ink
|
|
943
|
-
* + free text in the draw channel, text markup + caret behind text selection, and
|
|
944
|
-
* the click-to-place stamp). Order is display-neutral; the toolbar owns layout.
|
|
945
|
-
*/
|
|
946
|
-
const DEFAULT_TOOLS = [
|
|
947
|
-
{
|
|
948
|
-
id: "square",
|
|
949
|
-
subtype: "square",
|
|
950
|
-
cursor: "crosshair",
|
|
951
|
-
enables: DRAW_TAGS,
|
|
952
|
-
defaults: { strokeWidth: 6 },
|
|
953
|
-
clickCreate: {
|
|
954
|
-
width: 80,
|
|
955
|
-
height: 60
|
|
956
|
-
}
|
|
957
|
-
},
|
|
958
|
-
{
|
|
959
|
-
id: "circle",
|
|
960
|
-
subtype: "circle",
|
|
961
|
-
cursor: "crosshair",
|
|
962
|
-
enables: DRAW_TAGS,
|
|
963
|
-
defaults: { strokeWidth: 6 },
|
|
964
|
-
clickCreate: {
|
|
965
|
-
width: 80,
|
|
966
|
-
height: 60
|
|
967
|
-
}
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
id: "line",
|
|
971
|
-
subtype: "line",
|
|
972
|
-
cursor: "crosshair",
|
|
973
|
-
enables: DRAW_TAGS,
|
|
974
|
-
defaults: { strokeWidth: 6 },
|
|
975
|
-
clickCreate: { length: 80 }
|
|
976
|
-
},
|
|
977
|
-
{
|
|
978
|
-
id: "polygon",
|
|
979
|
-
subtype: "polygon",
|
|
980
|
-
cursor: "crosshair",
|
|
981
|
-
enables: DRAW_TAGS,
|
|
982
|
-
defaults: { strokeWidth: 6 }
|
|
983
|
-
},
|
|
984
|
-
{
|
|
985
|
-
id: "polyline",
|
|
986
|
-
subtype: "polyline",
|
|
987
|
-
cursor: "crosshair",
|
|
988
|
-
enables: DRAW_TAGS,
|
|
989
|
-
defaults: { strokeWidth: 6 }
|
|
990
|
-
},
|
|
991
|
-
{
|
|
992
|
-
id: "ink",
|
|
993
|
-
subtype: "ink",
|
|
994
|
-
cursor: "crosshair",
|
|
995
|
-
enables: DRAW_TAGS,
|
|
996
|
-
defaults: {
|
|
997
|
-
color: "#ef4444",
|
|
998
|
-
strokeWidth: 6,
|
|
999
|
-
blendMode: "normal"
|
|
1000
|
-
},
|
|
1001
|
-
ink: { groupStrokesMs: 800 }
|
|
1002
|
-
},
|
|
1003
|
-
{
|
|
1004
|
-
id: "ink-highlight",
|
|
1005
|
-
extends: "ink",
|
|
1006
|
-
intent: "ink-highlight",
|
|
1007
|
-
defaults: {
|
|
1008
|
-
color: "#ffcd45",
|
|
1009
|
-
strokeWidth: 14,
|
|
1010
|
-
blendMode: "multiply"
|
|
1011
|
-
},
|
|
1012
|
-
ink: { straighten: {
|
|
1013
|
-
deviationThreshold: .15,
|
|
1014
|
-
axisSnapDegrees: 15
|
|
1015
|
-
} }
|
|
1016
|
-
},
|
|
1017
|
-
{
|
|
1018
|
-
id: "free-text",
|
|
1019
|
-
subtype: "free-text",
|
|
1020
|
-
cursor: "crosshair",
|
|
1021
|
-
enables: DRAW_TAGS,
|
|
1022
|
-
defaults: { fontColor: "#ef4444" },
|
|
1023
|
-
upright: true,
|
|
1024
|
-
clickCreate: {
|
|
1025
|
-
width: 180,
|
|
1026
|
-
height: 40,
|
|
1027
|
-
anchor: "top-left"
|
|
1028
|
-
}
|
|
1029
|
-
},
|
|
1030
|
-
{
|
|
1031
|
-
id: "free-text-callout",
|
|
1032
|
-
subtype: "free-text-callout",
|
|
1033
|
-
propsKind: "free-text",
|
|
1034
|
-
cursor: "crosshair",
|
|
1035
|
-
enables: DRAW_TAGS,
|
|
1036
|
-
defaults: {
|
|
1037
|
-
strokeWidth: 6,
|
|
1038
|
-
lineEndings: { end: "open-arrow" }
|
|
1039
|
-
},
|
|
1040
|
-
upright: true
|
|
1041
|
-
},
|
|
1042
|
-
{
|
|
1043
|
-
id: "highlight",
|
|
1044
|
-
subtype: "highlight",
|
|
1045
|
-
cursor: "default",
|
|
1046
|
-
enables: MARKUP_TAGS,
|
|
1047
|
-
defaults: {
|
|
1048
|
-
color: "#ffe16a",
|
|
1049
|
-
blendMode: "multiply"
|
|
1050
|
-
},
|
|
1051
|
-
selection: { kind: "markup" }
|
|
1052
|
-
},
|
|
1053
|
-
{
|
|
1054
|
-
id: "redact",
|
|
1055
|
-
subtype: "redact",
|
|
1056
|
-
cursor: "crosshair",
|
|
1057
|
-
enables: [
|
|
1058
|
-
"text-select",
|
|
1059
|
-
"annotation-draw",
|
|
1060
|
-
"annotation-edit"
|
|
1061
|
-
],
|
|
1062
|
-
defaults: {
|
|
1063
|
-
color: "#e44234",
|
|
1064
|
-
interiorColor: "#000000",
|
|
1065
|
-
fontColor: "#ffffff",
|
|
1066
|
-
opacity: 1
|
|
1067
|
-
},
|
|
1068
|
-
selection: { kind: "markup" }
|
|
1069
|
-
},
|
|
1070
|
-
{
|
|
1071
|
-
id: "underline",
|
|
1072
|
-
subtype: "underline",
|
|
1073
|
-
cursor: "default",
|
|
1074
|
-
enables: MARKUP_TAGS,
|
|
1075
|
-
defaults: { color: "#ef4444" },
|
|
1076
|
-
selection: { kind: "markup" }
|
|
1077
|
-
},
|
|
1078
|
-
{
|
|
1079
|
-
id: "strikeout",
|
|
1080
|
-
subtype: "strikeout",
|
|
1081
|
-
cursor: "default",
|
|
1082
|
-
enables: MARKUP_TAGS,
|
|
1083
|
-
defaults: { color: "#ef4444" },
|
|
1084
|
-
selection: { kind: "markup" }
|
|
1085
|
-
},
|
|
1086
|
-
{
|
|
1087
|
-
id: "squiggly",
|
|
1088
|
-
subtype: "squiggly",
|
|
1089
|
-
cursor: "default",
|
|
1090
|
-
enables: MARKUP_TAGS,
|
|
1091
|
-
defaults: { color: "#ef4444" },
|
|
1092
|
-
selection: { kind: "markup" }
|
|
1093
|
-
},
|
|
1094
|
-
{
|
|
1095
|
-
id: "insert-text",
|
|
1096
|
-
subtype: "caret",
|
|
1097
|
-
propsKind: "caret",
|
|
1098
|
-
preset: "caret",
|
|
1099
|
-
cursor: "default",
|
|
1100
|
-
enables: MARKUP_TAGS,
|
|
1101
|
-
defaults: { color: "#ef4444" },
|
|
1102
|
-
selection: {
|
|
1103
|
-
kind: "text-edit",
|
|
1104
|
-
operation: "insert"
|
|
1105
|
-
}
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
id: "replace-text",
|
|
1109
|
-
subtype: "strikeout",
|
|
1110
|
-
propsKind: "strikeout",
|
|
1111
|
-
cursor: "default",
|
|
1112
|
-
enables: MARKUP_TAGS,
|
|
1113
|
-
defaults: { color: "#ef4444" },
|
|
1114
|
-
selection: {
|
|
1115
|
-
kind: "text-edit",
|
|
1116
|
-
operation: "replace"
|
|
1117
|
-
}
|
|
1118
|
-
},
|
|
1119
|
-
{
|
|
1120
|
-
id: ARMED_STAMP_TOOL_ID,
|
|
1121
|
-
subtype: "stamp",
|
|
1122
|
-
cursor: "copy",
|
|
1123
|
-
enables: ["annotation-place", "annotation-edit"],
|
|
1124
|
-
source: {
|
|
1125
|
-
kind: "prompt",
|
|
1126
|
-
accept: "image/png,image/jpeg,application/pdf"
|
|
1127
|
-
},
|
|
1128
|
-
upright: true,
|
|
1129
|
-
ghost: { mode: "footprint" }
|
|
1130
|
-
},
|
|
1131
|
-
{
|
|
1132
|
-
id: "link",
|
|
1133
|
-
subtype: "link",
|
|
1134
|
-
cursor: "crosshair",
|
|
1135
|
-
enables: DRAW_TAGS
|
|
1136
|
-
},
|
|
1137
|
-
{
|
|
1138
|
-
id: "note",
|
|
1139
|
-
subtype: "text",
|
|
1140
|
-
cursor: "crosshair",
|
|
1141
|
-
enables: ["annotation-place", "annotation-edit"],
|
|
1142
|
-
defaults: {
|
|
1143
|
-
icon: "comment",
|
|
1144
|
-
color: "#facc15"
|
|
1145
|
-
},
|
|
1146
|
-
flags: {
|
|
1147
|
-
noZoom: true,
|
|
1148
|
-
noRotate: true
|
|
1149
|
-
},
|
|
1150
|
-
upright: true,
|
|
1151
|
-
ghost: { mode: "footprint" }
|
|
1152
|
-
},
|
|
1153
|
-
{
|
|
1154
|
-
id: "attachment",
|
|
1155
|
-
subtype: "file-attachment",
|
|
1156
|
-
cursor: "copy",
|
|
1157
|
-
enables: ["annotation-place", "annotation-edit"],
|
|
1158
|
-
source: { kind: "prompt" },
|
|
1159
|
-
defaults: {
|
|
1160
|
-
icon: "paperclip",
|
|
1161
|
-
color: "#facc15"
|
|
1162
|
-
},
|
|
1163
|
-
flags: {
|
|
1164
|
-
noZoom: true,
|
|
1165
|
-
noRotate: true
|
|
1166
|
-
},
|
|
1167
|
-
upright: true,
|
|
1168
|
-
ghost: { mode: "footprint" }
|
|
1169
|
-
}
|
|
1170
|
-
];
|
|
1171
|
-
/** Merge two default patches, `b` over `a`, with line endings merged per side. */
|
|
1172
|
-
function mergeDefaults(a, b) {
|
|
1173
|
-
if (!a) return b;
|
|
1174
|
-
if (!b) return a;
|
|
1175
|
-
const merged = {
|
|
1176
|
-
...a,
|
|
1177
|
-
...b
|
|
1178
|
-
};
|
|
1179
|
-
if (a.lineEndings || b.lineEndings) merged.lineEndings = {
|
|
1180
|
-
...a.lineEndings,
|
|
1181
|
-
...b.lineEndings
|
|
1182
|
-
};
|
|
1183
|
-
return merged;
|
|
1184
|
-
}
|
|
1185
|
-
/** Overlay a same-id override onto a base definition (configure a built-in). */
|
|
1186
|
-
function mergeDef(base, over) {
|
|
1187
|
-
return {
|
|
1188
|
-
...base,
|
|
1189
|
-
...over,
|
|
1190
|
-
enables: over.enables ?? base.enables,
|
|
1191
|
-
meta: base.meta || over.meta ? {
|
|
1192
|
-
...base.meta,
|
|
1193
|
-
...over.meta
|
|
1194
|
-
} : void 0,
|
|
1195
|
-
defaults: mergeDefaults(base.defaults, over.defaults),
|
|
1196
|
-
flags: base.flags || over.flags ? {
|
|
1197
|
-
...base.flags,
|
|
1198
|
-
...over.flags
|
|
1199
|
-
} : void 0,
|
|
1200
|
-
ink: base.ink || over.ink ? {
|
|
1201
|
-
...base.ink,
|
|
1202
|
-
...over.ink
|
|
1203
|
-
} : void 0
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
function validateDefaults(tool) {
|
|
1207
|
-
if (!tool.defaults) return;
|
|
1208
|
-
const allowed = TOOL_DEFAULT_KEYS[tool.subtype];
|
|
1209
|
-
if (!allowed) return;
|
|
1210
|
-
const keys = new Set(allowed);
|
|
1211
|
-
for (const key of Object.keys(tool.defaults)) if (!keys.has(key)) throw new Error(`[annotation] tool '${tool.id}' does not support default '${key}' for '${tool.subtype}'`);
|
|
1212
|
-
}
|
|
1213
|
-
/**
|
|
1214
|
-
* Resolve {@link DEFAULT_TOOLS} + embedder overrides into the ready-to-use table.
|
|
1215
|
-
* A same-id override merges over the built-in; a new id adds a tool; `extends`
|
|
1216
|
-
* inherits from an already-defined tool. Keyed by id, so lookups are O(1).
|
|
1217
|
-
*/
|
|
1218
|
-
function buildToolRegistry(overrides = []) {
|
|
1219
|
-
const defs = /* @__PURE__ */ new Map();
|
|
1220
|
-
for (const d of DEFAULT_TOOLS) defs.set(d.id, d);
|
|
1221
|
-
for (const o of overrides) {
|
|
1222
|
-
const prev = defs.get(o.id);
|
|
1223
|
-
defs.set(o.id, prev ? mergeDef(prev, o) : o);
|
|
1224
|
-
}
|
|
1225
|
-
const out = /* @__PURE__ */ new Map();
|
|
1226
|
-
const resolving = /* @__PURE__ */ new Set();
|
|
1227
|
-
const resolve = (id) => {
|
|
1228
|
-
const cached = out.get(id);
|
|
1229
|
-
if (cached) return cached;
|
|
1230
|
-
const def = defs.get(id);
|
|
1231
|
-
if (!def) throw new Error(`[annotation] tool '${id}' extends an unknown tool`);
|
|
1232
|
-
let base;
|
|
1233
|
-
if (def.extends && def.extends !== id && !resolving.has(def.extends)) {
|
|
1234
|
-
resolving.add(id);
|
|
1235
|
-
base = resolve(def.extends);
|
|
1236
|
-
resolving.delete(id);
|
|
1237
|
-
}
|
|
1238
|
-
const subtype = def.subtype ?? base?.subtype ?? def.id;
|
|
1239
|
-
const resolved = {
|
|
1240
|
-
id: def.id,
|
|
1241
|
-
subtype,
|
|
1242
|
-
preset: def.preset ?? def.id,
|
|
1243
|
-
propsKind: def.propsKind ?? base?.propsKind ?? subtype,
|
|
1244
|
-
cursor: def.cursor ?? base?.cursor ?? "crosshair",
|
|
1245
|
-
enables: new Set(def.enables ?? (base ? [...base.enables] : [])),
|
|
1246
|
-
defaults: mergeDefaults(base?.defaults, def.defaults),
|
|
1247
|
-
flags: base?.flags || def.flags ? {
|
|
1248
|
-
...base?.flags,
|
|
1249
|
-
...def.flags
|
|
1250
|
-
} : void 0,
|
|
1251
|
-
source: def.source ?? base?.source,
|
|
1252
|
-
selection: def.selection ?? base?.selection,
|
|
1253
|
-
intent: def.intent ?? base?.intent,
|
|
1254
|
-
ink: base?.ink || def.ink ? {
|
|
1255
|
-
...base?.ink,
|
|
1256
|
-
...def.ink
|
|
1257
|
-
} : void 0,
|
|
1258
|
-
upright: def.upright ?? base?.upright ?? false,
|
|
1259
|
-
clickCreate: def.clickCreate ?? base?.clickCreate ?? false,
|
|
1260
|
-
ghost: def.ghost ?? base?.ghost ?? false,
|
|
1261
|
-
meta: base?.meta || def.meta ? {
|
|
1262
|
-
...base?.meta,
|
|
1263
|
-
...def.meta
|
|
1264
|
-
} : void 0
|
|
1265
|
-
};
|
|
1266
|
-
validateDefaults(resolved);
|
|
1267
|
-
out.set(id, resolved);
|
|
1268
|
-
return resolved;
|
|
1269
|
-
};
|
|
1270
|
-
for (const id of defs.keys()) resolve(id);
|
|
1271
|
-
return out;
|
|
1272
|
-
}
|
|
1273
|
-
//#endregion
|
|
1274
1195
|
//#region src/placement.ts
|
|
1275
1196
|
/**
|
|
1276
1197
|
* Per-kind CODE for the click-to-place ICON kinds (note / file attachment)
|
|
@@ -1337,14 +1258,23 @@ const TEXT_COMMIT_DEBOUNCE_MS = 250;
|
|
|
1337
1258
|
*/
|
|
1338
1259
|
function createAnnotationCapability(ctx, config = {}) {
|
|
1339
1260
|
const behaviors = [];
|
|
1340
|
-
/** Per-annotation debounce timer for the engine
|
|
1261
|
+
/** Per-annotation debounce timer for the engine text write while typing. */
|
|
1341
1262
|
const textTimers = /* @__PURE__ */ new Map();
|
|
1263
|
+
/** The registered fonts, for face ↔ key mapping (the local engine's list;
|
|
1264
|
+
* the cloud engine registers none). */
|
|
1265
|
+
const fonts = () => ctx.engine?.fonts?.list() ?? [];
|
|
1342
1266
|
const configTools = config.tools ?? [];
|
|
1343
|
-
const registry = buildToolRegistry(configTools);
|
|
1267
|
+
const registry = require_tools.buildToolRegistry(configTools);
|
|
1344
1268
|
/** The installed file-picker port (a DOM file dialog, wired by the framework
|
|
1345
1269
|
* adapter), or null — every click-then-pick tool resolves through this ONE
|
|
1346
1270
|
* slot. See {@link FilePickerProvider}. */
|
|
1347
1271
|
let filePickerProvider = null;
|
|
1272
|
+
const pageViewports = /* @__PURE__ */ new Map();
|
|
1273
|
+
const captured = /* @__PURE__ */ new Set();
|
|
1274
|
+
ctx.cleanup(() => {
|
|
1275
|
+
captured.clear();
|
|
1276
|
+
pageViewports.clear();
|
|
1277
|
+
});
|
|
1348
1278
|
const model = () => ctx.getState().model;
|
|
1349
1279
|
const actionsForHover = ctx.tryGet(_embedpdf_plugin_actions_contract.ActionsToken);
|
|
1350
1280
|
const hoverFeed = actionsForHover ? createAnnotationHoverFeed(actionsForHover, (id) => model().byId[id] ?? null) : null;
|
|
@@ -1462,10 +1392,18 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1462
1392
|
};
|
|
1463
1393
|
return v;
|
|
1464
1394
|
};
|
|
1395
|
+
/** The editor's text RANGE inside the annotation being edited, or null
|
|
1396
|
+
* (no editor selection, a bare caret, or a selection left behind by a
|
|
1397
|
+
* previous edit). The one condition that routes the range keys to runs. */
|
|
1398
|
+
const activeTextRange = (m) => {
|
|
1399
|
+
const ts = ctx.getState().textSelection;
|
|
1400
|
+
return ts && m.editing === ts.id && ts.end > ts.start && m.byId[ts.id] ? ts : null;
|
|
1401
|
+
};
|
|
1465
1402
|
let selPropsCache = null;
|
|
1466
1403
|
const memoSelectionProps = () => {
|
|
1467
1404
|
const m = model();
|
|
1468
|
-
|
|
1405
|
+
const range = activeTextRange(m);
|
|
1406
|
+
if (selPropsCache && selPropsCache.model === m && selPropsCache.range === range) return selPropsCache.v;
|
|
1469
1407
|
const members = m.selected.map((id) => m.byId[id]).filter((a) => !!a);
|
|
1470
1408
|
const specs = (0, _embedpdf_core_annotation.sharedProps)(members.map((a) => a.subtype));
|
|
1471
1409
|
const values = {};
|
|
@@ -1477,6 +1415,16 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1477
1415
|
const firstJson = JSON.stringify(first);
|
|
1478
1416
|
if (members.some((a) => JSON.stringify(valueOf(a, spec.key)) !== firstJson)) mixed.push(spec.key);
|
|
1479
1417
|
}
|
|
1418
|
+
if (range && members.length === 1 && members[0].id === range.id) {
|
|
1419
|
+
const rp = rangeProps(richDocOf(members[0], fonts), range, fonts);
|
|
1420
|
+
for (const spec of specs) {
|
|
1421
|
+
if (!RANGE_KEYS.includes(spec.key)) continue;
|
|
1422
|
+
values[spec.key] = rp.values[spec.key];
|
|
1423
|
+
const i = mixed.indexOf(spec.key);
|
|
1424
|
+
if (i >= 0) mixed.splice(i, 1);
|
|
1425
|
+
if (rp.mixed.includes(spec.key)) mixed.push(spec.key);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1480
1428
|
const v = {
|
|
1481
1429
|
specs,
|
|
1482
1430
|
values,
|
|
@@ -1484,6 +1432,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1484
1432
|
};
|
|
1485
1433
|
selPropsCache = {
|
|
1486
1434
|
model: m,
|
|
1435
|
+
range,
|
|
1487
1436
|
v
|
|
1488
1437
|
};
|
|
1489
1438
|
return v;
|
|
@@ -1625,7 +1574,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1625
1574
|
...input.subject !== void 0 ? { subject: input.subject } : {}
|
|
1626
1575
|
};
|
|
1627
1576
|
ctx.dispatch({ type: "STAMP_ARM_CHANGED" });
|
|
1628
|
-
ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.activateTool(ARMED_STAMP_TOOL_ID);
|
|
1577
|
+
ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.activateTool(require_tools.ARMED_STAMP_TOOL_ID);
|
|
1629
1578
|
};
|
|
1630
1579
|
const disarmStamp = () => {
|
|
1631
1580
|
if (!armedStamp) return;
|
|
@@ -1922,8 +1871,8 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1922
1871
|
* changed the appearance (create / restyle / resize), `'baked'` when the AP is
|
|
1923
1872
|
* still authoritative (a move, which preserves it, or a remote edit).
|
|
1924
1873
|
* `bumpAp` marks the upsert as confirming an engine /AP re-bake with new
|
|
1925
|
-
* content,
|
|
1926
|
-
*
|
|
1874
|
+
* content, advancing the annotation's `apVersion`. Only baked annotations
|
|
1875
|
+
* contribute to the page's appearance epoch and trigger a raster fetch.
|
|
1927
1876
|
*/
|
|
1928
1877
|
const syncDTO = (dto, source, bumpAp = false) => {
|
|
1929
1878
|
const crop = cropOf(dto.pageObjectNumber);
|
|
@@ -1941,7 +1890,35 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
1941
1890
|
const pon = ponForRef(ref);
|
|
1942
1891
|
if (pon == null) throw new Error("[annotation] cannot resolve page for ref");
|
|
1943
1892
|
const res = await doc.page(pon).annotations.update(ref, patch);
|
|
1944
|
-
syncDTO(res.updated, "vector");
|
|
1893
|
+
syncDTO(res.updated, "vector", res.appearance?.changed);
|
|
1894
|
+
};
|
|
1895
|
+
const commitText = (ref) => {
|
|
1896
|
+
const key = require_seam.refKey(ref);
|
|
1897
|
+
clearTimeout(textTimers.get(key));
|
|
1898
|
+
textTimers.delete(key);
|
|
1899
|
+
const a = model().byId[key];
|
|
1900
|
+
const pon = ponForRef(ref);
|
|
1901
|
+
if (!a || pon == null) return;
|
|
1902
|
+
const patch = textCommitPatch(a, richDocOf(a, fonts).paragraphs, fonts);
|
|
1903
|
+
ctx.doc?.page(pon).annotations.update(ref, {
|
|
1904
|
+
subtype: "free-text",
|
|
1905
|
+
...patch
|
|
1906
|
+
}).then(() => {}, () => {});
|
|
1907
|
+
};
|
|
1908
|
+
const scheduleTextCommit = (ref) => {
|
|
1909
|
+
const key = require_seam.refKey(ref);
|
|
1910
|
+
clearTimeout(textTimers.get(key));
|
|
1911
|
+
textTimers.set(key, setTimeout(() => commitText(ref), TEXT_COMMIT_DEBOUNCE_MS));
|
|
1912
|
+
};
|
|
1913
|
+
const flushTextCommits = () => {
|
|
1914
|
+
for (const key of [...textTimers.keys()]) {
|
|
1915
|
+
const a = model().byId[key];
|
|
1916
|
+
if (a?.ref) commitText(a.ref);
|
|
1917
|
+
else {
|
|
1918
|
+
clearTimeout(textTimers.get(key));
|
|
1919
|
+
textTimers.delete(key);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1945
1922
|
};
|
|
1946
1923
|
function apply(msg) {
|
|
1947
1924
|
const [next, effects] = (0, _embedpdf_core_annotation.update)(model(), msg);
|
|
@@ -2214,7 +2191,9 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2214
2191
|
})).ref;
|
|
2215
2192
|
},
|
|
2216
2193
|
edit: async (ref, text) => {
|
|
2217
|
-
const
|
|
2194
|
+
const data = model().byId[require_seam.refKey(ref)]?.data;
|
|
2195
|
+
if (data && (0, _embedpdf_engine_core_runtime.isDimension)(data)) throw new Error("[annotation] measurement contents are derived");
|
|
2196
|
+
const subtype = data?.subtype;
|
|
2218
2197
|
if (!subtype) throw new Error("[annotation] cannot edit an uncommitted annotation");
|
|
2219
2198
|
await updateOne(ref, {
|
|
2220
2199
|
subtype,
|
|
@@ -2284,7 +2263,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2284
2263
|
canSetStatus: allowsCreate(),
|
|
2285
2264
|
canEditText: (() => {
|
|
2286
2265
|
const a = model().byId[require_seam.refKey(ref)];
|
|
2287
|
-
return !!a && (0, _embedpdf_core_annotation.annotContentsEditable)(a);
|
|
2266
|
+
return !!a && !(a.data && (0, _embedpdf_engine_core_runtime.isDimension)(a.data)) && (0, _embedpdf_core_annotation.annotContentsEditable)(a);
|
|
2288
2267
|
})(),
|
|
2289
2268
|
canDelete: deletableOne(ref),
|
|
2290
2269
|
canDeleteThread: t !== null && threadMemberRefs(t).every(deletableOne)
|
|
@@ -2395,7 +2374,21 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2395
2374
|
function perform(fx, m) {
|
|
2396
2375
|
const doc = ctx.doc;
|
|
2397
2376
|
if (!doc) return;
|
|
2398
|
-
if (fx.fx === "
|
|
2377
|
+
if (fx.fx === "captured") {
|
|
2378
|
+
const crop = cropOf(fx.pon);
|
|
2379
|
+
if (crop && fx.geom.t === "line") {
|
|
2380
|
+
const pdf = (p) => ({
|
|
2381
|
+
x: p.x + crop.left,
|
|
2382
|
+
y: crop.top - p.y
|
|
2383
|
+
});
|
|
2384
|
+
for (const cb of captured) cb({
|
|
2385
|
+
tool: fx.tool,
|
|
2386
|
+
pon: fx.pon,
|
|
2387
|
+
from: pdf(fx.geom.a),
|
|
2388
|
+
to: pdf(fx.geom.b)
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
} else if (fx.fx === "createGroup") {
|
|
2399
2392
|
const ids = [fx.primary, ...fx.members];
|
|
2400
2393
|
const annots = ids.map((id) => m.byId[id]);
|
|
2401
2394
|
const primary = annots[0];
|
|
@@ -2559,7 +2552,92 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2559
2552
|
if (patch.flags) out.flags = patch.flags;
|
|
2560
2553
|
return out;
|
|
2561
2554
|
};
|
|
2555
|
+
const updateSelection = (patch) => {
|
|
2556
|
+
const m = model();
|
|
2557
|
+
const range = activeTextRange(m);
|
|
2558
|
+
if (range) {
|
|
2559
|
+
const a = m.byId[range.id];
|
|
2560
|
+
const { delta, rest } = runDeltaForProps(patch, fonts);
|
|
2561
|
+
if (Object.keys(delta).length) {
|
|
2562
|
+
const next = (0, _embedpdf_core_annotation.applyStyleToRange)({ paragraphs: richDocOf(a, fonts).paragraphs }, range, delta);
|
|
2563
|
+
apply({
|
|
2564
|
+
t: "setRichText",
|
|
2565
|
+
id: range.id,
|
|
2566
|
+
doc: next
|
|
2567
|
+
});
|
|
2568
|
+
if (a.ref) scheduleTextCommit(a.ref);
|
|
2569
|
+
}
|
|
2570
|
+
if (Object.keys(rest).length) {
|
|
2571
|
+
flushTextCommits();
|
|
2572
|
+
apply({
|
|
2573
|
+
t: "setProps",
|
|
2574
|
+
patch: rest
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
if (m.editing) flushTextCommits();
|
|
2580
|
+
apply({
|
|
2581
|
+
t: "setProps",
|
|
2582
|
+
patch
|
|
2583
|
+
});
|
|
2584
|
+
};
|
|
2562
2585
|
return {
|
|
2586
|
+
setPageViewports: (pon, viewports, fallback) => {
|
|
2587
|
+
pageViewports.set(pon, {
|
|
2588
|
+
viewports,
|
|
2589
|
+
fallback
|
|
2590
|
+
});
|
|
2591
|
+
},
|
|
2592
|
+
onDraftCaptured: (cb) => {
|
|
2593
|
+
captured.add(cb);
|
|
2594
|
+
return () => {
|
|
2595
|
+
captured.delete(cb);
|
|
2596
|
+
};
|
|
2597
|
+
},
|
|
2598
|
+
remeasurePage: async (pon, scale) => {
|
|
2599
|
+
await rehydrate();
|
|
2600
|
+
const report = {
|
|
2601
|
+
pon,
|
|
2602
|
+
scale,
|
|
2603
|
+
updated: [],
|
|
2604
|
+
skipped: [],
|
|
2605
|
+
failed: []
|
|
2606
|
+
};
|
|
2607
|
+
const hydration = ctx.getState().hydration;
|
|
2608
|
+
if (hydration.status !== "complete") {
|
|
2609
|
+
report.error = (0, _embedpdf_engine_core_runtime.serializeError)(hydration.status === "error" ? hydration.error : /* @__PURE__ */ new Error("Annotation hydration is incomplete"));
|
|
2610
|
+
return report;
|
|
2611
|
+
}
|
|
2612
|
+
const candidates = Object.values(model().byId).filter((a) => a.pon === pon && a.data && (0, _embedpdf_engine_core_runtime.isDimension)(a.data));
|
|
2613
|
+
for (const a of candidates) {
|
|
2614
|
+
const dto = a.data, ref = dto.ref;
|
|
2615
|
+
const reason = a.authority?.update === false ? "no-authority" : !(0, _embedpdf_core_annotation.annotTransformable)(a) || !(0, _embedpdf_core_annotation.annotContentsEditable)(a) ? "locked" : "measure" in dto && dto.measure && dto.measure.subtype !== "RL" ? "foreign-measure" : !(0, _embedpdf_engine_core_runtime.isReadout)((0, _embedpdf_engine_core_runtime.measurementReadout)({
|
|
2616
|
+
...dto,
|
|
2617
|
+
measure: scale
|
|
2618
|
+
})) ? "unavailable" : null;
|
|
2619
|
+
if (reason) {
|
|
2620
|
+
report.skipped.push({
|
|
2621
|
+
ref,
|
|
2622
|
+
reason
|
|
2623
|
+
});
|
|
2624
|
+
continue;
|
|
2625
|
+
}
|
|
2626
|
+
try {
|
|
2627
|
+
await updateOne(ref, {
|
|
2628
|
+
subtype: dto.subtype,
|
|
2629
|
+
measure: scale
|
|
2630
|
+
});
|
|
2631
|
+
report.updated.push(ref);
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
report.failed.push({
|
|
2634
|
+
ref,
|
|
2635
|
+
error: (0, _embedpdf_engine_core_runtime.serializeError)(error)
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return report;
|
|
2640
|
+
},
|
|
2563
2641
|
create: async (pon, draft) => {
|
|
2564
2642
|
const doc = ctx.doc;
|
|
2565
2643
|
if (!doc) throw new Error("[annotation] no document bound");
|
|
@@ -2617,14 +2695,14 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2617
2695
|
tool: (id) => registry.get(id) ?? null,
|
|
2618
2696
|
toolSubtype: (id) => registry.get(id)?.subtype ?? id,
|
|
2619
2697
|
registerTool: (def) => {
|
|
2620
|
-
const resolved = buildToolRegistry([...configTools, def]).get(def.id);
|
|
2698
|
+
const resolved = require_tools.buildToolRegistry([...configTools, def]).get(def.id);
|
|
2621
2699
|
if (!resolved) throw new Error(`[annotation] could not resolve tool '${def.id}'`);
|
|
2622
2700
|
registry.set(resolved.id, resolved);
|
|
2623
2701
|
const un = ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.registerTool({
|
|
2624
2702
|
id: resolved.id,
|
|
2625
2703
|
cursor: resolved.cursor,
|
|
2626
2704
|
enables: resolved.enables,
|
|
2627
|
-
touchDirect: isTouchDirect(resolved.enables)
|
|
2705
|
+
touchDirect: require_tools.isTouchDirect(resolved.enables)
|
|
2628
2706
|
});
|
|
2629
2707
|
if (resolved.defaults) apply({
|
|
2630
2708
|
t: "setDefaults",
|
|
@@ -2637,10 +2715,11 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2637
2715
|
};
|
|
2638
2716
|
},
|
|
2639
2717
|
update: (ref, patch) => updateOne(ref, patch),
|
|
2640
|
-
updateSelection
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2718
|
+
updateSelection,
|
|
2719
|
+
toggleTextFormat: (format) => {
|
|
2720
|
+
const current = memoSelectionProps().values[format];
|
|
2721
|
+
updateSelection({ [format]: !current });
|
|
2722
|
+
},
|
|
2644
2723
|
updateSelectionFlags: (patch) => apply({
|
|
2645
2724
|
t: "setFlags",
|
|
2646
2725
|
patch
|
|
@@ -2696,6 +2775,10 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2696
2775
|
chrome: (pon, scale, rotation, zoom) => memoChrome(pon, scale, rotation, zoom),
|
|
2697
2776
|
selectionAnchor: (scale, rotation, zoom) => memoAnchor(scale, rotation, zoom),
|
|
2698
2777
|
creationDraftAnchor: () => memoDraftAnchor(),
|
|
2778
|
+
distanceCreationPage: () => {
|
|
2779
|
+
const draft = model().draft;
|
|
2780
|
+
return draft?.g === "create-distance" && draft.step === "offset" ? draft.pon : null;
|
|
2781
|
+
},
|
|
2699
2782
|
selection: () => model().selected,
|
|
2700
2783
|
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,
|
|
2701
2784
|
claimsTouchAt: (pon, point, scale, rotation, zoom) => {
|
|
@@ -2775,14 +2858,63 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2775
2858
|
}
|
|
2776
2859
|
}),
|
|
2777
2860
|
createPointer: (tool, phase, pon, point, finish = false, displayRotation) => {
|
|
2778
|
-
if (!allowsCreate()) return;
|
|
2779
2861
|
const t = registry.get(tool);
|
|
2862
|
+
if (t?.meta?.capture === true ? !ctx.doc?.security.allows("doc.annotate.modify") : !allowsCreate()) return;
|
|
2863
|
+
const crop = cropOf(pon);
|
|
2864
|
+
const cache = pageViewports.get(pon);
|
|
2865
|
+
const draft = model().draft;
|
|
2866
|
+
const continuingMeasurement = (draft?.g === "create-distance" || draft?.g === "create-poly") && draft.pon === pon && draft.preset === (t?.preset ?? tool);
|
|
2867
|
+
const dimension = t && (0, _embedpdf_engine_core_runtime.isDimension)(t);
|
|
2868
|
+
if (dimension && phase === "down" && !continuingMeasurement && (!crop || !cache?.viewports)) return;
|
|
2869
|
+
const viewport = crop && cache?.viewports ? (0, _embedpdf_engine_core_runtime.viewportForPoint)(cache.viewports, {
|
|
2870
|
+
x: point.x + crop.left,
|
|
2871
|
+
y: crop.top - point.y
|
|
2872
|
+
}) : void 0;
|
|
2873
|
+
const measure = dimension && crop && cache ? {
|
|
2874
|
+
intent: t.intent,
|
|
2875
|
+
measure: viewport ? viewport.measure ?? null : cache.fallback,
|
|
2876
|
+
caption: t.measurement?.caption ?? { enabled: true },
|
|
2877
|
+
...t.intent === "LineDimension" ? { leader: t.measurement?.leader } : {},
|
|
2878
|
+
crop,
|
|
2879
|
+
text: ""
|
|
2880
|
+
} : void 0;
|
|
2881
|
+
if (measure && phase === "down" && !continuingMeasurement && !(0, _embedpdf_engine_core_runtime.isReadout)((0, _embedpdf_engine_core_runtime.measurementReadout)({
|
|
2882
|
+
subtype: t.subtype,
|
|
2883
|
+
intent: measure.intent,
|
|
2884
|
+
measure: measure.measure,
|
|
2885
|
+
linePoints: {
|
|
2886
|
+
start: {
|
|
2887
|
+
x: 0,
|
|
2888
|
+
y: 0
|
|
2889
|
+
},
|
|
2890
|
+
end: {
|
|
2891
|
+
x: 1,
|
|
2892
|
+
y: 0
|
|
2893
|
+
}
|
|
2894
|
+
},
|
|
2895
|
+
vertices: [
|
|
2896
|
+
{
|
|
2897
|
+
x: 0,
|
|
2898
|
+
y: 0
|
|
2899
|
+
},
|
|
2900
|
+
{
|
|
2901
|
+
x: 1,
|
|
2902
|
+
y: 0
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
x: 1,
|
|
2906
|
+
y: 1
|
|
2907
|
+
}
|
|
2908
|
+
]
|
|
2909
|
+
}))) return;
|
|
2780
2910
|
apply({
|
|
2781
2911
|
t: "createPointer",
|
|
2912
|
+
measure,
|
|
2913
|
+
capture: t?.meta?.capture === true ? tool : void 0,
|
|
2782
2914
|
phase,
|
|
2783
2915
|
subtype: t?.subtype ?? tool,
|
|
2784
2916
|
preset: t?.preset ?? tool,
|
|
2785
|
-
intent: t?.intent,
|
|
2917
|
+
intent: t?.intent === "ink-highlight" ? t.intent : void 0,
|
|
2786
2918
|
clickCreate: t?.clickCreate,
|
|
2787
2919
|
flags: t?.flags,
|
|
2788
2920
|
deferInkCommit: (t?.ink?.groupStrokesMs ?? 0) > 0,
|
|
@@ -2798,6 +2930,7 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2798
2930
|
}
|
|
2799
2931
|
});
|
|
2800
2932
|
},
|
|
2933
|
+
hasCreationDraft: () => model().draft?.g.startsWith("create-") ?? false,
|
|
2801
2934
|
finishCreationDraft: () => apply({ t: "finishCreationDraft" }),
|
|
2802
2935
|
finishInkDraft: () => apply({ t: "finishInkDraft" }),
|
|
2803
2936
|
cancelCreationDraft: () => apply({ t: "cancel" }),
|
|
@@ -2979,30 +3112,37 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
2979
3112
|
id: require_seam.refKey(ref),
|
|
2980
3113
|
text
|
|
2981
3114
|
});
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
}, TEXT_COMMIT_DEBOUNCE_MS));
|
|
3115
|
+
scheduleTextCommit(ref);
|
|
3116
|
+
},
|
|
3117
|
+
setRichText: (ref, doc) => {
|
|
3118
|
+
apply({
|
|
3119
|
+
t: "setRichText",
|
|
3120
|
+
id: require_seam.refKey(ref),
|
|
3121
|
+
doc: { paragraphs: doc.paragraphs }
|
|
3122
|
+
});
|
|
3123
|
+
scheduleTextCommit(ref);
|
|
2992
3124
|
},
|
|
3125
|
+
setTextSelection: (ref, range) => {
|
|
3126
|
+
const id = require_seam.refKey(ref);
|
|
3127
|
+
const prev = ctx.getState().textSelection;
|
|
3128
|
+
const next = range ? {
|
|
3129
|
+
id,
|
|
3130
|
+
start: range.start,
|
|
3131
|
+
end: range.end
|
|
3132
|
+
} : null;
|
|
3133
|
+
if (prev === null === (next === null) && (!prev || !next || prev.id === next.id && prev.start === next.start && prev.end === next.end)) return;
|
|
3134
|
+
ctx.dispatch({
|
|
3135
|
+
type: "SET_TEXT_SELECTION",
|
|
3136
|
+
selection: next
|
|
3137
|
+
});
|
|
3138
|
+
},
|
|
3139
|
+
cssFontFamily: (family) => cssFontFamilyForFace(family, fonts),
|
|
2993
3140
|
endTextEdit: () => {
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
const text = a.data?.contents ?? "";
|
|
3000
|
-
if (pon != null) ctx.doc?.page(pon).annotations.update(a.ref, {
|
|
3001
|
-
subtype: "free-text",
|
|
3002
|
-
contents: text
|
|
3003
|
-
}).then(() => {}, () => {});
|
|
3004
|
-
}
|
|
3005
|
-
textTimers.clear();
|
|
3141
|
+
flushTextCommits();
|
|
3142
|
+
if (ctx.getState().textSelection) ctx.dispatch({
|
|
3143
|
+
type: "SET_TEXT_SELECTION",
|
|
3144
|
+
selection: null
|
|
3145
|
+
});
|
|
3006
3146
|
apply({ t: "endTextEdit" });
|
|
3007
3147
|
},
|
|
3008
3148
|
commitScriptEffects: async (entries) => {
|
|
@@ -3081,24 +3221,6 @@ function createAnnotationCapability(ctx, config = {}) {
|
|
|
3081
3221
|
};
|
|
3082
3222
|
}
|
|
3083
3223
|
//#endregion
|
|
3084
|
-
Object.defineProperty(exports, "ARMED_STAMP_TOOL_ID", {
|
|
3085
|
-
enumerable: true,
|
|
3086
|
-
get: function() {
|
|
3087
|
-
return ARMED_STAMP_TOOL_ID;
|
|
3088
|
-
}
|
|
3089
|
-
});
|
|
3090
|
-
Object.defineProperty(exports, "DEFAULT_TOOLS", {
|
|
3091
|
-
enumerable: true,
|
|
3092
|
-
get: function() {
|
|
3093
|
-
return DEFAULT_TOOLS;
|
|
3094
|
-
}
|
|
3095
|
-
});
|
|
3096
|
-
Object.defineProperty(exports, "buildToolRegistry", {
|
|
3097
|
-
enumerable: true,
|
|
3098
|
-
get: function() {
|
|
3099
|
-
return buildToolRegistry;
|
|
3100
|
-
}
|
|
3101
|
-
});
|
|
3102
3224
|
Object.defineProperty(exports, "createAnnotationCapability", {
|
|
3103
3225
|
enumerable: true,
|
|
3104
3226
|
get: function() {
|
|
@@ -3111,12 +3233,6 @@ Object.defineProperty(exports, "fromDTO", {
|
|
|
3111
3233
|
return fromDTO;
|
|
3112
3234
|
}
|
|
3113
3235
|
});
|
|
3114
|
-
Object.defineProperty(exports, "isTouchDirect", {
|
|
3115
|
-
enumerable: true,
|
|
3116
|
-
get: function() {
|
|
3117
|
-
return isTouchDirect;
|
|
3118
|
-
}
|
|
3119
|
-
});
|
|
3120
3236
|
Object.defineProperty(exports, "toCreateDraft", {
|
|
3121
3237
|
enumerable: true,
|
|
3122
3238
|
get: function() {
|
|
@@ -3130,4 +3246,4 @@ Object.defineProperty(exports, "toPatch", {
|
|
|
3130
3246
|
}
|
|
3131
3247
|
});
|
|
3132
3248
|
|
|
3133
|
-
//# sourceMappingURL=capability-
|
|
3249
|
+
//# sourceMappingURL=capability-Bt320iJP.cjs.map
|