@embedpdf/core-annotation 3.0.0-next.0

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.
@@ -0,0 +1,1636 @@
1
+ import { AnnotationDTO, AnnotationFlags, AnnotationFlags as AnnotationFlags$1, AnnotationRef, BlendMode, BlendMode as BlendMode$1, CaretIntent, InkIntent, LineEnding, LineEndings, NO_ANNOTATION_FLAGS, PdfLinkTarget, PdfPoint, PdfRect, StrikeoutIntent } from "@embedpdf/engine-core/runtime";
2
+ import { PageObjectNumber } from "@embedpdf/core";
3
+ import { PageRotation, Point, Rect as Rect$1, Size } from "@embedpdf/core-geometry";
4
+ //#region src/flags.d.ts
5
+ /** The `/F` every freshly DRAWN annotation starts with: `print` set (Acrobat
6
+ * parity — without it the annotation silently disappears when printed). */
7
+ declare const DRAWN_FLAGS: AnnotationFlags;
8
+ /** All ten flags, for iteration/equality — kept in the primitives' order. */
9
+ declare const FLAG_KEYS: ReadonlyArray<keyof AnnotationFlags>;
10
+ declare const flagsEqual: (a: AnnotationFlags, b: AnnotationFlags) => boolean;
11
+ /** Merge a partial write over the current flags. */
12
+ declare const mergeFlags: (base: AnnotationFlags, patch: Partial<AnnotationFlags>) => AnnotationFlags;
13
+ /**
14
+ * On-screen visibility. `hidden` beats everything; `noView` hides unless
15
+ * `toggleNoView` and the annotation is ENGAGED — the spec says hover/selection,
16
+ * and v1 uses selection (the model tracks no hover).
17
+ */
18
+ declare const viewable: (f: AnnotationFlags, engaged?: boolean) => boolean;
19
+ /** Any pointer interaction (click-to-select, hover). ReadOnly kills it. */
20
+ declare const interactive: (f: AnnotationFlags) => boolean;
21
+ /** The subset of an Annot these predicates read — keeps them testable bare. */
22
+ interface FlagBearer {
23
+ subtype: string;
24
+ flags: AnnotationFlags;
25
+ }
26
+ /** Interaction gate for a concrete annotation: widget kinds ignore `readOnly`
27
+ * (ISO 32000 — a ReadOnly form field must still be movable by a form designer;
28
+ * the form-filling layer enforces field ReadOnly itself). */
29
+ declare const annotInteractive: (a: FlagBearer) => boolean;
30
+ /** Geometry/style/delete mutations — `locked` freezes the OBJECT, not its
31
+ * contents (that's `lockedContents`). */
32
+ declare const annotTransformable: (a: FlagBearer) => boolean;
33
+ /** `/Contents` text edits — the contents counterpart of `locked`. */
34
+ declare const annotContentsEditable: (a: FlagBearer) => boolean;
35
+ //#endregion
36
+ //#region src/types.d.ts
37
+ /**
38
+ * Content-space point/rect (y-down, PDF points, crop-relative) — the viewer space.
39
+ * These are the shared `@embedpdf/core-geometry` primitives: a `Vec` IS a `Point`
40
+ * and `Rect` IS geometry's `Rect`, so the whole stack speaks one vocabulary and
41
+ * the coordinate math lives in ONE package.
42
+ */
43
+ type Vec = Point;
44
+ type Rect = Rect$1;
45
+ /** Four content-space points (a /QuadPoints quad). */
46
+ type Quad = [Vec, Vec, Vec, Vec];
47
+ type Id = string;
48
+ type Cursor = string;
49
+ type PolySubtype = 'polygon' | 'polyline';
50
+ /**
51
+ * The per-page view environment the screen-anchor projection consumes:
52
+ * `zoom` — the page's zoom RELATIVE to its 100% baseline (dimensionless;
53
+ * 1 = Acrobat's 100% = `viewUnitsPerPoint × userUnit` view px per point) —
54
+ * and the TOTAL display rotation (document /Rotate + view rotation).
55
+ * Deliberately NOT a px-per-point scale: `noZoom` means "hold the body at its
56
+ * 100%-zoom size", a statement about zoom, so feeding it a units conversion
57
+ * (the old mistake) shrank bodies at 100%. Per-event / per-call context (the
58
+ * `pageBox` pattern) — never stored on the model; the pointer drafts capture
59
+ * it at DOWN so anchored gestures project and commit with the view they
60
+ * started under. See anchor.ts.
61
+ */
62
+ interface ViewEnv {
63
+ zoom: number;
64
+ rotation: PageRotation;
65
+ }
66
+ type Subtype = 'highlight' | 'underline' | 'squiggly' | 'strikeout' | 'square' | 'circle' | 'line' | 'polygon' | 'polyline' | 'caret' | 'ink' | 'stamp' | 'text' | 'file-attachment' | 'redact' | 'link' | (string & {});
67
+ /**
68
+ * Content-space geometry — the ONE thing hit-testing, editing, and rendering work
69
+ * on. A small closed union covers every kind: shapes (rect/ellipse), line,
70
+ * polygon/polyline (poly), text markup (quads), and caret.
71
+ */
72
+ /**
73
+ * A free-text callout's leader: the `/CL` line + `/LE` arrow. `tip` is the
74
+ * called-out point (the arrow is drawn here); `knee` is the optional elbow. The
75
+ * point where the leader meets the text box (the third `/CL` point) is NEVER
76
+ * stored — it is DERIVED from the box + the knee (see `calloutConnection`), so it
77
+ * can't drift when the box or knee moves. Content space (y-down).
78
+ */
79
+ interface Callout {
80
+ tip: Vec;
81
+ knee?: Vec;
82
+ ending: LineEnding;
83
+ }
84
+ /**
85
+ * Rotation (degrees, clockwise in content space, normalized `[0,360)`) carried by
86
+ * the rotatable `Geom` variants. The semantics differ by family, which is exactly
87
+ * the box-vs-vertex split:
88
+ *
89
+ * - **Box** (`rect`, `text`): `rect` is the UNROTATED local box and `rot` is the
90
+ * applied tilt — together they reconstruct the visual. The repository emits
91
+ * `rect` as `unrotatedRect`, `rot` as the rendered `/EMBD_Metadata/Rotation`,
92
+ * and the rotated visual AABB as `/Rect`, so PDFium bakes a portable `/AP`.
93
+ * - **Vertex** (`line`, `poly`, `ink`): the points are ALREADY rotated (they are
94
+ * the portable visual), so `rot` is an ADVISORY scalar — the cumulative tilt the
95
+ * user applied since authoring. It lets EmbedPDF reconstruct an oriented
96
+ * selection box (`obbFromTheta`) and offer reset-to-0; it is inert for
97
+ * rendering (PDFium ignores a lone `Rotation` with no `UnrotatedRect`).
98
+ */
99
+ type Geom = {
100
+ t: 'rect';
101
+ rect: Rect;
102
+ ellipse: boolean;
103
+ rot?: number;
104
+ } | {
105
+ t: 'line';
106
+ a: Vec;
107
+ b: Vec;
108
+ ends?: LineEndings;
109
+ rot?: number;
110
+ } | {
111
+ t: 'poly';
112
+ points: Vec[];
113
+ closed: boolean;
114
+ ends?: LineEndings;
115
+ rot?: number;
116
+ } | {
117
+ t: 'quads';
118
+ quads: Quad[];
119
+ } | {
120
+ t: 'caret';
121
+ rect: Rect;
122
+ } | {
123
+ t: 'ink';
124
+ strokes: Vec[][];
125
+ rot?: number;
126
+ } | {
127
+ t: 'text';
128
+ rect: Rect;
129
+ callout?: Callout;
130
+ rot?: number;
131
+ };
132
+ /**
133
+ * How a shape's outline is stroked. A discriminated union so illegal combinations
134
+ * — a dash array on a cloudy border, an intensity on a dashed one — are simply
135
+ * unrepresentable. Maps onto the engine's `/BS /S` (`borderStyle`), `/BS /D`
136
+ * (`dashArray`), and `/BE /I` (`cloudyIntensity`) wire fields. Cloudy is only
137
+ * honoured for shapes (square/circle); other kinds treat it as solid.
138
+ */
139
+ type Border = {
140
+ kind: 'solid';
141
+ } | {
142
+ kind: 'dashed';
143
+ dash: number[];
144
+ } | {
145
+ kind: 'cloudy';
146
+ intensity: number;
147
+ };
148
+ interface Style {
149
+ /** `/C` colour — stroke for geometric kinds, highlight colour for markup. */
150
+ color: string;
151
+ /** `/IC` interior (fill) colour. `null` when the annotation has no fill. */
152
+ interiorColor: string | null;
153
+ strokeWidth: number;
154
+ opacity: number;
155
+ /** Effective blend mode of the annotation's normal appearance. */
156
+ blendMode: BlendMode$1;
157
+ /** Outline style — defaults to `{ kind: 'solid' }`. */
158
+ border: Border;
159
+ }
160
+ type TextAlign = 'left' | 'center' | 'right';
161
+ /**
162
+ * Content-space text styling for a text-editable kind (free text) — the text
163
+ * counterpart of {@link Style}, projected from the DTO's `/DA` fields the same
164
+ * way `style` is projected from `/C`/`/CA`/`/BS`. CSS colour string; the engine
165
+ * `Color` seam is crossed only in the plugin repository.
166
+ */
167
+ interface TextStyle {
168
+ /** A PDF standard font name or a registered font key. */
169
+ fontFamily: string;
170
+ /** Content units (PDF points). */
171
+ fontSize: number;
172
+ fontColor: string;
173
+ textAlign: TextAlign;
174
+ }
175
+ /**
176
+ * The ONE flat vocabulary for editable appearance properties — what property
177
+ * sidebars/toolbars read and write, regardless of where each property is stored
178
+ * internally (`style`, `geom.ends`, or `text`). Which keys apply to a kind, and
179
+ * in what UI order, is declared per kind in the KIND table (`propsFor`).
180
+ */
181
+ interface AnnotationProps extends Style, TextStyle {
182
+ /** `/LE` endings (line / polyline). */
183
+ lineEndings: LineEndings;
184
+ /**
185
+ * `/Name` icon of an icon kind (text note, file attachment). Optional in
186
+ * the bag — each kind falls back to its own spec default ('comment',
187
+ * 'paperclip') at creation, so one flat vocabulary needs no per-kind base
188
+ * value. Valid values are the `options` of the kind's `icon` PropSpec.
189
+ */
190
+ icon?: string;
191
+ /**
192
+ * Where this annotation LINKS to, or `null` for none. Optional in the bag
193
+ * like `icon`. One key, two storages (the props.ts routing rule): on the
194
+ * link kind it is the annotation's OWN target (`/A`); on every other
195
+ * linkable kind it is the target of the ATTACHED link child(ren) —
196
+ * grouped `/Link` annotations the model folds into their parent (the
197
+ * serialization the PDF spec forces, since only Link annotations are
198
+ * clickable). Writing it creates/retargets/deletes those children through
199
+ * the ONE `syncLink` seam.
200
+ */
201
+ link?: PdfLinkTarget | null;
202
+ }
203
+ type PropKey = keyof AnnotationProps;
204
+ /**
205
+ * What a committed edit CHANGED — carried on the `patch` effect so the shell
206
+ * emits exactly that intent (repository `toScopedPatch`) instead of
207
+ * reconstructing a full projection. `geometry` covers every gesture commit
208
+ * (move/resize/rotate/vertex edit); `props` forwards the user's patch keys
209
+ * verbatim. Text content never rides this effect (the debounced text-edit
210
+ * write owns `contents`).
211
+ */
212
+ type PatchScope = {
213
+ kind: 'geometry';
214
+ } | {
215
+ kind: 'props';
216
+ keys: PropKey[];
217
+ };
218
+ /** A partial property write. `lineEndings` merges per side (set just `end`
219
+ * without knowing `start`); every other key overwrites. */
220
+ type AnnotationPropsPatch = { [K in PropKey]?: K extends 'lineEndings' ? Partial<LineEndings> : AnnotationProps[K]; };
221
+ interface Annot {
222
+ id: Id;
223
+ ref: AnnotationRef | null;
224
+ pon: PageObjectNumber;
225
+ subtype: Subtype;
226
+ geom: Geom;
227
+ style: Style;
228
+ /** Text styling — present only for text-editable kinds (free text). Like
229
+ * `style`, a content-space projection of `data`, editable via `setProps`. */
230
+ text?: TextStyle;
231
+ /** Redaction label (`/OverlayText` + `/Repeat`) — redact kind only. A
232
+ * projection of `data` like `text`; the hover preview scene draws it. */
233
+ label?: {
234
+ text: string;
235
+ repeat: boolean;
236
+ };
237
+ /** `/Name` icon — present only for icon kinds (text note, file attachment).
238
+ * Like `style`, a projection of `data`, editable via `setProps`. */
239
+ icon?: string;
240
+ /**
241
+ * The `/F` annotation flags, verbatim from the DTO (freshly drawn annotations
242
+ * start at {@link DRAWN_FLAGS} — `print` set). NEVER read individual keys to
243
+ * gate behavior — the predicates in `flags.ts` (`annotInteractive`,
244
+ * `annotTransformable`, `annotContentsEditable`, `viewable`) are the one
245
+ * interpretation of the spec, and `anchorModeOf` owns `noZoom`/`noRotate`.
246
+ */
247
+ flags: AnnotationFlags$1;
248
+ source: 'baked' | 'vector';
249
+ /**
250
+ * Content-space box of the engine appearance raster (the AP `/Rect`), set when
251
+ * the annotation is derived from a DTO. While `source === 'baked'` the renderer
252
+ * blits the engine bitmap into this box; a move translates it (a rigid shift
253
+ * keeps the raster valid), so the bitmap rides along without re-rendering.
254
+ * Ignored once `source === 'vector'`.
255
+ */
256
+ apBox?: Rect;
257
+ /**
258
+ * Rotation (deg, CW) that was STRIPPED from the baked raster — present only
259
+ * when the engine rendered this appearance rotation-free (a box-family kind
260
+ * whose DTO carries BOTH `rotation` and `unrotatedRect`; `apBox` is then the
261
+ * unrotated box). The blit re-applies it as a view transform about the box
262
+ * centre. Vertex kinds pre-rotate their geometry, so this stays unset there
263
+ * and their rasters blit untransformed.
264
+ */
265
+ apRot?: number;
266
+ /**
267
+ * Revision of the engine-baked `/AP` CONTENT (absent ≡ 0). The raster a baked
268
+ * annotation shows depends on exactly this and the render scale — never on
269
+ * position (`apBox` translates the blit) or rotation (`apRot` transforms it) —
270
+ * so the shell re-fetches appearances precisely when it changes. Bumped by the
271
+ * `upsert` that confirms an engine re-bake with new content: a geometry patch
272
+ * that changed the authoring frame's SIZE resolving (`Effect.apChanged`), or a
273
+ * remote edit folding in. A move or rotate leaves it untouched: the old raster
274
+ * is still pixel-exact, so those cost zero re-renders.
275
+ */
276
+ apVersion?: number;
277
+ /**
278
+ * The canonical engine DTO this annotation was derived from (PDF-space, sRGB)
279
+ * — the single source of truth for its data. `geom` and `style` are
280
+ * content-space RENDER PROJECTIONS of it, recomputed (never edited directly)
281
+ * whenever `data` changes, so the two can't drift. Absent only for a vector
282
+ * draft that hasn't been committed to the engine yet (no DTO exists).
283
+ */
284
+ data?: AnnotationDTO;
285
+ /** Normalized PDF `/IT` for intent-bearing annotations authored before a DTO exists. */
286
+ intent?: CaretIntent | StrikeoutIntent | InkIntent;
287
+ /**
288
+ * The `link` prop's value (see {@link AnnotationProps.link}): the link
289
+ * kind's own target, or — on other linkable kinds — the folded attached
290
+ * link's target. Editable via `setProps`.
291
+ */
292
+ link?: PdfLinkTarget | null;
293
+ /**
294
+ * INTERNAL join keys, never a prop: the engine refs of the attached link
295
+ * child annotation(s) this parent's `link` value is materialized as (one
296
+ * per visual segment for markup). Maintained by the repository fold and
297
+ * consumed by the shell's `syncLink` reconciler + delete expansion — the
298
+ * ONLY code allowed to touch those children. Absent on the link kind
299
+ * itself and on anything without an attached link.
300
+ */
301
+ linkRefs?: AnnotationRef[];
302
+ /**
303
+ * Relationship to another annotation. `irt` ("in reply to") links a child to a
304
+ * parent — a reply in a comment thread, or a caret bound to its strikeout in a
305
+ * replace-text pair. `group` ties a set into one composite unit (created and,
306
+ * typically, deleted together). Both are unused until comments / replace-text
307
+ * land, but the field lives here from the start so select/delete/persistence
308
+ * never have to be retrofitted around it.
309
+ */
310
+ irt?: Id;
311
+ group?: string;
312
+ }
313
+ /** A draggable handle: a resize corner/edge (rect) or a vertex (line/poly). */
314
+ interface Handle {
315
+ id: string;
316
+ at: Vec;
317
+ cursor: Cursor;
318
+ }
319
+ /** An alignment guide produced by move-snapping: a vertical (`axis: 'x'`) or
320
+ * horizontal (`axis: 'y'`) line at `at`, spanning `lo..hi` (content units). */
321
+ interface Guide {
322
+ axis: 'x' | 'y';
323
+ at: number;
324
+ lo: number;
325
+ hi: number;
326
+ }
327
+ /** Snapping behaviour — seeded from the plugin config, live-adjustable via the
328
+ * `setSnap` msg (so an app can wire a UI toggle). */
329
+ interface SnapSettings {
330
+ /** Alignment guides while moving (snap to other annotations + the page). */
331
+ guides: boolean;
332
+ /** Guide snap tolerance, content units (PDF pt) — the `hitMargin` convention. */
333
+ guideThreshold: number;
334
+ /** Snap the rotate gesture onto `rotationAngles`. */
335
+ rotation: boolean;
336
+ rotationAngles: number[];
337
+ /** Rotation snap tolerance, degrees. */
338
+ rotationThreshold: number;
339
+ }
340
+ /**
341
+ * The `defaults` key a creation draft resolves its props from — the authoring
342
+ * TOOL that started it, which may differ from the PDF `subtype`. Two tools can
343
+ * share a subtype but carry distinct defaults (an "arrow" is a `line` with an
344
+ * arrowhead default); the preset keeps them apart. Absent → fall back to
345
+ * `subtype` (a headless / programmatic caller that isn't tool-driven), so the
346
+ * built-in tools where preset === subtype behave exactly as before.
347
+ */
348
+ type Draft = {
349
+ g: 'create-rect';
350
+ subtype: Subtype;
351
+ preset?: string;
352
+ pon: PageObjectNumber;
353
+ from: Vec;
354
+ to: Vec;
355
+ ellipse: boolean;
356
+ /** Display rotation + upright policy captured at DOWN (the gesture's home
357
+ * page), so the commit counter-rotates against what the author SAW even
358
+ * if the up sample resolves elsewhere. See {@link PointerInput}. */
359
+ displayRotation?: PageRotation;
360
+ upright?: boolean;
361
+ /** The tool's click-create policy, captured at DOWN like `upright`. */
362
+ clickCreate?: ClickCreate | false;
363
+ /** The tool's `/F` seed, captured at DOWN like `upright` — merged over
364
+ * {@link DRAWN_FLAGS} at commit (a note tool sets noZoom/noRotate). */
365
+ flags?: Partial<AnnotationFlags$1>;
366
+ } | {
367
+ g: 'create-line';
368
+ subtype: Subtype;
369
+ preset?: string;
370
+ pon: PageObjectNumber;
371
+ from: Vec;
372
+ to: Vec;
373
+ /** The tool's click-create policy, captured at DOWN like `upright`. */
374
+ clickCreate?: ClickCreate | false;
375
+ /** The tool's `/F` seed, captured at DOWN (see the rect draft). */
376
+ flags?: Partial<AnnotationFlags$1>;
377
+ } | {
378
+ g: 'create-poly';
379
+ subtype: Subtype;
380
+ preset?: string;
381
+ pon: PageObjectNumber;
382
+ points: Vec[];
383
+ cur: Vec;
384
+ closed: boolean;
385
+ /** The tool's `/F` seed, captured at DOWN (see the rect draft). */
386
+ flags?: Partial<AnnotationFlags$1>;
387
+ } | {
388
+ g: 'create-ink';
389
+ subtype: Subtype;
390
+ preset?: string;
391
+ pon: PageObjectNumber;
392
+ strokes: Vec[][];
393
+ intent?: InkIntent;
394
+ /** The tool's `/F` seed, captured at DOWN (see the rect draft). */
395
+ flags?: Partial<AnnotationFlags$1>;
396
+ } | {
397
+ g: 'create-callout';
398
+ subtype: Subtype;
399
+ preset?: string;
400
+ pon: PageObjectNumber;
401
+ step: 'knee' | 'box';
402
+ tip: Vec;
403
+ knee?: Vec;
404
+ cur: Vec;
405
+ boxFrom?: Vec;
406
+ boxTo?: Vec;
407
+ /** Display rotation + upright policy captured at the TIP click (the
408
+ * gesture's home page) — the text BOX commits counter-rotated so it
409
+ * reads upright; the leader (tip/knee) is page-space and never turns.
410
+ * Same capture rule as the rect draft. */
411
+ displayRotation?: PageRotation;
412
+ upright?: boolean;
413
+ /** The tool's `/F` seed, captured at DOWN (see the rect draft). */
414
+ flags?: Partial<AnnotationFlags$1>;
415
+ } | {
416
+ g: 'move';
417
+ ids: Id[];
418
+ start: Vec;
419
+ delta: Vec;
420
+ guides: Guide[];
421
+ } | {
422
+ g: 'handle';
423
+ id: Id;
424
+ handle: string;
425
+ base: Geom;
426
+ cur: Geom;
427
+ view?: ViewEnv;
428
+ } | {
429
+ g: 'rotate';
430
+ ids: Id[];
431
+ pivot: Vec;
432
+ start: Vec;
433
+ cur: Vec;
434
+ free?: boolean;
435
+ view?: ViewEnv;
436
+ } | {
437
+ g: 'group';
438
+ op: 'resize';
439
+ ids: Id[];
440
+ handle: string;
441
+ anchor: Vec;
442
+ base: Rect;
443
+ cur: Rect;
444
+ view?: ViewEnv;
445
+ } | {
446
+ g: 'marquee';
447
+ pon: PageObjectNumber;
448
+ from: Vec;
449
+ to: Vec;
450
+ };
451
+ /** A live text-markup preview (the in-progress selection rendered as the markup it
452
+ * will become). Per page, since a selection can span pages. */
453
+ interface MarkupPreview {
454
+ subtype: Subtype;
455
+ /** Defaults key, distinct from subtype for presets such as replace-text. */
456
+ preset: string;
457
+ byPage: Record<number, Quad[]>;
458
+ }
459
+ /** Anchor + affordance state for UI that controls an in-progress creation draft. */
460
+ interface CreationDraftAnchor {
461
+ kind: 'poly';
462
+ subtype: PolySubtype;
463
+ pon: PageObjectNumber;
464
+ bounds: Rect;
465
+ pointCount: number;
466
+ minPoints: number;
467
+ canFinish: boolean;
468
+ }
469
+ interface Model {
470
+ byId: Record<Id, Annot>;
471
+ order: Id[];
472
+ selected: Id[];
473
+ /** The annotation under the pointer (topmost hit), or null. View-model
474
+ * state like `selected` — drives hover affordances (a redaction mark's
475
+ * applied-look preview) purely from the scene. Updated on CHANGE only
476
+ * (enter/leave cadence, never per-move). */
477
+ hovered: Id | null;
478
+ draft: Draft | null;
479
+ /** Transient ghost of an in-progress markup selection (null when idle). */
480
+ preview: MarkupPreview | null;
481
+ seq: number;
482
+ /** The base style new annotations inherit (per-tool `defaults` layer on top). */
483
+ style: Style;
484
+ /** Per-tool (keyed by subtype / tool id) property overrides for newly drawn
485
+ * annotations — the SAME flat vocabulary `setProps` uses. `lineEndings` is
486
+ * stored fully resolved (merged at `setDefaults` time). */
487
+ defaults: Record<string, AnnotationPropsPatch>;
488
+ /** Extra clickable margin (content units) around a stroke — bump it for touch. */
489
+ hitMargin: number;
490
+ /** The free-text annotation currently in TEXT-EDIT mode (its `contentEditable`
491
+ * is focused), or null. Distinct from `selected`: you select to move/resize,
492
+ * you edit to type. */
493
+ editing: Id | null;
494
+ /** Snapping behaviour (alignment guides + rotation). */
495
+ snap: SnapSettings;
496
+ }
497
+ /**
498
+ * Selection-chrome geometry in CONTENT units — the grab zones and the knob
499
+ * stalk. The core is unit-agnostic and zoom-free: callers own the px→content
500
+ * conversion (settings are CSS px; divide by the page's view scale), so grab
501
+ * zones stay screen-constant across zoom.
502
+ */
503
+ interface ChromeGeom {
504
+ /** Half-side of a resize/vertex handle's square grab zone. */
505
+ handleTol: number;
506
+ /** Half-side of the rotate knob's square grab zone. */
507
+ knobTol: number;
508
+ /** How far the rotate knob hangs off the selection edge. */
509
+ knobOffset: number;
510
+ }
511
+ interface PointerInput {
512
+ pon: PageObjectNumber;
513
+ point: Vec;
514
+ shift: boolean;
515
+ finish?: boolean;
516
+ /**
517
+ * The page's content box (`{0, 0, crop.width, crop.height}`) — when present,
518
+ * gestures are CLAMPED to it: a move keeps the selection's bounds inside the
519
+ * page (sliding along the edge when the pointer overshoots), resize/create
520
+ * points pin to the edge. Annotations are page-bound; the pointer isn't.
521
+ */
522
+ pageBox?: Rect;
523
+ /**
524
+ * Chrome grab-zone geometry for this event — per-event environmental context
525
+ * exactly like `pageBox` (the caller converts its CSS-px settings by the
526
+ * page's view scale at dispatch). Absent → `DEFAULT_CHROME_GEOM`.
527
+ */
528
+ chrome?: ChromeGeom;
529
+ /**
530
+ * The page's TOTAL display rotation (document /Rotate + view rotation) at the
531
+ * gesture — per-event environmental context like `pageBox`. A creation DOWN
532
+ * captures it on the draft (an `upright` commit counter-rotates against how
533
+ * the page was DISPLAYED); edit gestures read it per sample, paired with
534
+ * `scale`, so screen-anchored (`noZoom`/`noRotate`) annotations hit-test and
535
+ * clamp at their EFFECTIVE geometry. Content space itself never rotates.
536
+ */
537
+ displayRotation?: PageRotation;
538
+ /**
539
+ * The page's zoom RELATIVE to its 100% baseline at the event — the other
540
+ * half of the {@link ViewEnv} pair with `displayRotation` (dimensionless,
541
+ * `transform.zoom`; NOT the px-per-point scale the caller uses for its
542
+ * CSS-px chrome conversion). Absent → 1 (headless callers; screen-anchored
543
+ * bodies then hit-test at rect size).
544
+ */
545
+ zoom?: number;
546
+ /**
547
+ * Counter-rotate the created annotation so it reads upright at
548
+ * `displayRotation` — the authoring TOOL's `upright` policy, resolved by the
549
+ * caller (the core knows subtypes, not tools). Box kinds only (free-text /
550
+ * stamp are the ones with a natural reading orientation); vertex kinds and
551
+ * callouts ignore it.
552
+ */
553
+ upright?: boolean;
554
+ /**
555
+ * Ids invisible to hit-testing and marquee for THIS event — per-event
556
+ * environmental context like `pageBox`. The caller (plugin shell) resolves
557
+ * its engaged Behaviors (form widgets under a fill tool render their own
558
+ * DOM and must not select/move), so the core stays behavior-agnostic.
559
+ */
560
+ inert?: ReadonlySet<Id>;
561
+ }
562
+ /**
563
+ * What a bare CLICK (a press-release under the drag threshold) creates for a
564
+ * tool: a default-size box with an EXPLICIT anchor (`center` unless stated —
565
+ * free text declares `top-left` so the box hangs where you'll type,
566
+ * display-frame-aware under `upright`), or a default-length line from the
567
+ * point (`angleDeg` 0 = rightward, CW-positive in y-down space). Resolved
568
+ * from the TOOL by the caller and passed on the message — the core knows
569
+ * subtypes, not tools (the `upright` pattern). `false` suppresses a kind's
570
+ * own click fallback (free text always click-creates by default).
571
+ * Anchoring is policy DATA, never inferred from the kind — the same policy
572
+ * drives annotation commits, footprint ghosts, and form-field placement
573
+ * (see `resolveClickPlacement`).
574
+ */
575
+ type ClickCreate = {
576
+ width: number;
577
+ height: number;
578
+ anchor?: 'center' | 'top-left';
579
+ } | {
580
+ length: number;
581
+ angleDeg?: number;
582
+ };
583
+ type Msg = {
584
+ t: 'editPointer';
585
+ phase: 'down' | 'move' | 'up';
586
+ in: PointerInput;
587
+ } | {
588
+ t: 'marqueePointer';
589
+ phase: 'down' | 'move' | 'up';
590
+ in: PointerInput;
591
+ } | {
592
+ t: 'createPointer';
593
+ phase: 'down' | 'move' | 'up';
594
+ subtype: Subtype;
595
+ /** The authoring tool's `defaults` key (see {@link Draft}). Defaults to `subtype`. */
596
+ preset?: string;
597
+ /** PDF intent carried by an ink authoring preset. */
598
+ intent?: InkIntent;
599
+ /** The tool's click-create policy (see {@link ClickCreate}). */
600
+ clickCreate?: ClickCreate | false;
601
+ /** The tool's `/F` seed — merged over {@link DRAWN_FLAGS} at commit (a
602
+ * note tool passes `{ noZoom: true, noRotate: true }`). */
603
+ flags?: Partial<AnnotationFlags$1>;
604
+ /** Keep a completed ink stroke in the draft until `finishInkDraft`. */
605
+ deferInkCommit?: boolean;
606
+ /** Optional pure straight-line recognition applied to each completed stroke. */
607
+ straightenInk?: InkStraightenOptions;
608
+ in: PointerInput;
609
+ } | {
610
+ t: 'finishInkDraft';
611
+ } | {
612
+ t: 'finishCreationDraft';
613
+ } | {
614
+ t: 'createCaret';
615
+ pon: PageObjectNumber;
616
+ rect: Rect;
617
+ flags?: Partial<AnnotationFlags$1>;
618
+ } | {
619
+ t: 'createReplaceText';
620
+ pon: PageObjectNumber;
621
+ rects: Rect[];
622
+ endRect: Rect;
623
+ preset?: string;
624
+ } | {
625
+ t: 'createMarkup';
626
+ subtype: Subtype;
627
+ pon: PageObjectNumber;
628
+ rects: Rect[];
629
+ preset?: string;
630
+ /** The tool's `/F` seed — merged over {@link DRAWN_FLAGS} at commit. */
631
+ flags?: Partial<AnnotationFlags$1>;
632
+ } | {
633
+ t: 'setMarkupPreview';
634
+ subtype: Subtype;
635
+ rectsByPage: Record<number, Rect[]>;
636
+ preset?: string;
637
+ } | {
638
+ t: 'clearMarkupPreview';
639
+ } | {
640
+ t: 'deselect';
641
+ ids?: Id[];
642
+ } |
643
+ /** Pointer entered/left an annotation (topmost hit id, or null). Pure state. */
644
+ {
645
+ t: 'hover';
646
+ id: Id | null;
647
+ } | {
648
+ t: 'select';
649
+ ids: Id[];
650
+ add?: boolean;
651
+ } | {
652
+ t: 'setProps';
653
+ patch: AnnotationPropsPatch;
654
+ } | {
655
+ t: 'setFlags';
656
+ patch: Partial<AnnotationFlags$1>;
657
+ ids?: Id[];
658
+ } | {
659
+ t: 'setDefaults';
660
+ subtype: Subtype;
661
+ patch: AnnotationPropsPatch;
662
+ } | {
663
+ t: 'setSnap';
664
+ patch: Partial<SnapSettings>;
665
+ } | {
666
+ t: 'rotate90';
667
+ } | {
668
+ t: 'resetRotation';
669
+ } | {
670
+ t: 'delete';
671
+ } | {
672
+ t: 'cancel';
673
+ } | {
674
+ t: 'loaded';
675
+ annots: Annot[];
676
+ } | {
677
+ t: 'created';
678
+ tempId: Id;
679
+ id: Id;
680
+ ref: AnnotationRef;
681
+ } | {
682
+ t: 'createFailed';
683
+ tempId: Id;
684
+ } | {
685
+ t: 'upsert';
686
+ annots: Annot[];
687
+ bumpAp?: boolean;
688
+ } | {
689
+ t: 'bumpAp';
690
+ ids: Id[];
691
+ } | {
692
+ t: 'remove';
693
+ ids: Id[];
694
+ } | {
695
+ t: 'beginTextEdit';
696
+ id: Id;
697
+ } | {
698
+ t: 'setText';
699
+ id: Id;
700
+ text: string;
701
+ } | {
702
+ t: 'endTextEdit';
703
+ };
704
+ type Effect = {
705
+ fx: 'create';
706
+ id: Id;
707
+ } | {
708
+ fx: 'createGroup';
709
+ primary: Id;
710
+ members: Id[];
711
+ } |
712
+ /** `apChanged` is set (to `true`) ONLY when this patch INVALIDATED a baked
713
+ * raster — the annotation stayed `baked` (an opaque-body kind) and the edit
714
+ * resized its `/AP` frame, so the engine's re-bake produces new content (in
715
+ * practice: a stamp resize). The shell's resolve handler then turns it into
716
+ * an `upsert` with `bumpAp`. Absent for everything else — moves/rotations
717
+ * (the blit repositions the same pixels) and any kind that flipped to
718
+ * `vector` (it renders live; the raster stops mattering) — so those keep the
719
+ * bare `{ fx, id }` shape and trigger no appearance re-fetch. */
720
+ {
721
+ fx: 'patch';
722
+ id: Id;
723
+ scope: PatchScope;
724
+ apChanged?: true;
725
+ } |
726
+ /** A `/F`-only engine write for one committed annotation: the shell emits a
727
+ * flags-only patch (the model already holds the merged flags) and re-syncs
728
+ * PRESERVING the render source — flags never re-bake an appearance. */
729
+ {
730
+ fx: 'flags';
731
+ id: Id;
732
+ } |
733
+ /** The parent's `link` prop changed on a NON-link kind: reconcile its
734
+ * attached link children (create / retarget / delete) against the desired
735
+ * state derived from `link` + the parent's geometry. Declarative — the
736
+ * shell's reconciler is the only code that spells out child operations.
737
+ * (Geometry commits don't emit this; the shell re-runs the reconciler on
738
+ * any `patch` of an annotation that has `linkRefs`.) */
739
+ {
740
+ fx: 'syncLink';
741
+ id: Id;
742
+ } | {
743
+ fx: 'delete';
744
+ ref: AnnotationRef;
745
+ };
746
+ /** Per-annotation render data — its content geometry + style + live state. */
747
+ interface RenderItem {
748
+ id: Id;
749
+ ref: AnnotationRef | null;
750
+ subtype: Subtype;
751
+ geom: Geom;
752
+ /**
753
+ * The VISUAL box (geometry + stroke + line endings) in content space — the SAME
754
+ * `geomVisualBounds` that feeds the engine `/Rect`. The renderer paints into THIS
755
+ * box and does no bounds math of its own, so the on-screen box and the baked
756
+ * appearance can never drift (the v2 "patch computes the rect" rule).
757
+ */
758
+ box: Rect;
759
+ /**
760
+ * Content-space box the engine appearance raster occupies (the AP `/Rect`),
761
+ * with the live move gesture applied — so a baked annotation's bitmap follows
762
+ * a drag. Only meaningful when `source === 'baked'`; absent otherwise.
763
+ */
764
+ apBox?: Rect;
765
+ /**
766
+ * Rotation (deg, CW) to apply to the baked raster as a view transform —
767
+ * exactly the rotation the engine STRIPPED from it (see `Annot.apRot`).
768
+ * Live for `opaqueBody` kinds (a stamp spins with the rotate gesture);
769
+ * absent when the raster already contains its rotation (vertex kinds).
770
+ */
771
+ apRot?: number;
772
+ style: Style;
773
+ /** Text styling (/DA projection) — present for text-bearing kinds (free
774
+ * text, text/choice widgets). Lets a behavior renderer's focused editor
775
+ * match the baked appearance's font. */
776
+ text?: TextStyle;
777
+ source: 'baked' | 'vector' | 'ghost';
778
+ selected: boolean;
779
+ /** The pointer is over this annotation — scene-level hover affordances
780
+ * (e.g. a redaction mark previews its applied look). */
781
+ hovered?: boolean;
782
+ /** Redaction label projection (redact kind only) — see {@link Annot.label}. */
783
+ label?: {
784
+ text: string;
785
+ repeat: boolean;
786
+ };
787
+ /**
788
+ * Applied rotation (deg, CW), or 0/undefined. For BOX kinds (`rect`/`text`)
789
+ * `box` is the UNROTATED visual box and the renderer applies this rotation
790
+ * about its centre (CSS/SVG transform). For VERTEX kinds the geometry is
791
+ * already rotated, so this is advisory only — the renderer must NOT re-apply it.
792
+ */
793
+ rot?: number;
794
+ /**
795
+ * Mix-blend-mode the annotation composites with against the page (highlights
796
+ * multiply). The vector painter reads blend per scene node; the baked /AP image
797
+ * has no scene, so it reads this. Undefined = normal compositing.
798
+ */
799
+ blend?: Exclude<BlendMode$1, 'normal'>;
800
+ }
801
+ /** The dumb draw vocabulary the framework renderer maps to SVG (content space).
802
+ * A CLOSED node (rect, ellipse, closed poly) takes the annotation's FILL colour;
803
+ * an open node (line, open poly — open arrows, butt, slash) is stroke-only. The
804
+ * stroke colour applies to every node. Closed-ness is the only fill signal. */
805
+ type RenderNode = {
806
+ kind: 'rect';
807
+ rect: Rect;
808
+ } | {
809
+ kind: 'ellipse';
810
+ rect: Rect;
811
+ } | {
812
+ kind: 'line';
813
+ a: Vec;
814
+ b: Vec;
815
+ } | {
816
+ kind: 'poly';
817
+ points: Vec[];
818
+ closed: boolean;
819
+ } | {
820
+ kind: 'path';
821
+ d: string;
822
+ };
823
+ /**
824
+ * How to paint one node. The pure core fills this in (per kind/subtype), and a
825
+ * framework renderer applies it verbatim — so ALL appearance logic (markup fill vs
826
+ * stroke, blend, dash, derived widths) lives once, in the portable core, not in
827
+ * every framework. Omitted `fill`/`stroke` mean none.
828
+ */
829
+ interface Paint {
830
+ fill?: string;
831
+ stroke?: string;
832
+ width?: number;
833
+ opacity?: number;
834
+ dash?: number[];
835
+ blend?: Exclude<BlendMode$1, 'normal'>;
836
+ cap?: 'round';
837
+ join?: 'round';
838
+ }
839
+ /**
840
+ * A fully-painted draw node: geometry + paint. `scene(item)` returns these and a
841
+ * per-framework painter maps each to ONE element, applying `paint` — the entire
842
+ * surface a new framework renderer must implement. Supersedes the geometry-only
843
+ * `RenderNode` for rendering; `geomScene` stays the internal geometry helper.
844
+ */
845
+ type SceneNode = {
846
+ kind: 'rect';
847
+ rect: Rect;
848
+ paint: Paint;
849
+ } | {
850
+ kind: 'ellipse';
851
+ rect: Rect;
852
+ paint: Paint;
853
+ } | {
854
+ kind: 'line';
855
+ a: Vec;
856
+ b: Vec;
857
+ paint: Paint;
858
+ } | {
859
+ kind: 'poly';
860
+ points: Vec[];
861
+ closed: boolean;
862
+ paint: Paint;
863
+ } | {
864
+ kind: 'path';
865
+ d: string;
866
+ paint: Paint;
867
+ } |
868
+ /** Painted (non-interactive) text — `at` is the BASELINE start point in
869
+ * content units. Editable text (free text) stays a framework element;
870
+ * this is for pure pixels, e.g. a redaction label preview. */
871
+ {
872
+ kind: 'text';
873
+ at: Vec;
874
+ text: string;
875
+ fontSize: number;
876
+ fontFamily?: string;
877
+ paint: Paint;
878
+ };
879
+ /** Pure geometry settings for recognising and axis-snapping a freehand stroke. */
880
+ interface InkStraightenOptions {
881
+ /** Maximum `max perpendicular deviation / endpoint distance`. */
882
+ deviationThreshold: number;
883
+ /** Degrees from horizontal/vertical within which the line snaps to that axis. */
884
+ axisSnapDegrees: number;
885
+ }
886
+ type ChromeNode = {
887
+ kind: 'outline';
888
+ rect: Rect;
889
+ } | {
890
+ kind: 'obb';
891
+ corners: [Vec, Vec, Vec, Vec];
892
+ angle: number;
893
+ } | {
894
+ kind: 'handle';
895
+ at: Vec;
896
+ cursor: Cursor;
897
+ rot?: number;
898
+ } | {
899
+ kind: 'rotate-knob';
900
+ at: Vec;
901
+ from: Vec;
902
+ } | {
903
+ kind: 'guide';
904
+ axis: 'x' | 'y';
905
+ at: number;
906
+ lo: number;
907
+ hi: number;
908
+ } | {
909
+ kind: 'angle-chip';
910
+ at: Vec;
911
+ angle: number;
912
+ } | {
913
+ kind: 'rotate-guides';
914
+ center: Vec;
915
+ angle: number;
916
+ lines: Array<{
917
+ a: Vec;
918
+ b: Vec;
919
+ role: 'axis' | 'indicator';
920
+ }>;
921
+ } | {
922
+ kind: 'marquee';
923
+ rect: Rect;
924
+ };
925
+ //#endregion
926
+ //#region src/anchor.d.ts
927
+ /** Which display factors this annotation is exempt from: `zoom` =
928
+ * screen-constant size, `upright` = screen-constant orientation. */
929
+ interface AnchorMode {
930
+ zoom: boolean;
931
+ upright: boolean;
932
+ }
933
+ /**
934
+ * The anchor behaviors of one annotation: its `/F` flags OR'd with the kind's
935
+ * statics — the spec's "Text (note) annotations behave as if NoZoom and
936
+ * NoRotate are ALWAYS set", expressed as kind caps. Null when not anchored.
937
+ */
938
+ declare function anchorModeOf(a: FlagBearer): AnchorMode | null;
939
+ /** The fixed page point: the geometry's bounds top-left in content space
940
+ * (y-down) — which IS the spec's "upper-left corner of the annotation
941
+ * rectangle", since `/Rect` is emitted from these bounds. */
942
+ declare const anchorOf: (g: Geom) => Vec;
943
+ /**
944
+ * THE effective content-space geometry of a screen-anchored body at `view`:
945
+ * the stored geometry scaled by `1/max(zoom, 1)` about the anchor (`zoom`
946
+ * exemption, Adobe-clamped) and counter-rotated by `-rotation` about it
947
+ * (`upright`), so that after the page's own display transform the body reads
948
+ * at its 100%-zoom size and its authored tilt, hanging from the fixed anchor.
949
+ * Returns `g` unchanged when there is nothing to do — callers apply it
950
+ * unconditionally.
951
+ */
952
+ declare function anchoredGeom(g: Geom, mode: AnchorMode | null, view: ViewEnv | undefined): Geom;
953
+ /**
954
+ * The exact inverse of {@link anchoredGeom} for a VIEW-space geometry a
955
+ * gesture produced: the stored geometry whose OWN projection is `target`.
956
+ *
957
+ * Solving `anchoredGeom(stored) = target` where the projection anchors at
958
+ * `stored`'s (unknown) bounds top-left `a` gives a closed form: map `target`
959
+ * through the pure linear part (scale `s` + rotate `r` about the origin),
960
+ * then translate so the result's bounds top-left `b` lands where the
961
+ * projection needs it — at `a = R(-r)·b / s`, the unique fixed point. This is
962
+ * what makes a released gesture commit EXACTLY what its preview showed.
963
+ */
964
+ declare function unanchoredGeom(target: Geom, mode: AnchorMode | null, view: ViewEnv | undefined): Geom;
965
+ /** Stroke width in the projected (view) space: a `noZoom` body's line weight
966
+ * scales with its geometry so it stays screen-constant — through the SAME
967
+ * factors (incl. the ≤100% clamp) the geometry projects with. */
968
+ declare const anchoredStrokeWidth: (width: number, mode: AnchorMode | null, view: ViewEnv | undefined) => number;
969
+ /**
970
+ * The similarity image of an axis-aligned box (a baked raster's AP `/Rect`)
971
+ * under the anchor projection, as the blit vocabulary expects it: an
972
+ * UNROTATED box (the projected centre + scaled size) plus the rotation to
973
+ * re-apply about its centre. `anchor` is the OWNING GEOMETRY's anchor — the
974
+ * raster rides its annotation's projection, it doesn't anchor itself. Null
975
+ * when the projection is the identity.
976
+ */
977
+ declare function anchoredBox(box: {
978
+ x: number;
979
+ y: number;
980
+ width: number;
981
+ height: number;
982
+ }, anchor: Vec, mode: AnchorMode | null, view: ViewEnv | undefined): {
983
+ box: {
984
+ x: number;
985
+ y: number;
986
+ width: number;
987
+ height: number;
988
+ };
989
+ rot: number;
990
+ } | null;
991
+ //#endregion
992
+ //#region src/update.d.ts
993
+ /** The click ↔ drag threshold (content units): a press-release whose width AND
994
+ * height both stay under it is a CLICK. Exported so every gesture owner (the
995
+ * draw handler, the form plugin's place handler) shares ONE definition. */
996
+ declare const MIN_DRAG = 3;
997
+ declare const initialStyle: Style;
998
+ declare const initialModel: Model;
999
+ /**
1000
+ * Resolve a tool's effective defaults as a FULL flat props bag: the base `style`
1001
+ * + the font/endings base, with the per-tool override layered on top. This is
1002
+ * what a defaults-editing UI reads, and what creation projects `style`/`text`
1003
+ * from (`styleFromProps` / `textStyleFromProps`).
1004
+ */
1005
+ declare function defaultsFor(m: Model, subtype: Subtype): AnnotationProps;
1006
+ /**
1007
+ * The live rotation of a rotate draft, snapping applied — the ONE angle rule
1008
+ * shared by the preview (`effGeom`), the commit (`editUp`) and the angle chip,
1009
+ * so they can never disagree. The selection's ABSOLUTE angle (a single member's
1010
+ * `rot` + the raw pointer delta; a group's raw delta from 0) locks onto the
1011
+ * configured angles within the threshold; `free` (shift held) bypasses.
1012
+ * `delta` is what `geomRotateAbout` applies; `angle` is what the chip shows.
1013
+ */
1014
+ declare function rotateDraftDelta(m: Model, d: Extract<Draft, {
1015
+ g: 'rotate';
1016
+ }>): {
1017
+ delta: number;
1018
+ angle: number;
1019
+ snapped: boolean;
1020
+ };
1021
+ declare function update(m: Model, msg: Msg): [Model, Effect[]];
1022
+ //#endregion
1023
+ //#region src/placement.d.ts
1024
+ /** A resolved click placement: what the click will occupy, page-clamped. */
1025
+ type ClickPlacement = {
1026
+ kind: 'box';
1027
+ rect: Rect;
1028
+ /** Upright counter-rotation captured for the commit (deg CW; 0 = none). */
1029
+ rot: number;
1030
+ } | {
1031
+ kind: 'segment';
1032
+ a: Vec;
1033
+ b: Vec;
1034
+ };
1035
+ /** Slide a rect (as a unit) to sit inside `box`; pins at the origin edge when
1036
+ * it doesn't fit. Placements are page-bound; the pointer isn't. */
1037
+ declare const clampRectToBox: (r: Rect, box: Rect | undefined) => Rect;
1038
+ /**
1039
+ * Resolve a click-create POLICY at a content point. `anchor` defaults to
1040
+ * `center`; under `upright` a box counter-rotates against the page's display
1041
+ * rotation exactly as the drag commit would (centre-anchored boxes transpose
1042
+ * about their centre, top-left boxes anchor in the DISPLAY frame).
1043
+ */
1044
+ declare function resolveClickPlacement(point: Vec, policy: ClickCreate, opts?: {
1045
+ pageBox?: Rect;
1046
+ upright?: boolean;
1047
+ displayRotation?: PageRotation;
1048
+ }): ClickPlacement;
1049
+ /**
1050
+ * ANNOTATION-ONLY: convert a placement into the `Geom` the commit stores and
1051
+ * the ghost paints, for a routing kind. This is where annotation VISUAL
1052
+ * semantics live — ellipse for circles, the cloudy outer-box via
1053
+ * `shapeRectFor`. Forms never call this; a field box is the placement rect
1054
+ * itself. Null for kinds a click cannot author.
1055
+ */
1056
+ declare function clickCreateGeom(subtype: Subtype, placement: ClickPlacement, def: AnnotationProps): Geom | null;
1057
+ //#endregion
1058
+ //#region src/snap.d.ts
1059
+ interface SnapResult {
1060
+ delta: Vec;
1061
+ guides: Guide[];
1062
+ }
1063
+ /**
1064
+ * Snap a move delta: shift the selection's union bounds by `raw`, compare its
1065
+ * 3 keys per axis (min / center / max) against every target's, and take the
1066
+ * closest in-threshold pair per axis as an adjustment. Targets are the page box
1067
+ * (edges + center) and every non-moving annotation on the page.
1068
+ */
1069
+ declare function computeMoveSnap(m: Model, ids: Id[], pon: number, raw: Vec, threshold: number, pageBox: Rect | undefined): SnapResult;
1070
+ //#endregion
1071
+ //#region src/view.d.ts
1072
+ declare function pageItems(m: Model, pon: number, view?: ViewEnv): RenderItem[];
1073
+ /** One free-text box, geometry only (the live move/resize gesture applied), with
1074
+ * its edit flag. The framework renders an editable element here; the plugin
1075
+ * layers the DTO-derived text style on top. */
1076
+ interface TextBox {
1077
+ id: Id;
1078
+ box: Rect;
1079
+ editing: boolean;
1080
+ /** Applied rotation (deg, CW). `box` is the UNROTATED text box; the framework
1081
+ * rotates the editable element about its centre by this. 0/undefined = none. */
1082
+ rot?: number;
1083
+ }
1084
+ /** The free-text boxes on a page — the text counterpart of `pageItems`. */
1085
+ declare function textBoxes(m: Model, pon: number, view?: ViewEnv): TextBox[];
1086
+ /** The currently selected annotations as render items (live gesture applied) —
1087
+ * cross-page, for selection-aware toolbars (style + line-ending editing). */
1088
+ declare function selectedItems(m: Model, view?: ViewEnv): RenderItem[];
1089
+ /**
1090
+ * The rotate knob for the current selection on `pon` — the SAME knob `chrome`
1091
+ * draws — or null when the selection has none (non-rotatable single, or a group
1092
+ * whose caps aren't rotatable). A single shape's knob hangs off its OBB top edge;
1093
+ * a group's off the union box. With `pageBox` the knob is placed PAGE-BOUND
1094
+ * (`placeRotateKnob`: flip below / clamp inside) — off-page it would be visible
1095
+ * but unreachable, since pointer dispatch resolves pages by containment. Shared
1096
+ * by `chrome` (to draw) and `selectionAnchor` (to push the menu clear of it), so
1097
+ * the two can never disagree on where it is.
1098
+ *
1099
+ * The placement DECISION is made at rest; a gesture never re-decides — it RIDES.
1100
+ * During a live rotate the knob starts exactly where it was grabbed (the rest
1101
+ * placement on the COMMITTED geometry) and turns rigidly about the pivot by the
1102
+ * same snapped delta the shape uses; the policy re-evaluates only on release.
1103
+ * Re-deciding per frame would jump at grab and side-switch mid-spin.
1104
+ */
1105
+ declare function selectionKnob(m: Model, pon: number, pageBox?: Rect, knobOffset?: number, view?: ViewEnv): {
1106
+ at: Vec;
1107
+ from: Vec;
1108
+ } | null;
1109
+ declare function chrome(m: Model, pon: number, pageBox?: Rect, knobOffset?: number, view?: ViewEnv): ChromeNode[];
1110
+ /** The content-space union of the selectable selected items on `pon`, or null if
1111
+ * the page holds none. This is the SAME box the chrome outline draws, so a
1112
+ * floating menu sits exactly on the selection. */
1113
+ declare function selectionBoundsOnPage(m: Model, pon: number, view?: ViewEnv): Rect | null;
1114
+ /** The anchor for a selection-aware floating menu: the PRIMARY page (the first
1115
+ * selectable selected id) + the union box of the selection on that page (content
1116
+ * space). Null when nothing selectable is selected. A cross-page selection
1117
+ * anchors to its primary page, so there is exactly one menu. `pageBoxOf` is a
1118
+ * lookup (the anchor resolves its own page) feeding the page-bound knob
1119
+ * placement — the menu then dodges the knob where it ACTUALLY sits. */
1120
+ declare function selectionAnchor(m: Model, pageBoxOf?: (pon: number) => Rect | undefined, knobOffsetOf?: (pon: number) => number | undefined, viewOf?: (pon: number) => ViewEnv | undefined): {
1121
+ pon: number;
1122
+ bounds: Rect;
1123
+ knob?: Vec;
1124
+ } | null;
1125
+ /** Anchor for controls that finish/cancel an active multi-click creation draft.
1126
+ * It is rect-based like selectionAnchor, using the committed vertices only so
1127
+ * the menu remains stable while the hover preview follows the pointer. */
1128
+ declare function creationDraftAnchor(m: Model): CreationDraftAnchor | null;
1129
+ //#endregion
1130
+ //#region src/hit.d.ts
1131
+ type Target = {
1132
+ t: 'handle';
1133
+ id: Id;
1134
+ handle: string;
1135
+ cursor: Cursor;
1136
+ } | {
1137
+ t: 'rotate';
1138
+ ids: Id[];
1139
+ pivot: Vec;
1140
+ } | {
1141
+ t: 'group-handle';
1142
+ ids: Id[];
1143
+ handle: string;
1144
+ cursor: Cursor;
1145
+ box: Rect;
1146
+ } | {
1147
+ t: 'annot';
1148
+ id: Id;
1149
+ } | {
1150
+ t: 'empty';
1151
+ };
1152
+ /** Can this annotation be clicked to select? (`/F` interaction flags override
1153
+ * all caps — hidden/noView/readOnly are inert; LOCKED stays selectable, it
1154
+ * just won't transform.) */
1155
+ declare const isSelectable: (m: Model, id: Id) => boolean;
1156
+ /** Can this annotation be dragged by its body to move? (`locked` freezes it.) */
1157
+ declare const canMove: (m: Model, id: Id) => boolean;
1158
+ /**
1159
+ * What's under the content point.
1160
+ * 1. a resize/vertex handle of the single selection,
1161
+ * 2. an editable annotation body — a SELECTED one anywhere in its bounds (so you
1162
+ * can drag to move it), an UNSELECTED one only on its stroke/fill (margin-aware,
1163
+ * so an unfilled circle is grabbed only on its outline),
1164
+ * 3. else empty.
1165
+ */
1166
+ declare function hitTest(m: Model, pon: number, p: Vec, geom: ChromeGeom, strokeMargin: number, pageBox?: Rect, inert?: ReadonlySet<Id>, view?: ViewEnv): Target;
1167
+ /** The cursor to show on hover: a resize cursor over a handle, move/pointer over a body. */
1168
+ declare function cursorAt(m: Model, pon: number, p: Vec, geom: ChromeGeom, strokeMargin: number, pageBox?: Rect, inert?: ReadonlySet<Id>, view?: ViewEnv): Cursor | null;
1169
+ //#endregion
1170
+ //#region src/group.d.ts
1171
+ /**
1172
+ * The transform capabilities of a multi-target (group) selection: a group can
1173
+ * move/resize/rotate only if EVERY member's kind allows that group op AND no
1174
+ * member is locked. This is the SINGLE resolver every adapter consumes (v2
1175
+ * duplicated the `.every(...)` across three framework components). The iso-vs-
1176
+ * aniso resize choice is computed separately, per-gesture, since it depends on
1177
+ * the live `rot` of the members, not the static caps.
1178
+ */
1179
+ interface GroupCaps {
1180
+ movable: boolean;
1181
+ resizable: boolean;
1182
+ rotatable: boolean;
1183
+ }
1184
+ declare function groupCaps(m: Model, ids: Id[]): GroupCaps;
1185
+ /**
1186
+ * The key of the group `id` belongs to, or `null` when it is ungrouped. A
1187
+ * subordinate's key is its `group` field; a primary's key is its own id (it is
1188
+ * the target of at least one member's `group`). An annotation that is neither a
1189
+ * subordinate nor the target of any subordinate is ungrouped.
1190
+ */
1191
+ declare function groupKeyOf(m: Model, id: Id): Id | null;
1192
+ /**
1193
+ * Every member of the group containing `id` (primary first), or just `[id]`
1194
+ * when it is ungrouped. The primary is the annotation whose id is the key; the
1195
+ * rest are everything whose `group` equals the key, in `order`.
1196
+ */
1197
+ declare function groupMembers(m: Model, id: Id): Id[];
1198
+ /** Union of every id's full group — the selection seen as whole groups. */
1199
+ declare function expandGroups(m: Model, ids: Id[]): Id[];
1200
+ //#endregion
1201
+ //#region src/kinds.d.ts
1202
+ /**
1203
+ * One editable property of a kind, as a UI contract: which {@link AnnotationProps}
1204
+ * key, rendered how (the union arm fixes the control + its constraints), labelled
1205
+ * what by default. A property sidebar is a `switch (spec.key)` over these — the
1206
+ * per-kind lists below are the v2 snippet's hand-rolled `TOOL_PROPERTIES` schema,
1207
+ * promoted into the library so every consumer gets it for free.
1208
+ *
1209
+ * `label` is a default (English) display name — apps with i18n map `key`s to
1210
+ * their own strings and ignore it. Array ORDER is display order.
1211
+ */
1212
+ type PropSpec = {
1213
+ key: 'color';
1214
+ label: string;
1215
+ } | {
1216
+ key: 'interiorColor';
1217
+ label: string;
1218
+ } | {
1219
+ key: 'fontColor';
1220
+ label: string;
1221
+ } | {
1222
+ key: 'opacity';
1223
+ label: string;
1224
+ min: number;
1225
+ max: number;
1226
+ step: number;
1227
+ } | {
1228
+ key: 'strokeWidth';
1229
+ label: string;
1230
+ min: number;
1231
+ max: number;
1232
+ step: number;
1233
+ } | {
1234
+ key: 'fontSize';
1235
+ label: string;
1236
+ min: number;
1237
+ max: number;
1238
+ step: number;
1239
+ } |
1240
+ /** Border style picker; `cloudy` says whether this kind honours a cloudy border. */
1241
+ {
1242
+ key: 'border';
1243
+ label: string;
1244
+ cloudy: boolean;
1245
+ } | {
1246
+ key: 'lineEndings';
1247
+ label: string;
1248
+ } | {
1249
+ key: 'fontFamily';
1250
+ label: string;
1251
+ } | {
1252
+ key: 'textAlign';
1253
+ label: string;
1254
+ } | {
1255
+ key: 'blendMode';
1256
+ label: string;
1257
+ } |
1258
+ /** `/Name` icon picker for icon kinds; `options` are the legal names. */
1259
+ {
1260
+ key: 'icon';
1261
+ label: string;
1262
+ options: readonly string[];
1263
+ } |
1264
+ /** Link-target editor (URL / page destination). Declared by the link kind
1265
+ * (its own target) and by every kind that may carry an ATTACHED link;
1266
+ * kinds that omit it (widgets, caret, redact…) simply cannot be links —
1267
+ * `applyProps` drops the key and menus never show the control. */
1268
+ {
1269
+ key: 'link';
1270
+ label: string;
1271
+ };
1272
+ /** Orthogonal capability flags. Static data — the annotation's `/F` flags are
1273
+ * the runtime overrides (a locked annotation is never transformable, a hidden
1274
+ * one never renders, regardless of these — see flags.ts). */
1275
+ interface KindCaps {
1276
+ /** Can be clicked to select. */
1277
+ selectable: boolean;
1278
+ /** Can be dragged (by its body) to translate. */
1279
+ movable: boolean;
1280
+ /** Exposes the 8 box resize handles (shapes). */
1281
+ resizable: boolean;
1282
+ /** Exposes per-vertex handles (line endpoints, polygon/polyline vertices). */
1283
+ vertexEditable: boolean;
1284
+ /** Can be rotated (shapes, free text, lines/polys/ink). */
1285
+ rotatable: boolean;
1286
+ /** Can be MOVED as part of a multi-target (group) transform. */
1287
+ groupMovable: boolean;
1288
+ /** Can be uniformly SCALED as part of a multi-target (group) transform — ON
1289
+ * even for vertex kinds that have no single-shape box resize (their handles
1290
+ * ARE the vertices; in a group they scale fine). */
1291
+ groupResizable: boolean;
1292
+ /** Can be ROTATED as part of a multi-target (group) transform. */
1293
+ groupRotatable: boolean;
1294
+ /** Carries editable text content (free text, the comment popup). */
1295
+ textEditable: boolean;
1296
+ /** Can carry a comment/note + threaded replies (`/Contents` + `/Popup`). */
1297
+ commentable: boolean;
1298
+ /** Has a popup as its primary surface (the comment/Text icon). */
1299
+ hasPopup: boolean;
1300
+ /** Bound to underlying text (markup, caret) — never freely moved/resized. */
1301
+ anchored: boolean;
1302
+ /** Has an interior fill (`/IC`). */
1303
+ hasFill: boolean;
1304
+ /** Has line endings (`/LE` — line, polyline). */
1305
+ hasEndings: boolean;
1306
+ /** Can take a cloudy border effect (`/BE` — shapes). */
1307
+ hasCloudy: boolean;
1308
+ /** The whole body is visible content, so hit-testing grabs anywhere inside
1309
+ * the box (stamp images) — NOT just the stroke/fill like outline shapes. */
1310
+ opaqueBody: boolean;
1311
+ /** The `/F` ReadOnly flag is IGNORED for this kind (ISO 32000: widgets — a
1312
+ * ReadOnly form FIELD must still be movable by a form designer; the
1313
+ * form-filling layer enforces field ReadOnly itself). */
1314
+ ignoresReadOnly: boolean;
1315
+ /** Behaves as if `/F` NoZoom is always set (screen-constant size — the
1316
+ * spec's rule for Text/note icons). See anchor.ts. */
1317
+ noZoom: boolean;
1318
+ /** Behaves as if `/F` NoRotate is always set (screen-upright — the spec's
1319
+ * rule for Text/note icons). See anchor.ts. */
1320
+ noRotate: boolean;
1321
+ }
1322
+ interface AnnotationKind {
1323
+ subtype: Subtype;
1324
+ /** Future: PDF `/IT` intent (free-text vs callout, caret insert vs replace). */
1325
+ intent?: string;
1326
+ variant: Geom['t'];
1327
+ caps: KindCaps;
1328
+ /** The kind's editable properties, in DISPLAY ORDER — the contract a property
1329
+ * sidebar renders from (see {@link PropSpec}). Empty = nothing to edit. */
1330
+ props: PropSpec[];
1331
+ }
1332
+ /** The built-in kinds. Shapes resize, lines/polys vertex-edit, markup is anchored
1333
+ * (selectable + recolor/delete, never move/resize). */
1334
+ declare const KINDS: Record<string, AnnotationKind>;
1335
+ /** The capabilities of a subtype, or the read-only default for unknown kinds. */
1336
+ declare const capsFor: (subtype: string) => KindCaps;
1337
+ /** A kind's editable properties in display order — empty for unknown kinds.
1338
+ * Stable references, so selectors can compare by identity. */
1339
+ declare const propsFor: (subtype: string) => PropSpec[];
1340
+ //#endregion
1341
+ //#region src/props.d.ts
1342
+ /** Base text styling for kinds/tools with no explicit defaults. */
1343
+ declare const initialTextStyle: TextStyle;
1344
+ /** The `Style` slice of a resolved props bag (the create-time projection). */
1345
+ declare const styleFromProps: (p: AnnotationProps) => Style;
1346
+ /** The `TextStyle` slice of a resolved props bag (the create-time projection). */
1347
+ declare const textStyleFromProps: (p: AnnotationProps) => TextStyle;
1348
+ /**
1349
+ * Read one property off an annotation — from wherever it lives — or `undefined`
1350
+ * when the annotation's kind doesn't carry it (a `fontSize` on a square, endings
1351
+ * on a polygon). The read side of `applyProps`.
1352
+ */
1353
+ declare function readProp<K extends PropKey>(a: Annot, key: K): AnnotationProps[K] | undefined;
1354
+ /**
1355
+ * Apply a property patch to one annotation, honouring its kind's declared keys.
1356
+ * Returns the changed annotation, or `null` when nothing applied (locked /
1357
+ * read-only per its `/F` flags, or no declared key in the patch) — so the
1358
+ * caller emits no spurious engine write. Flags themselves are NOT props: they
1359
+ * write through the `setFlags` message, which is deliberately not gated here
1360
+ * (unlocking must work on a locked annotation).
1361
+ */
1362
+ declare function applyProps(a: Annot, patch: AnnotationPropsPatch): Annot | null;
1363
+ /**
1364
+ * The ordered property specs EVERY given kind declares — the schema for a mixed
1365
+ * selection, in the FIRST kind's display order. One kind → its own list, verbatim.
1366
+ */
1367
+ declare function sharedProps(subtypes: readonly string[]): PropSpec[];
1368
+ //#endregion
1369
+ //#region src/geometry.d.ts
1370
+ type RectHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';
1371
+ declare const RECT_HANDLES: RectHandle[];
1372
+ /** The resize cursor for a handle on a box tilted by `rot` deg: rotate the
1373
+ * handle's compass direction with the box and pick the nearest 45° sector.
1374
+ * At `rot = 0` this reproduces {@link RECT_CURSOR} exactly. */
1375
+ declare const rotatedHandleCursor: (h: RectHandle, rot: number) => Cursor;
1376
+ declare const rectFromPoints: (a: Vec, b: Vec) => Rect;
1377
+ declare function unionRect(pts: Vec[]): Rect;
1378
+ /** How far (content units) the rotate knob hangs off the top edge of the box. */
1379
+ declare const ROTATE_KNOB_OFFSET = 24;
1380
+ /** Fallback chrome geometry (content units) when the caller supplies none —
1381
+ * the pre-settings behavior, so bare-core callers and tests stay stable. */
1382
+ declare const DEFAULT_CHROME_GEOM: {
1383
+ readonly handleTol: 6;
1384
+ readonly knobTol: 6;
1385
+ readonly knobOffset: 24;
1386
+ };
1387
+ /** Normalize degrees into `[0, 360)`. */
1388
+ declare const normalizeDeg: (d: number) => number;
1389
+ /** A geom's applied rotation (deg), or 0 for the non-rotatable kinds. */
1390
+ declare function geomRotation(g: Geom): number;
1391
+ /**
1392
+ * The rotation PIVOT for a single shape: a box turns about its own `rect`
1393
+ * centre; a vertex shape about the CENTROID of its points (the mean). Rotating a
1394
+ * point set about its centroid leaves the centroid fixed, so the advisory `rot`
1395
+ * stays cleanly additive across gestures and reset is exact.
1396
+ */
1397
+ declare function centroidOf(g: Geom): Vec;
1398
+ /** Is this kind rotatable (the geometry carries a meaningful `rot`)? */
1399
+ declare function isRotatableGeom(g: Geom): boolean;
1400
+ /**
1401
+ * Rotate a geom by `deltaDeg` (clockwise) about `pivot`.
1402
+ * - BOX (`rect`/plain `text`): orbit the box centre about the pivot (a rigid
1403
+ * translation of the unrotated `rect`) and add the angle to `rot`. When the
1404
+ * pivot IS the box centre this is a pure `rot += delta`.
1405
+ * - VERTEX (`line`/`poly`/`ink`): map every point through the rotation AND bump
1406
+ * the advisory `rot` (the points stay the authoritative visual).
1407
+ * Non-rotatable kinds (caret/quads, callouts) are returned unchanged.
1408
+ */
1409
+ declare function geomRotateAbout(g: Geom, pivot: Vec, deltaDeg: number): Geom;
1410
+ /** The AABB of `rect` rotated `deg` about its own centre. */
1411
+ declare function rotatedAabb(rect: Rect, deg: number): Rect;
1412
+ /**
1413
+ * The counter-rotation (CW content degrees) that makes an annotation read
1414
+ * upright at `displayRotation`: on screen the two compose to 0.
1415
+ */
1416
+ declare const uprightRotation: (displayRotation: number) => number;
1417
+ /** `rect` with width↔height swapped about its own centre — the unrotated box
1418
+ * whose quarter-turn AABB is exactly `rect` again. */
1419
+ declare function transposedAboutCenter(rect: Rect): Rect;
1420
+ /**
1421
+ * The unrotated content rect for a DEFAULT-SIZE upright box "placed at" a
1422
+ * point: positioned so that, after the upright counter-rotation, the box shows
1423
+ * `width`×`height` with its TOP-LEFT at `anchor` in the ROTATED view — the
1424
+ * same on-screen feel as the rotation-0 `{ x: anchor.x, y: anchor.y }` box at
1425
+ * every quarter-turn (a click-created free-text box always opens down-right of
1426
+ * the cursor as the author sees it). Derived by placing the box in the display
1427
+ * frame and pulling its AABB back through the quarter-turn; the page dims
1428
+ * cancel, so no page box is needed.
1429
+ */
1430
+ declare function uprightAnchoredRect(anchor: Vec, width: number, height: number, displayRotation: number): Rect;
1431
+ /**
1432
+ * The LOGICAL (unrotated) content-space box for a stamp placed at `center`,
1433
+ * sized `desired` (points), fit onto the page and clamped fully within it —
1434
+ * the v2 rubber-stamp rule: keep the image's own size unless it would overflow
1435
+ * the page, then scale down (aspect preserved, never up) so it just fits.
1436
+ *
1437
+ * Under an upright quarter-turn the on-page FOOTPRINT is the box transposed
1438
+ * (w↔h), so BOTH the fit and the edge clamp use that footprint — a landscape
1439
+ * stamp uprighted onto a portrait page fits by its rotated silhouette, not its
1440
+ * logical box. The box centre and its footprint AABB centre coincide (rotation
1441
+ * is about the centre), so clamping one clamps the other.
1442
+ */
1443
+ declare function fitStampBox(center: Vec, desired: Size, page: Size, rotCW: number): Rect;
1444
+ /** Reset a geom to its as-authored orientation (`rot → 0`). Box: drop `rot`.
1445
+ * Vertex: spin the points by `-rot` about their centroid so they return to the
1446
+ * orientation they were drawn at, in place. */
1447
+ declare function geomResetRotation(g: Geom): Geom;
1448
+ /**
1449
+ * The oriented selection box (OBB) of a rotatable geom: four corners (in order
1450
+ * nw, ne, se, sw of the LOCAL box, transformed) + the angle. For a box this is
1451
+ * the `rect` rotated about its centre; for a vertex shape it is reconstructed
1452
+ * from the advisory `rot` — un-rotate the points to recover the as-authored
1453
+ * shape, take that tight local box, then rotate it back — giving the snug tilted
1454
+ * rectangle. Returns null for non-rotatable kinds.
1455
+ */
1456
+ declare function obbFromGeom(g: Geom, strokeWidth: number, border?: Border): {
1457
+ corners: [Vec, Vec, Vec, Vec];
1458
+ angle: number;
1459
+ } | null;
1460
+ /** The fixed point of a group resize: the OPPOSITE handle's point on the box. */
1461
+ declare function groupResizeAnchor(base: Rect, handle: string): Vec;
1462
+ /**
1463
+ * The live group resize box for a drag. Anisotropic = the plain axis-aligned
1464
+ * `resizeRect`; isotropic = a uniform scale of `base` about the anchor by the
1465
+ * larger of the two drag ratios (so the preview matches the committed scale and
1466
+ * never shears a rotated member).
1467
+ */
1468
+ declare function groupResizeBox(base: Rect, handle: string, to: Vec, isotropic: boolean): Rect;
1469
+ /** The (sx, sy) factors a `base`→`cur` group resize applied about its anchor. */
1470
+ declare function groupResizeFactors(base: Rect, cur: Rect): {
1471
+ sx: number;
1472
+ sy: number;
1473
+ };
1474
+ /**
1475
+ * Scale a geom about `anchor` by `(sx, sy)`. For a rotated box (iso scale, so
1476
+ * `sx === sy`) the unrotated `rect` is scaled about the anchor and `rot` is
1477
+ * preserved (a uniform scale commutes with rotation). For unrotated members
1478
+ * (the anisotropic case) every point/extent scales directly.
1479
+ */
1480
+ declare function geomScaleAbout(g: Geom, anchor: Vec, sx: number, sy: number): Geom;
1481
+ /** Where the rotate knob sits, given the OBB corners (nw, ne, se, sw) and the
1482
+ * outward offset (content units). `from` is the top-edge midpoint the connector
1483
+ * stalk anchors to; `at` is the grab dot, offset along the outward normal. */
1484
+ declare function rotateKnob(corners: [Vec, Vec, Vec, Vec], offset: number): {
1485
+ at: Vec;
1486
+ from: Vec;
1487
+ };
1488
+ /**
1489
+ * `rotateKnob` with a TOTAL page-bound placement policy: annotations, gestures
1490
+ * and chrome are all page-bound, so the grab dot must land inside `pageBox` —
1491
+ * off-page it is unreachable (the pointer dispatch resolves pages by
1492
+ * containment). Policy: top edge (the default) → FLIP to the bottom edge when
1493
+ * the stalk exits the page → CLAMP the top candidate inside (degenerate: the
1494
+ * selection spans ~the whole page; the knob may then overlap the shape, and
1495
+ * hit-testing checks it first so it stays grabbable). Both render (`chrome`)
1496
+ * and hit-test (`hitTest`) place the knob through THIS function, so what you
1497
+ * see is what you can grab — by construction. No `pageBox` → the raw knob.
1498
+ */
1499
+ declare function placeRotateKnob(corners: [Vec, Vec, Vec, Vec], offset: number, pageBox?: Rect): {
1500
+ at: Vec;
1501
+ from: Vec;
1502
+ };
1503
+ /** The box-edge midpoint the leader connects to: the side `ref` (the knee, else
1504
+ * the tip) points toward, by horizontal/vertical dominance vs the box centre.
1505
+ * With `rot` (a callout box tilted by the upright policy) the decision runs in
1506
+ * the box's LOCAL frame and the midpoint lands back on the ROTATED edge — the
1507
+ * edge the user actually sees. Mirror of v2's `computeCalloutConnectionPoint`. */
1508
+ declare function calloutConnection(box: Rect, ref: Vec, rot?: number): Vec;
1509
+ /** The leader polyline `[tip, knee?, conn]`, with `conn` derived from the box
1510
+ * (its ROTATED footprint when the box carries `rot`). */
1511
+ declare function calloutLinePoints(g: Extract<Geom, {
1512
+ t: 'text';
1513
+ }>): Vec[];
1514
+ /**
1515
+ * The stored `rect` for a freshly-drawn shape, given the box the user dragged. A
1516
+ * cloudy border stores the OUTER box (dragged + cloud extent), so the dragged box
1517
+ * is its inner edge and the scallops bulge out to the stored box — just like a
1518
+ * solid shape, whose /Rect is the dragged box. So `g.rect` is ALWAYS the outer box;
1519
+ * the cloud-vs-solid difference lives only here, at creation.
1520
+ */
1521
+ declare function shapeRectFor(dragged: Rect, ellipse: boolean, style: Style): Rect;
1522
+ declare function caretRectFromTextEnd(lineRect: Rect): Rect;
1523
+ /** Miter limit shared by the bounds math AND the SVG renderer (`stroke-miterlimit`),
1524
+ * so the computed box and the drawn stroke always agree on where a sharp join
1525
+ * bevels instead of spiking. 10 = the PDF default (also what the baked /AP uses). */
1526
+ declare const MITER_LIMIT = 10;
1527
+ /**
1528
+ * A geom's VISUAL bounds: the rect that encloses the drawn appearance, so the
1529
+ * baked /AP is never clipped and the selection outline wraps exactly what's drawn.
1530
+ *
1531
+ * A shape's `rect` IS its visual box (the PDF /Rect): solid/dashed strokes draw
1532
+ * inside it, and a cloudy border's scallops also inset back into it (the dragged
1533
+ * inner edge sits `/RD` in from it — see `shapeRectFor`). So growing the stroke or
1534
+ * the cloud thickens inward, never spilling past the handles. Lines / polylines /
1535
+ * ink have no box, so they expand by the stroke (+ endings) — the SAME math feeds
1536
+ * `geomScene`, so the visual box and what's drawn always agree.
1537
+ *
1538
+ * `border` matters for ONE case: a CLOSED poly with a cloudy border, whose curls
1539
+ * are centred on the vertex path and reach OUTWARD (there is no outer box to
1540
+ * inset into) — the bounds grow by the cloud extent, and the engine `/Rect`
1541
+ * (via `geomPdfBounds`) grows with them so the baked scallops are never clipped.
1542
+ */
1543
+ declare function geomVisualBounds(g: Geom, strokeWidth: number, border?: Border): Rect;
1544
+ /**
1545
+ * The rect the SELECTION wraps — and the region a SELECTED annotation can be grabbed
1546
+ * from. Centre-line geometries (line / polyline / polygon / ink) straddle their path,
1547
+ * so this is their VISUAL bounds (the join-aware stroke outline + endings) — a
1548
+ * polygon's outline wraps its stroke exactly like a polyline. Box kinds (square /
1549
+ * circle / free-text) sit tight on their `rect` (their stroke draws INSIDE the box,
1550
+ * so the 8 resize handles land on the corners). The chrome outline AND the selected
1551
+ * hit-test both call this, so what you see highlighted is exactly what you can grab —
1552
+ * they can never drift.
1553
+ */
1554
+ declare function selectionBounds(g: Geom, strokeWidth: number, border?: Border): Rect;
1555
+ /**
1556
+ * The four corners of the ORIENTED selection box — the SAME quad `chrome` draws.
1557
+ * For a rotatable kind this is the OBB (the tilted box, from `obbFromGeom`); at
1558
+ * `rot == 0` those are just the axis-aligned corners of `selectionBounds`, and
1559
+ * for non-rotatable kinds (markup quads, callouts) it falls back to the rect
1560
+ * corners. The grab region, the floating-menu anchor and the multi/group union
1561
+ * all consume this, so what you can grab / where the menu sits never drifts from
1562
+ * the outline you see.
1563
+ */
1564
+ declare function selectionQuad(g: Geom, strokeWidth: number, border?: Border): [Vec, Vec, Vec, Vec];
1565
+ /** Is the point inside the (convex) selection quad? Even-odd ring test. */
1566
+ declare const pointInQuad: (p: Vec, quad: [Vec, Vec, Vec, Vec]) => boolean;
1567
+ /**
1568
+ * Does the (convex) selection quad intersect an axis-aligned rect? Separating
1569
+ * axis test on the only four candidate axes — the rect's x/y plus the quad's
1570
+ * two edge normals; a gap on any axis proves disjoint, otherwise they overlap
1571
+ * (touching counts). This is the marquee's predicate: it must catch a tilted
1572
+ * shape by the ORIENTED box that is actually drawn — the AABB of that quad has
1573
+ * empty corners covering most of the unrotated footprint, so testing it selects
1574
+ * shapes the marquee never touched. At `rot 0` the quad is axis-aligned and
1575
+ * this degenerates to exactly `rectsIntersect`.
1576
+ */
1577
+ declare function quadIntersectsRect(quad: [Vec, Vec, Vec, Vec], r: Rect): boolean;
1578
+ /**
1579
+ * The centre of the ORIENTED selection box — the middle of the rect you see.
1580
+ * For box kinds this is the rect centre (so squares/circles are unchanged); for
1581
+ * vertex kinds it is the OBB centre, so rotation spins the shape in place rather
1582
+ * than swinging it about the off-centre vertex mean (`centroidOf`).
1583
+ */
1584
+ declare function selectionCenter(g: Geom, strokeWidth: number): Vec;
1585
+ declare function geomBounds(g: Geom): Rect;
1586
+ /**
1587
+ * Is the content point ON the annotation: within `margin` of the stroke, or
1588
+ * inside the fill (when `filled`). The stroke band widens with the stroke width.
1589
+ *
1590
+ * A shape's stroke is drawn INSIDE its box, centred on `insetRect(rect, sw/2)`
1591
+ * (see `geomScene`), so the clickable band follows that inset centre-line — not
1592
+ * the box edge. The fill still reaches the box. (Cloudy scallops aren't modelled
1593
+ * here; with their typically thin stroke the inset is sub-pixel, so this matches.)
1594
+ */
1595
+ declare function geomHit(g: Geom, p: Vec, margin: number, filled: boolean, strokeWidth: number): boolean;
1596
+ declare function geomHandles(g: Geom): Handle[];
1597
+ declare function geomTranslate(g: Geom, d: Vec): Geom;
1598
+ declare function geomDragHandle(g: Geom, handle: string, to: Vec): Geom;
1599
+ declare function geomScene(g: Geom, strokeWidth?: number, border?: Border): RenderNode[];
1600
+ declare const pdfToContentPoint: (p: PdfPoint, crop: PdfRect) => Vec;
1601
+ declare const contentToPdfPoint: (p: Vec, crop: PdfRect) => PdfPoint;
1602
+ declare const pdfToContentRect: (pdf: PdfRect, crop: PdfRect) => Rect;
1603
+ declare const contentToPdfRect: (r: Rect, crop: PdfRect) => PdfRect;
1604
+ /** A geom's VISUAL bounding box (geometry + stroke + line endings) as a PdfRect —
1605
+ * the engine requires an explicit `rect` that encloses the baked /AP. */
1606
+ declare const geomPdfBounds: (g: Geom, strokeWidth: number, crop: PdfRect, border?: Border) => PdfRect;
1607
+ //#endregion
1608
+ //#region src/cloudy.d.ts
1609
+ /**
1610
+ * The per-side inset (content units) from a shape's outer box to the cloud's
1611
+ * inner boundary — the scallop radius plus half the stroke. This IS the `/RD`
1612
+ * the engine stores, and it's sized so the scallop peaks reach back out to the
1613
+ * box edge: the box remains the annotation's outer boundary.
1614
+ */
1615
+ declare function cloudyBorderExtent(intensity: number, strokeWidth: number, ellipse: boolean): number;
1616
+ /**
1617
+ * SVG path data for a cloudy square (rect) or circle (ellipse), in ABSOLUTE
1618
+ * content coordinates. The scallops are generated on the box inset by
1619
+ * `cloudyBorderExtent` and bulge back out to `box`'s edge.
1620
+ */
1621
+ declare function cloudyPath(box: Rect, ellipse: boolean, intensity: number, strokeWidth: number): string;
1622
+ //#endregion
1623
+ //#region src/scene.d.ts
1624
+ /** The full painted scene for one annotation. */
1625
+ declare function scene(item: RenderItem): SceneNode[];
1626
+ //#endregion
1627
+ //#region src/ink.d.ts
1628
+ /**
1629
+ * Recognise a sufficiently straight freehand stroke and axis-snap it when it is
1630
+ * close to horizontal or vertical. Diagonal strokes remain untouched: this is
1631
+ * a highlighter aid, not a general line simplifier.
1632
+ */
1633
+ declare function straightenInkStroke(points: readonly Vec[], options: InkStraightenOptions): Vec[];
1634
+ //#endregion
1635
+ export { type AnchorMode, type Annot, type AnnotationFlags, type AnnotationKind, type AnnotationProps, type AnnotationPropsPatch, type BlendMode, type Border, type Callout, type ChromeGeom, type ChromeNode, type ClickCreate, type ClickPlacement, type CreationDraftAnchor, type Cursor, DEFAULT_CHROME_GEOM, DRAWN_FLAGS, type Draft, type Effect, FLAG_KEYS, type FlagBearer, type Geom, type GroupCaps, type Guide, type Handle, type Id, type InkStraightenOptions, KINDS, type KindCaps, type LineEnding, type LineEndings, MIN_DRAG, MITER_LIMIT, type Model, type Msg, NO_ANNOTATION_FLAGS, type Paint, type PatchScope, type PointerInput, type PropKey, type PropSpec, type Quad, RECT_HANDLES, ROTATE_KNOB_OFFSET, type Rect, type RectHandle, type RenderItem, type RenderNode, type SceneNode, type SnapResult, type SnapSettings, type Style, type Subtype, type Target, type TextAlign, type TextBox, type TextStyle, type Vec, type ViewEnv, anchorModeOf, anchorOf, anchoredBox, anchoredGeom, anchoredStrokeWidth, annotContentsEditable, annotInteractive, annotTransformable, applyProps, calloutConnection, calloutLinePoints, canMove, capsFor, caretRectFromTextEnd, centroidOf, chrome, clampRectToBox, clickCreateGeom, cloudyBorderExtent, cloudyPath, computeMoveSnap, contentToPdfPoint, contentToPdfRect, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, flagsEqual, geomBounds, geomDragHandle, geomHandles, geomHit, geomPdfBounds, geomResetRotation, geomRotateAbout, geomRotation, geomScaleAbout, geomScene, geomTranslate, geomVisualBounds, groupCaps, groupKeyOf, groupMembers, groupResizeAnchor, groupResizeBox, groupResizeFactors, hitTest, initialModel, initialStyle, initialTextStyle, interactive, isRotatableGeom, isSelectable, mergeFlags, normalizeDeg, obbFromGeom, pageItems, pdfToContentPoint, pdfToContentRect, placeRotateKnob, pointInQuad, propsFor, quadIntersectsRect, readProp, rectFromPoints, resolveClickPlacement, rotateDraftDelta, rotateKnob, rotatedAabb, rotatedHandleCursor, scene, selectedItems, selectionAnchor, selectionBounds, selectionBoundsOnPage, selectionCenter, selectionKnob, selectionQuad, shapeRectFor, sharedProps, straightenInkStroke, styleFromProps, textBoxes, textStyleFromProps, transposedAboutCenter, unanchoredGeom, unionRect, update, uprightAnchoredRect, uprightRotation, viewable };
1636
+ //# sourceMappingURL=index.d.cts.map