@ingcreators/annot-core 0.1.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +201 -0
  3. package/README.md +56 -0
  4. package/dist/auto-capture-options.d.ts +78 -0
  5. package/dist/editor/arrow-markers.d.ts +142 -0
  6. package/dist/editor/bake-translate.d.ts +192 -0
  7. package/dist/editor/font-registry.d.ts +58 -0
  8. package/dist/editor/gradient-utils.d.ts +23 -0
  9. package/dist/editor/history-core.d.ts +48 -0
  10. package/dist/editor/icons/brand-icons.d.ts +53 -0
  11. package/dist/editor/icons/material-symbols.d.ts +105 -0
  12. package/dist/editor/icons/registry.d.ts +179 -0
  13. package/dist/editor/icons/render.d.ts +22 -0
  14. package/dist/editor/icons/sanitize.d.ts +60 -0
  15. package/dist/editor/index.d.ts +13 -0
  16. package/dist/editor/path-utils.d.ts +32 -0
  17. package/dist/editor/property-schema.d.ts +263 -0
  18. package/dist/editor/rich-text-mapper.d.ts +8 -0
  19. package/dist/editor/selection-geometry.d.ts +66 -0
  20. package/dist/editor/shape-utils.d.ts +27 -0
  21. package/dist/editor/svg-format.d.ts +68 -0
  22. package/dist/editor/svg-id-utils.d.ts +37 -0
  23. package/dist/editor/svg-to-annotation-shapes.d.ts +34 -0
  24. package/dist/editor/text-utils.d.ts +212 -0
  25. package/dist/editor/tool-lifecycle.d.ts +56 -0
  26. package/dist/editor/tool-options.d.ts +126 -0
  27. package/dist/editor/tool-panel-adapter.d.ts +105 -0
  28. package/dist/editor/tool-preset-serde.d.ts +43 -0
  29. package/dist/editor/tool-registry.d.ts +320 -0
  30. package/dist/editor/tool-style-reader.d.ts +36 -0
  31. package/dist/editor/tool-style-writer.d.ts +33 -0
  32. package/dist/editor/toolbar-icons.d.ts +84 -0
  33. package/dist/editor/transform-utils.d.ts +127 -0
  34. package/dist/editor/viewport-math.d.ts +69 -0
  35. package/dist/encode/index.d.ts +4 -0
  36. package/dist/encode/options.d.ts +79 -0
  37. package/dist/encode/png8.d.ts +10 -0
  38. package/dist/headless.d.ts +29 -0
  39. package/dist/icons/index.d.ts +15 -0
  40. package/dist/icons/types.d.ts +108 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +3164 -0
  43. package/dist/storage/errors.d.ts +59 -0
  44. package/dist/storage/index.d.ts +6 -0
  45. package/dist/storage/metadata-cache.d.ts +231 -0
  46. package/dist/storage/path.d.ts +49 -0
  47. package/dist/storage/thumbnail-cache.d.ts +110 -0
  48. package/dist/storage/thumbnail.d.ts +31 -0
  49. package/dist/storage/types.d.ts +677 -0
  50. package/dist/utils/assert.d.ts +31 -0
  51. package/dist/utils/constants.d.ts +10 -0
  52. package/dist/utils/dash-utils.d.ts +6 -0
  53. package/dist/utils/desktop-bridge.d.ts +349 -0
  54. package/dist/utils/filename.d.ts +48 -0
  55. package/dist/utils/id.d.ts +21 -0
  56. package/dist/utils/index.d.ts +5 -0
  57. package/dist/utils/types.d.ts +20 -0
  58. package/dist/xmp/xmp-browser.d.ts +39 -0
  59. package/dist/zip/zip-builder.d.ts +8 -0
  60. package/dist/zip/zip-bytes.d.ts +22 -0
  61. package/package.json +58 -0
  62. package/styles/editor.css +1912 -0
  63. package/styles/fonts.css +46 -0
  64. package/styles/property-panel.css +779 -0
  65. package/styles/toolbar.css +673 -0
@@ -0,0 +1,320 @@
1
+ import { PropertyControlId } from './property-schema.js';
2
+ import { ToolOptions } from './tool-options.js';
3
+ /** Tool-side panel sections. The Tool property panel uses the same
4
+ * four pp-section buckets as the SELECTION-side property panel, in
5
+ * the same visual order (Type → Fill → Line → Label). The renderer
6
+ * groups consecutive entries with the same section into a single
7
+ * `pp-section` card; section ORDER follows first-encounter in the
8
+ * per-tool `panelControls` array. */
9
+ export type ToolPanelSection = "Type" | "Fill" | "Line" | "Label";
10
+ /** Tool-only control ids — affordances the Tool panel renders that
11
+ * the SELECTION registry doesn't model. Distinct from
12
+ * `PropertyControlId` so the schema-driven renderer in Phase 2 can
13
+ * dispatch on whichever id family it sees.
14
+ *
15
+ * `tool.typeChips`
16
+ * Type-row chip picker driven by `TOOL_REGISTRY[toolId].variants`.
17
+ * ONE entry per tool with a variant flyout. Reads / writes
18
+ * `preset[TOOL_REGISTRY[toolId].variantField]` (the only adapter
19
+ * that genuinely needs `toolId` at read/write time — every other
20
+ * adapter ignores it).
21
+ *
22
+ * `tool.transparencyPercent`
23
+ * 0..100% inverse of `preset.strokeOpacity` (so "60% transparent"
24
+ * reads as 60, not 0.4). The SELECTION-side `strokeOpacity` def
25
+ * uses the same convention; the Tool panel always shows percent
26
+ * directly without the helper, so this id keeps its own adapter.
27
+ *
28
+ * `tool.fillTransparencyPercent`
29
+ * 0..100% inverse of `preset.fillOpacity`. Used by Highlight
30
+ * (default 0.4 ↔ 60% transparent) where the visual language is
31
+ * "transparency" — the larger the number, the see-throughier.
32
+ *
33
+ * `tool.fillOpacityPercent`
34
+ * 0..100% DIRECT of `preset.fillOpacity`. Used by Shape (default
35
+ * 1.0 ↔ 100% opaque) where the imperative renderer used the
36
+ * "Opacity" label. Distinct from `tool.fillTransparencyPercent`
37
+ * so Phase 2 can preserve byte-equivalent DOM (label string +
38
+ * number direction) per tool. Whether to unify the two surfaces
39
+ * onto a single "Transparency" idiom is a UX decision, not a
40
+ * refactor concern — out of scope for this plan.
41
+ *
42
+ * `tool.freehandDone`
43
+ * Click-action button that ends the active freehand drawing
44
+ * session. No persisted value — the adapter's read/write are
45
+ * a no-op pair (read returns null, write does nothing) so the
46
+ * shape-invariant test treats it uniformly with the rest. */
47
+ export type ToolPanelExtraControlId = "tool.typeChips" | "tool.transparencyPercent" | "tool.fillTransparencyPercent" | "tool.fillOpacityPercent" | "tool.freehandDone";
48
+ /** Frozen tuple of every `ToolPanelExtraControlId`. Drives the
49
+ * shape-invariant test that asserts every extra id has a matching
50
+ * adapter. Keep in sync with the union above when adding new ids. */
51
+ export declare const TOOL_PANEL_EXTRA_CONTROL_IDS: ReadonlyArray<ToolPanelExtraControlId>;
52
+ /** A single Tool-side panel control. The renderer (Phase 2, Tier C)
53
+ * consumes `panelControls` arrays to produce the per-tool side
54
+ * panel. The id either reuses a `PropertyControlId` (when the
55
+ * SELECTION registry can supply label / options / min / max
56
+ * metadata) or names a Tool-only affordance via
57
+ * `ToolPanelExtraControlId`.
58
+ *
59
+ * Plain data — no closures over canvas state, no DOM globals at
60
+ * module load. The optional `visibleWhen` predicate runs against
61
+ * the CURRENT preset (NOT an SVGElement — this is the Tool side,
62
+ * no element exists yet) so a control like Redact's Color row can
63
+ * hide unless `preset.redactStyle === "solid"`. */
64
+ export interface ToolPanelControlDef {
65
+ /** Section header. Renderer batches consecutive entries with the
66
+ * same section into one `pp-section`. Order in the registry
67
+ * controls visual order. */
68
+ section: ToolPanelSection;
69
+ /** Control id. Either a SELECTION-side id (`fillColor`,
70
+ * `strokeWidth`, …) where the adapter routes the mutation onto
71
+ * the matching `ToolOptions` field, or a Tool-only id
72
+ * (`tool.typeChips`, `tool.transparencyPercent`, …) the renderer
73
+ * resolves via its Tier C-local table. */
74
+ id: PropertyControlId | ToolPanelExtraControlId;
75
+ /** Optional gating predicate against the CURRENT preset. Returning
76
+ * `false` tells the renderer to skip this control for the active
77
+ * tool (e.g. "Redact Color row only when redactStyle === 'solid'").
78
+ *
79
+ * Pure — no DOM access, no `Toolbar` access. Tier B-safe. */
80
+ visibleWhen?: (preset: ToolOptions) => boolean;
81
+ }
82
+ /** A single sub-shape pickable from a tool's variant flyout. Mirrors
83
+ * the shape of `ToolVariant` in the legacy `toolbar-variants.ts`
84
+ * (which becomes a thin re-export once Phase 4 deletes the file).
85
+ * `svg` overrides `icon` when present — used for variants where
86
+ * Material Symbols glyphs don't read clearly at button scale (rect
87
+ * vs. rounded-rect, the three arrow-head variants, etc.). */
88
+ export interface ToolRegistryVariant {
89
+ /** Value written into `ToolOptions[variantField]` when this variant
90
+ * is selected. Also forms the suffix of the preset-storage key
91
+ * (`shape.rect`, `arrow.both`, …). */
92
+ value: string;
93
+ /** Builtin icon id (registry key from
94
+ * `@ingcreators/annot-core/editor/icons/registry`). Rendered via
95
+ * the toolbar's `<annot-icon>` element when no `svg` is provided. */
96
+ icon: string;
97
+ /** Human-readable label for tooltips + a11y. */
98
+ label: string;
99
+ /** Optional inline SVG markup. When present, takes precedence over
100
+ * `icon`. Should use `viewBox="0 0 24 24"`, omit explicit
101
+ * width/height (so CSS sizes per-context), and use
102
+ * `stroke="currentColor"` + `fill="none"` to match the outlined
103
+ * Material-Symbols visual weight. */
104
+ svg?: string;
105
+ }
106
+ /** Tool semantic category. Drives toolbar / right-click menu grouping
107
+ * + divider placement.
108
+ *
109
+ * - `"annotation"` (default): tool emits an on-canvas SVG annotation
110
+ * (Arrow / Shape / Highlight / Text / Draw / Counter / Redact).
111
+ * Non-destructive — the underlying bitmap is untouched.
112
+ * - `"image-op"`: tool destructively mutates the underlying
113
+ * `ImageRecord` (Crop's `applyCrop` rewrites `originalDataUrl`).
114
+ * Visually separated from annotation tools so the divider
115
+ * signals "below this line, the tool changes the image itself".
116
+ *
117
+ * Hosts may register additional categories in the future
118
+ * (e.g. `"image-filter"` for non-annotation, non-bitmap-rewriting
119
+ * tools); the toolbar treats anything other than `"annotation"` as
120
+ * "trailing groups, separated by dividers in declaration order". */
121
+ export type ToolCategory = "annotation" | "image-op";
122
+ /** Full metadata for one tool. Plain data — no closures over canvas
123
+ * state, no DOM globals at module load.
124
+ *
125
+ * Tools without a variant flyout (Crop) omit `variants` /
126
+ * `variantField` / `defaultVariant`; their preset key is just the
127
+ * bare tool id.
128
+ *
129
+ * Tools without an element-on-canvas (Crop again) omit
130
+ * `variantKeyForElement` — there's no rubber-band style to harvest. */
131
+ export interface ToolRegistryEntry {
132
+ /** Stable id, matches the registry's record key. Used as the
133
+ * `data-tool` attribute on the toolbar button + as the prefix of
134
+ * every preset-storage key for this tool. */
135
+ id: string;
136
+ /** Tooltip label on the toolbar button. */
137
+ label: string;
138
+ /** Material Symbols ligature for the toolbar button's default icon
139
+ * (overridden at runtime by the active variant's icon for tools
140
+ * with a flyout — see `Toolbar.#syncToolButtonIcon`). */
141
+ icon: string;
142
+ /** Semantic category. Drives toolbar group placement + canvas
143
+ * right-click toolbox menu divider placement. Defaults to
144
+ * `"annotation"` when omitted (matches the legacy behavior — every
145
+ * pre-category tool was treated as an annotation tool). */
146
+ category?: ToolCategory;
147
+ /** Variant catalog. Empty / absent for tools without sub-variants. */
148
+ variants?: ReadonlyArray<ToolRegistryVariant>;
149
+ /** Which `ToolOptions` field discriminates the variant. Used by
150
+ * the flyout to show the active chip and by the preset machinery
151
+ * to migrate legacy tool-id-keyed entries to the new
152
+ * `tool.variant` form. */
153
+ variantField?: keyof ToolOptions;
154
+ /** Default variant when no preset exists yet. MUST appear in
155
+ * `variants`. */
156
+ defaultVariant?: string;
157
+ /** Which `ToolOptions` keys this tool's preset reads / writes.
158
+ * Phase 2 makes this the single source of truth for the
159
+ * camelCase ↔ snake_case (de)serializer; Phase 5 makes it the
160
+ * source of truth for the rubber-band reader / writer. Today
161
+ * it's data only — no consumer reads it yet. */
162
+ presetFields: ReadonlyArray<keyof ToolOptions>;
163
+ /** Element → preset-storage key extractor. Returns `null` for
164
+ * elements this tool doesn't own (so a generic dispatch loop
165
+ * can iterate `Object.entries(TOOL_REGISTRY)` and find the
166
+ * first non-null match — replacing both `toolIdForElement` and
167
+ * `elementKeyFromElement` in a single sweep, Phase 4 / 5).
168
+ *
169
+ * Returns the FULL key (`"shape.rounded"`, `"arrow.both"`, …),
170
+ * not just the variant suffix — so the caller doesn't need to
171
+ * juggle the toolId prefix separately.
172
+ *
173
+ * Implementations may assume jsdom-friendly Element APIs
174
+ * (`tagName`, `getAttribute`, `hasAttribute`, `querySelector`).
175
+ * No `document` / `window` access. */
176
+ variantKeyForElement?: (el: SVGElement) => string | null;
177
+ /** Tool-side panel control list. Drives the schema-driven renderer
178
+ * in `tool-property-renderer-schema.ts` (Phase 2 of
179
+ * `docs/plans/tool-property-renderer-schema.md`). Order is render
180
+ * order; the renderer groups consecutive entries with the same
181
+ * `section` into one `pp-section`.
182
+ *
183
+ * Crop omits this field — its activation is a transient overlay,
184
+ * not a per-tool side panel.
185
+ *
186
+ * Today (Phase 1) it's data only — no consumer reads it yet.
187
+ * Phase 2 adds the schema-driven renderer alongside the imperative
188
+ * one; Phase 3 swaps the live callsite. */
189
+ panelControls?: ReadonlyArray<ToolPanelControlDef>;
190
+ /** Tool-specific style reader. Mutates `preset` in place with
191
+ * values harvested from `el`'s attributes / children. The
192
+ * generic universal-style reader (stroke / fill / stroke-width /
193
+ * dasharray / opacity / linecap / linejoin) runs BEFORE this
194
+ * hook; the hook only handles tool-specific branches the
195
+ * universal reader can't capture (text font + variant on a
196
+ * child `<text>`, arrow per-end state, marker bg primitive
197
+ * reads, etc.).
198
+ *
199
+ * Phase 5 of `docs/plans/toolbar-schema.md`: the imperative
200
+ * `if (toolId === "text") { … } if (toolId === "arrow") { … }`
201
+ * cascades in `Toolbar.syncPresetFromElement` collapse to a
202
+ * single `TOOL_REGISTRY[toolId]?.extractStyleFromElement?.(el,
203
+ * preset)` dispatch.
204
+ *
205
+ * Tier B — implementations live in `tool-registry.ts` itself
206
+ * and may only use jsdom-friendly Element APIs. */
207
+ extractStyleFromElement?: (el: SVGElement, preset: ToolOptions) => void;
208
+ /** Tool-specific style writer. Inverse of
209
+ * `extractStyleFromElement`: writes the preset's style fields
210
+ * onto `el` (or its tool-specific child elements — marker's
211
+ * bg primitive, textbox's `<text>`, etc.).
212
+ *
213
+ * Deliberately does NOT touch fields that define the element's
214
+ * type / variant (shapeType, arrowHead, textVariant) — those
215
+ * were already established by the variant-change path that
216
+ * invoked the writer.
217
+ *
218
+ * Phase 3 of `docs/plans/toolbar-apply-style-to-element.md`:
219
+ * the imperative element-tag cascade in
220
+ * `applyPresetStyleAttrs` (in
221
+ * `packages/web/src/editor/toolbar-preset-helpers.ts`)
222
+ * collapses to a single
223
+ * `TOOL_REGISTRY[toolId]?.applyStyleToElement?.(el, preset)`
224
+ * dispatch — symmetric with how Phase 5 of
225
+ * `_done/toolbar-schema.md` collapsed the read side.
226
+ *
227
+ * Tier B — implementations live in `tool-registry.ts` itself
228
+ * and may only use jsdom-friendly Element APIs. The
229
+ * `refreshArrowPath` regen for arrow groups is the one
230
+ * exception: it lives in `core/editor/arrow-markers.ts`
231
+ * (Tier B) so the registry can call it without crossing
232
+ * package boundaries. */
233
+ applyStyleToElement?: (el: SVGElement, preset: ToolOptions) => void;
234
+ /** How the tool's variant flyout / variant badge / toolbox-menu
235
+ * submenu present each variant chip:
236
+ * - `"variant"` (default): icon glyph (Material Symbols
237
+ * ligature) or inline SVG, matching the chip's
238
+ * `icon` / `svg` fields.
239
+ * - `"color"`: filled color swatch driven by the chip's
240
+ * `chipColorForVariant` accessor (or, for the legacy
241
+ * Highlight catalog, the variant's `value` itself when
242
+ * it's a hex string).
243
+ *
244
+ * Default is `"variant"` for tools without sub-variants
245
+ * (Crop) and for tools whose variants are SHAPE-discriminated
246
+ * (shape / arrow / text / freehand / marker / redact).
247
+ * Highlight is the lone `"color"` today.
248
+ *
249
+ * Phase 1 of `docs/plans/toolbar-highlight-flyout-kind.md`:
250
+ * data only — no consumer reads it yet. Phases 2–4 lift the
251
+ * three `if (toolId === "highlight")` callsites in
252
+ * `toolbar.ts` + `toolbar-canvas-menu.ts` onto this
253
+ * discriminator. */
254
+ flyoutKind?: "variant" | "color";
255
+ /** Chip color resolver — only meaningful when
256
+ * `flyoutKind === "color"`. Returns the swatch fill color for
257
+ * a given variant value. When undefined, callers fall back to
258
+ * the variant value itself (the identity case — works for
259
+ * Highlight today because the variant value IS the hex
260
+ * string). Future tools whose variant value is an opaque id
261
+ * (e.g. `"red"` instead of `"#ff0000"`) override to map
262
+ * through their palette.
263
+ *
264
+ * Tier B — pure `(string) => string`; no DOM access. */
265
+ chipColorForVariant?: (variantValue: string) => string;
266
+ /** Tooltip-label resolver — overrides the default
267
+ * `${label} (${variantLabel})` formatter for tools whose
268
+ * variant LABEL doesn't render usefully. Receives both the
269
+ * raw variant VALUE (e.g. a hex string) and the variant's
270
+ * `label` field (which may not match the user-facing name).
271
+ *
272
+ * Highlight uses this to map ad-hoc hexes (saved in legacy
273
+ * documents outside the 6-color palette) to an empty string
274
+ * so the tooltip falls back to the bare tool name without
275
+ * parens, instead of showing "Highlight (#123456)". For
276
+ * palette colors it returns the palette label ("Yellow",
277
+ * "Green", …) which matches the Selection-side
278
+ * "Selected Highlight (Yellow)" formatter.
279
+ *
280
+ * When undefined, the default `${variantLabel}` format
281
+ * applies. Tier B — pure `(string, string) => string`; no
282
+ * DOM access. */
283
+ tooltipLabelForVariant?: (variantValue: string, variantLabel: string) => string;
284
+ /** After the user picks a variant in the FLYOUT (color or
285
+ * icon), fix up cross-field invariants on the preset that the
286
+ * variantField alone can't capture. The variantField write is
287
+ * applied first; this hook then mutates other fields the tool
288
+ * needs to render correctly.
289
+ *
290
+ * Today's only consumer: Highlight, which sets
291
+ * `shapeType = "highlight"` so the underlying ShapeTool's
292
+ * rendering dispatch sees the flag (the Highlight tool is
293
+ * internally a ShapeTool with shapeType forced on).
294
+ *
295
+ * Phase 5 of `docs/plans/toolbar-highlight-flyout-kind.md`:
296
+ * removes the last `if (toolId === "highlight")` literal from
297
+ * `Toolbar.#showColorFlyout`. Tier B — pure
298
+ * `(preset, variantValue) => void`; no DOM access. */
299
+ ensurePresetForVariantChange?: (preset: ToolOptions, variantValue: string) => void;
300
+ }
301
+ /** After a variant switch, fix up the side-fields on `preset` so the
302
+ * new variant's invariants hold. Today only `arrow` needs this:
303
+ * - `none`: both ends must be "none".
304
+ * - `end`: begin must be "none", end must be non-"none" (default tri).
305
+ * - `both`: both ends must be non-"none" (default tri).
306
+ *
307
+ * Mutates `preset` in place; safe to call for tools without a
308
+ * relevant invariant (it's a no-op).
309
+ *
310
+ * Tier B — pure (no DOM, no `Toolbar` access). Relocated from
311
+ * `packages/web/src/editor/toolbar-preset-helpers.ts` in Phase 5
312
+ * of `docs/plans/toolbar-schema.md` so the registry's
313
+ * `extractStyleFromElement` callbacks can call it without crossing
314
+ * the Tier B → Tier C boundary. */
315
+ export declare function normalizeVariantSideFields(toolId: string, newVariant: string, preset: ToolOptions): void;
316
+ export declare const TOOL_REGISTRY: Readonly<Record<string, ToolRegistryEntry>>;
317
+ /** All tool ids the toolbar exposes. Frozen tuple form for tests
318
+ * that assert the registry covers exactly this set. */
319
+ export declare const TOOL_REGISTRY_IDS: readonly ["arrow", "shape", "highlight", "text", "freehand", "marker", "redact", "crop"];
320
+ export type ToolRegistryId = (typeof TOOL_REGISTRY_IDS)[number];
@@ -0,0 +1,36 @@
1
+ import { ToolOptions } from './tool-options.js';
2
+ /** Pick the element whose attributes should be read for the
3
+ * universal-style fields. For most elements that's `el` itself;
4
+ * for freehand session groups (`<g data-type="freehand">`) it's
5
+ * the LAST `<path>` child — the user's most recent stroke,
6
+ * which is the natural rubber-band source. Returns `el` unchanged
7
+ * when the freehand group has no path children yet (defensive). */
8
+ export declare function resolveStyleReadSource(el: SVGElement): SVGElement;
9
+ /**
10
+ * Read the universal style attributes off `el` (or its
11
+ * freehand-group inner stroke) and write them onto `preset`. Field
12
+ * coverage:
13
+ *
14
+ * - `stroke` → `strokeColor`
15
+ * - `fill` → `fillColor`
16
+ * - `stroke-width` → `strokeWidth` (only when finite + > 0)
17
+ * - `data-dash-key` ?? `stroke-dasharray` → `strokeDasharray`
18
+ * (`data-dash-key` wins when present, matching the priority the
19
+ * legacy readers settled on)
20
+ * - `fill-opacity` → `fillOpacity` (only when finite)
21
+ * - `opacity` ?? `stroke-opacity` → `strokeOpacity` (only when
22
+ * finite). Lines / arrow `<g>`s carry transparency via
23
+ * `opacity` so SVG-marker arrowheads fade with the stem;
24
+ * other shapes use `stroke-opacity`. Reading both — preferring
25
+ * `opacity` — captures whichever is set.
26
+ * - `stroke-linecap` → `strokeLinecap` (whitelist butt/round/square)
27
+ * - `stroke-linejoin` → `strokeLinejoin` (whitelist
28
+ * miter/round/bevel)
29
+ *
30
+ * Tool-specific extras (text font, marker bg primitive, arrow
31
+ * per-end state, highlight's `fill → highlightColor` routing) are
32
+ * handled by the registry's `extractStyleFromElement` callbacks
33
+ * and intentionally NOT covered here — keep this helper toolId-
34
+ * agnostic.
35
+ */
36
+ export declare function readUniversalStyleAttrs(el: SVGElement, preset: ToolOptions): void;
@@ -0,0 +1,33 @@
1
+ import { ToolOptions } from './tool-options.js';
2
+ /**
3
+ * Write the universal style attributes from `preset` onto `el`.
4
+ * Field coverage mirrors `readUniversalStyleAttrs`:
5
+ *
6
+ * - `strokeColor` → `stroke`
7
+ * - `strokeWidth` → `stroke-width`
8
+ * - `strokeDasharray` → `stroke-dasharray` (numeric, via
9
+ * {@link computeDasharray}) AND
10
+ * `data-dash-key` (canonical key) so
11
+ * a subsequent read round-trips
12
+ * - `fillColor` → `fill`
13
+ * - `fillOpacity` → `fill-opacity`
14
+ * - `strokeLinecap` → `stroke-linecap`
15
+ * - `strokeLinejoin` → `stroke-linejoin`
16
+ * - `strokeOpacity` → `opacity` for `<line>` / arrow `<g>`
17
+ * (so SVG-marker arrowheads fade with
18
+ * the stem); `stroke-opacity` for
19
+ * everything else. Mirrors the read-
20
+ * side rule in `readUniversalStyleAttrs`.
21
+ *
22
+ * Tool-specific extras (text font + variant on a child `<text>`,
23
+ * marker's bg primitive walk, arrow's `refreshArrowPath` regen,
24
+ * highlight's `fillColor → fill` routing) are handled by the
25
+ * registry's `applyStyleToElement` callbacks and intentionally
26
+ * NOT covered here — keep this helper toolId-agnostic.
27
+ *
28
+ * Each branch is gated on the matching field being present so a
29
+ * partially-populated preset doesn't clobber attributes the user
30
+ * hasn't set. Mirrors the conditionals in the legacy
31
+ * `applyPresetStyleAttrs` generic path.
32
+ */
33
+ export declare function writeUniversalStyleAttrs(el: SVGElement, preset: ToolOptions): void;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Inline-SVG icon catalogues + highlight-color presets shared
3
+ * between the toolbar (in `@ingcreators/annot-web`) and core's
4
+ * `PropertyPanel`. Extracted out of `toolbar.ts` as part of
5
+ * Phase 5a of `docs/plans/_done/lit-migration.md` so the Toolbar
6
+ * class could relocate to web without dragging core's
7
+ * PropertyPanel into the cross-package import.
8
+ *
9
+ * These are pure data: ASCII strings + arrays + a tiny string
10
+ * lookup helper. No DOM access, no listeners, no globals.
11
+ */
12
+ /** Inline SVG icons for the Shape tool's variants — rendered
13
+ * as outline strokes so the chip glyphs read as "what shape
14
+ * type" without implying any fill color.
15
+ *
16
+ * Why hand-rolled: the Material Symbols `rectangle` /
17
+ * `square` ligatures are visually IDENTICAL at 36px button
18
+ * scale. `crop_square` (rounded outline) and `square` (sharp
19
+ * outline) both render as a square outline in 36px buttons —
20
+ * the difference (slight vs. no corner radius) is
21
+ * imperceptible. Authoring inline SVG with EXAGGERATED corner
22
+ * radius (rx/ry ≈ 1/3 of the side) makes the distinction
23
+ * clear, matching what PowerPoint / Google Slides / Keynote /
24
+ * Miro all do. */
25
+ export declare const SHAPE_ICON_SVG: {
26
+ /** Sharp-cornered rectangle outline. Stroke weight tuned to
27
+ * match the optical weight of adjacent Material-Symbols
28
+ * outlined icons. */
29
+ readonly rect: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"miter\" aria-hidden=\"true\"><rect x=\"4\" y=\"4\" width=\"16\" height=\"16\"/></svg>";
30
+ /** Rounded rectangle with rx=5 on a 16-wide square (≈ 31% —
31
+ * above the 20% threshold where humans reliably perceive
32
+ * corner rounding at icon scale). */
33
+ readonly rounded: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"5\"/></svg>";
34
+ /** Ellipse / circle outline. Drawn as an <ellipse> with rx=ry
35
+ * so it's perfectly circular; using <ellipse> rather than
36
+ * <circle> keeps a single element type if future variants
37
+ * (e.g. wide ellipse) need different rx/ry. Stroke-width
38
+ * matches rect / rounded so the three chips look optically
39
+ * equal. */
40
+ readonly ellipse: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><ellipse cx=\"12\" cy=\"12\" rx=\"8\" ry=\"8\"/></svg>";
41
+ };
42
+ /** Inline SVG icons for the Arrow tool's head variants.
43
+ * Material Symbols has no icon that accurately depicts a
44
+ * single line with arrowheads on both ends (`sync_alt` shows
45
+ * TWO parallel lines in opposite directions, which is not the
46
+ * same thing). Hand-rolling the three glyphs as a unified set
47
+ * — same line length, same stroke weight, only the arrowheads
48
+ * change — makes the "this is the same line with different
49
+ * ends" narrative visually obvious. */
50
+ export declare const ARROW_ICON_SVG: {
51
+ /** Plain horizontal line, no arrowheads. */
52
+ readonly none: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 12 H20\"/></svg>";
53
+ /** Single arrowhead at the right end ("end" variant). */
54
+ readonly end: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 12 H20\"/><path d=\"M16 8 L20 12 L16 16\"/></svg>";
55
+ /** Arrowheads at BOTH ends ("both" variant). */
56
+ readonly both: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 12 H20\"/><path d=\"M16 8 L20 12 L16 16\"/><path d=\"M8 8 L4 12 L8 16\"/></svg>";
57
+ };
58
+ /** Inline SVG icons for the Counter (marker) shape variants —
59
+ * each glyph shows the container shape filled with
60
+ * `currentColor` and a "1" cut out to represent the numeric
61
+ * label. The cutout uses fill-rule="evenodd" so the "1" reads
62
+ * as the panel background showing through, automatically
63
+ * adapting to light / dark themes without hard-coded colors. */
64
+ export declare const COUNTER_ICON_SVG: {
65
+ readonly circle: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><ellipse cx=\"12\" cy=\"12\" rx=\"8\" ry=\"8\"/><text x=\"12\" y=\"17\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"800\" font-family=\"system-ui, sans-serif\" fill=\"currentColor\" stroke=\"none\">1</text></svg>";
66
+ readonly rect: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"miter\" aria-hidden=\"true\"><rect x=\"4\" y=\"4\" width=\"16\" height=\"16\"/><text x=\"12\" y=\"17\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"800\" font-family=\"system-ui, sans-serif\" fill=\"currentColor\" stroke=\"none\">1</text></svg>";
67
+ readonly rounded: "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"5\"/><text x=\"12\" y=\"17\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"800\" font-family=\"system-ui, sans-serif\" fill=\"currentColor\" stroke=\"none\">1</text></svg>";
68
+ };
69
+ /** Preset highlight colors — matches common PDF / PowerPoint
70
+ * highlighter pen sets. The user can pick any of these from
71
+ * the Highlight tool's color-swatch flyout; the chosen color
72
+ * is persisted via the preset system so the next click on the
73
+ * Highlight button uses it again. */
74
+ export declare const HIGHLIGHT_COLORS: ReadonlyArray<{
75
+ value: string;
76
+ label: string;
77
+ }>;
78
+ /** Map a highlight fill hex (case-insensitive) to its palette
79
+ * label. Used by the right-panel selection title ("Selected
80
+ * Highlight (Yellow)") and by the Type-picker swatch tooltips.
81
+ * Falls back to the hex string itself for colors outside the
82
+ * preset palette (e.g. legacy documents with custom
83
+ * highlightColor values). */
84
+ export declare function highlightColorLabel(fill: string | null | undefined): string;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * transform-utils — composite transform management for annotation
3
+ * elements (rotation + flip + translate).
4
+ *
5
+ * State is persisted as `data-*` attributes on the element so the SVG
6
+ * is the single source of truth (no parallel JS map). The visual
7
+ * `transform` attribute is recomputed from those attrs whenever any
8
+ * piece of state changes.
9
+ *
10
+ * Stored state:
11
+ * data-rot ← rotation in degrees (CW positive). Default 0.
12
+ * data-flip-h ← "1" if horizontally flipped. Default unset/0.
13
+ * data-flip-v ← "1" if vertically flipped. Default unset/0.
14
+ * data-tx ← translate x (only used for path/g — for rect/
15
+ * ellipse/etc. position lives in their own attrs).
16
+ * data-ty ← translate y (path/g only).
17
+ *
18
+ * Rotation/flip pivot is the element's LOCAL bbox center (computed via
19
+ * getBBox() which ignores the element's own transform). Re-computed on
20
+ * every update so the pivot follows resizes naturally.
21
+ *
22
+ * Composite math (right-to-left in SVG):
23
+ * M = T(tx, ty) * T(cx, cy) * R(rot) * S(sx, sy) * T(-cx, -cy)
24
+ *
25
+ * For elements with no rotation and no flip, the function emits a
26
+ * plain `translate(tx, ty)` (or no transform at all when tx=ty=0) so
27
+ * existing serialization stays byte-identical for unrotated content.
28
+ */
29
+ declare function isLineLike(el: Element): boolean;
30
+ /** Return a line/arrow's endpoint + optional control-point coords in
31
+ * world space, applying any lingering `data-rot` / `data-flip-*` /
32
+ * `data-tx,ty` WITHOUT mutating the element. Used by read-only
33
+ * consumers (e.g. PPTX export) and by the mutating `bakeLineTransform`
34
+ * below. The control point is included so curved arrows survive the
35
+ * transform intact. */
36
+ export declare function getEffectiveLineEndpoints(el: SVGElement): {
37
+ x1: number;
38
+ y1: number;
39
+ x2: number;
40
+ y2: number;
41
+ cx: number | null;
42
+ cy: number | null;
43
+ };
44
+ /** Collapse any rotation/flip/translate state on a line/arrow into
45
+ * its endpoints (and control point, for curved arrows), clearing the
46
+ * `transform` attribute + data-* state. Idempotent. Call before any
47
+ * gesture on a line/arrow to normalize legacy saved content that
48
+ * still carries a transform attribute. */
49
+ export declare function bakeLineTransform(el: SVGElement): void;
50
+ /** Translate a line/arrow's endpoints (and arrow control point, if
51
+ * curved) by `(dx, dy)` in world space. Bakes any pending
52
+ * rotation/flip/translate state into endpoints first via
53
+ * `bakeLineTransform` so the result reflects the visual position
54
+ * the user sees on screen, not a stale endpoint pair the matrix
55
+ * was offsetting at render time.
56
+ *
57
+ * Used by the destructive-crop pipeline (`EditorShell.applyCrop`
58
+ * via `bakeAnnotationsTranslate`) to shift line/arrow annotations
59
+ * into the cropped image's new origin. The non-line dispatcher
60
+ * in `bake-translate.ts:bakeTranslate` covers everything else.
61
+ *
62
+ * No-op for `(0, 0)` and for non-line inputs (the caller is
63
+ * expected to dispatch line vs. non-line, but the guard keeps
64
+ * the helper safe to call defensively from a generic walker). */
65
+ export declare function bakeLineTranslate(el: SVGElement, dx: number, dy: number): void;
66
+ /** Rotate a line's endpoints (and control point, if curved) by `deg`
67
+ * around the midpoint of the two endpoints. */
68
+ export declare function rotateLineEndpointsBy(el: SVGElement, deg: number): void;
69
+ /** Mirror a line's endpoints (and control point) across its midpoint
70
+ * on the given axis. */
71
+ export declare function flipLineEndpoints(el: SVGElement, axis: "h" | "v"): void;
72
+ export { isLineLike };
73
+ export interface TransformState {
74
+ tx: number;
75
+ ty: number;
76
+ rotation: number;
77
+ flipH: boolean;
78
+ flipV: boolean;
79
+ }
80
+ /** Elements whose position lives in their own geometry attrs (not in
81
+ * a translate transform). For these, `tx`/`ty` are always 0; movement
82
+ * shifts those geometry attrs directly and the transform only carries
83
+ * rotation/flip. */
84
+ export declare function usesGeometryPosition(el: Element): boolean;
85
+ /** Read the current transform state from `data-tx` / `data-ty` /
86
+ * `data-rot` / `data-flip-{h,v}`. Geometry-positioned elements
87
+ * (`<rect>` / `<ellipse>` / etc.) use their geometry attributes for
88
+ * position so `tx` / `ty` stay zero. */
89
+ export declare function readTransformState(el: SVGElement): TransformState;
90
+ /** Recompute the element's `transform` attribute from its current
91
+ * data-* state and local geometry. Idempotent — call after any state
92
+ * change. */
93
+ export declare function applyTransformState(el: SVGElement, state?: TransformState): void;
94
+ /** Persist a (possibly partial) state update and re-apply the
95
+ * composite transform. Callout tails get rebuilt automatically since
96
+ * rotation/flip pivots can change as the bbox changes. */
97
+ export declare function writeTransformState(el: SVGElement, patch: Partial<TransformState>): void;
98
+ /** Apply a translation delta. For geometry-positioned elements, the
99
+ * caller updates x/y/cx/cy/etc. directly and then calls this with
100
+ * dx=dy=0 — the helper still recomputes the transform so the rotation
101
+ * pivot tracks the new bbox center. */
102
+ export declare function nudgeTranslate(el: SVGElement, dx: number, dy: number): void;
103
+ /** Toggle a flip axis. For line/arrow elements the flip is baked
104
+ * into endpoint coordinates (no transform attribute); for everything
105
+ * else the flip is persisted as a data-flip-h/v attribute.
106
+ *
107
+ * The flip acts in SCREEN space: a Flip V on a rect that's currently
108
+ * rotated 30° mirrors the visibly-rotated shape across a horizontal
109
+ * screen axis through its visible center — it does NOT mirror the
110
+ * rect in its pre-rotation local frame (which would visually look
111
+ * like the shape just jumped to a new rotation, not like a flip).
112
+ *
113
+ * Composing a screen-space reflection S_screen after the existing
114
+ * transform T * R * S * T⁻¹ reduces via the identity S * R(θ) =
115
+ * R(−θ) * S to the same transform shape with: rotation negated AND
116
+ * the target flip-axis toggled. So the fix is one sign flip on
117
+ * rotation + one boolean toggle. */
118
+ export declare function toggleFlip(el: SVGElement, axis: "h" | "v"): void;
119
+ /** Set rotation (degrees, CW positive). Normalized to [-180, 180).
120
+ * For line/arrow elements rotation is baked into endpoints: the
121
+ * argument is interpreted as the target absolute rotation relative
122
+ * to the element's current orientation (i.e. the caller sees a
123
+ * stateless rotation — always rotates FROM the current endpoints).
124
+ * Because baked lines always report rotation=0 via readTransformState,
125
+ * the typical caller pattern `setRotation(el, readRotation() + delta)`
126
+ * still works and applies `delta` degrees. */
127
+ export declare function setRotation(el: SVGElement, deg: number): void;