@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,212 @@
1
+ import { TextRun } from '../utils/desktop-bridge.js';
2
+ import { TextVariant } from './tool-options.js';
3
+ export declare function stickyBgFor(color: string): string;
4
+ /** In-place text-shape color update.
5
+ *
6
+ * Writes the color to:
7
+ * - `data-color` on the wrapper (the cache attr that survives
8
+ * save / paste / Office round-trips when the inner `<text>`
9
+ * gets re-rendered).
10
+ * - `fill` on the inner `<text>` child (what the SVG renderer
11
+ * paints).
12
+ * - For sticky / callout variants: `fill` on the bg `<rect>`
13
+ * and the tail `<path>` derived from `stickyBgFor(color)`,
14
+ * so the body tint follows the text color the way it would
15
+ * if the wrapper were freshly built.
16
+ *
17
+ * Text-on-shape wrappers (see `isTextOnShape`) carry the user's
18
+ * drawn fill on the geometry primitive; the bg fill stays
19
+ * untouched here so a deliberate user-set color isn't
20
+ * overwritten by the text-color change.
21
+ *
22
+ * Tier B — pure jsdom-friendly element manipulation, no live-
23
+ * canvas dependency. Used by both PropertyPanel's textColor
24
+ * effect and TextTool's commit path so a sticky's body tint
25
+ * stays in lockstep with its text color across both surfaces. */
26
+ export declare function applyTextShapeColor(g: SVGElement, color: string): void;
27
+ /** Plain-text view of a run array — joins runs in order with `\n`
28
+ * inserted at every `line_break_after`. Useful when an editor
29
+ * wants the unstyled body (e.g. for the contentEditable seed). */
30
+ export declare function runsToPlainText(runs: readonly TextRun[]): string;
31
+ /** Build a uniformly-styled run array from a plain-text string,
32
+ * one run per line. Convenience for callers that don't yet emit
33
+ * styled runs (Phase 1's TextTool, the contentEditable commit
34
+ * before Phase 2's rich-text mapper, …). */
35
+ export declare function plainTextToRuns(text: string): TextRun[];
36
+ /** Horizontal text alignment inside the shape box. Maps onto
37
+ * SVG's `text-anchor` (`start` / `middle` / `end`). */
38
+ export type TextAnchor = "start" | "middle" | "end";
39
+ /** Vertical text alignment inside the shape box. SVG has no
40
+ * built-in vertical-anchor attribute on `<text>`, so the
41
+ * layout pass computes the y-origin from total run height +
42
+ * the vanchor value (`top` / `middle` / `bottom`). */
43
+ export type TextVerticalAnchor = "top" | "middle" | "bottom";
44
+ /** Auto-fit policy for a text-bearing shape (matches PowerPoint's
45
+ * three radio options under Format Shape → Text Box):
46
+ *
47
+ * "none" — text is clipped to the box; the user resizes
48
+ * the box to make room. Default.
49
+ * "shrink" — text shrinks (scales font-size down) when it
50
+ * overflows so it always fits. Currently records
51
+ * intent only — the layout pass that does the
52
+ * actual scaling is a follow-up.
53
+ * "resize" — the box grows (height) so the text always fits
54
+ * without clipping. Width stays fixed.
55
+ *
56
+ * Stored on the wrapper as `data-text-autofit`. */
57
+ export type TextAutofit = "none" | "shrink" | "resize";
58
+ /** Per-side text-box padding in user-space units. PowerPoint's
59
+ * "Text Box → Margins" surface; defaults map to the legacy
60
+ * hard-coded inset (10 px on plain & callout, 2 px on plain).
61
+ * Stored on the wrapper as `data-text-margin-{l,r,t,b}` so the
62
+ * per-side margins survive variant changes / re-edits. */
63
+ export interface TextMargins {
64
+ left: number;
65
+ right: number;
66
+ top: number;
67
+ bottom: number;
68
+ }
69
+ export interface TextShapeSpec {
70
+ x: number;
71
+ y: number;
72
+ w: number;
73
+ h: number;
74
+ variant: TextVariant;
75
+ /** Per-run text content + per-character formatting. Use
76
+ * `plainTextToRuns(s)` to bridge from plain text. */
77
+ runs: TextRun[];
78
+ /** Default font size (px) for runs without a per-run override. */
79
+ fontSize: number;
80
+ /** Default font family for runs without a per-run override. */
81
+ fontFamily: string;
82
+ /** Default text color for runs without a per-run override. */
83
+ color: string;
84
+ /** Horizontal alignment inside the shape box. Defaults to
85
+ * `start` for the auto-bg variants (plain / sticky / callout,
86
+ * matching the pre-Phase-3 layout) and to `middle` for
87
+ * text-on-shape kinds (PowerPoint default). */
88
+ textAnchor?: TextAnchor;
89
+ /** Vertical alignment inside the shape box. Defaults to
90
+ * `top` for the auto-bg variants, `middle` for text-on-shape. */
91
+ textVerticalAnchor?: TextVerticalAnchor;
92
+ /** Callout tail tip in canvas coordinates. If undefined and the
93
+ * variant is "callout", a default position (below-left of the box)
94
+ * is picked. */
95
+ tailX?: number;
96
+ tailY?: number;
97
+ }
98
+ /** Returns true when the wrapper is a text-on-shape variant —
99
+ * i.e. the wrapper was promoted from a Shape-tool primitive
100
+ * (`rect` / `rounded` / `ellipse`) via `wrapBareRectForText`,
101
+ * so the geometry primitive carries the user's drawn fill /
102
+ * stroke. The other text-bearing variants (`plain` / `sticky` /
103
+ * `callout`) carry an auto-generated bg whose tint follows the
104
+ * text color via `stickyBgFor`.
105
+ *
106
+ * Used by:
107
+ * - `applyTextShapeColor` (above) to skip the bg-fill refresh
108
+ * for user-drawn geometry.
109
+ * - `TextTool` to decide whether to hide the wrapper or just
110
+ * its `<text>` child during edit, and whether the editor
111
+ * overlay should paint its own yellow background or stay
112
+ * transparent.
113
+ *
114
+ * Returns false for non-text-shape elements (no `data-type`,
115
+ * raw `<rect>`, etc.) so callers can use it as a discriminator
116
+ * without an outer `isTextShapeElement` guard. */
117
+ export declare function isTextOnShape(el: Element): boolean;
118
+ /** Returns true when the element is a unified text-bearing shape
119
+ * (`<g data-type="shape" data-shape-kind="...">`). */
120
+ export declare function isTextShapeElement(el: Element): boolean;
121
+ export declare function detectTextVariant(g: SVGElement): TextVariant;
122
+ /** Read the spec off an existing text-bearing shape. Used when
123
+ * converting variant or re-rendering after an edit. */
124
+ export declare function readTextShapeSpec(g: SVGElement): TextShapeSpec;
125
+ /**
126
+ * Construct a fresh text-bearing shape group element that matches
127
+ * the spec. Does NOT insert it into the DOM; caller is responsible.
128
+ */
129
+ export declare function createTextShape(spec: TextShapeSpec): SVGGElement;
130
+ /**
131
+ * Translate a text-bearing shape's children (bg `<rect>`, optional
132
+ * callout tail `<path>` + `data-tail-*` attrs, optional clipPath
133
+ * `<rect>`, every positioned `<tspan>` inside the inner `<text>`)
134
+ * by (dx, dy) in world space.
135
+ *
136
+ * Phase 2 of `docs/plans/move-bakes-coordinates.md`. The text-shape
137
+ * skeleton stores positions in child geometry attrs only; the outer
138
+ * `<g>` carries no x/y of its own. Shifting all those attrs in place
139
+ * leaves the visual unchanged when the OLD wrapper transform is
140
+ * removed and the children are re-read at their new world coords —
141
+ * which is exactly what the move-baker wants for the move-only
142
+ * case (no rotation / no flip).
143
+ *
144
+ * No-op for non-text-shape inputs. The caller is expected to have
145
+ * confirmed `g.getAttribute("data-type") === "shape"` before
146
+ * dispatching here.
147
+ *
148
+ * Tier B — pure Element manipulation, jsdom-friendly. The path
149
+ * shifter routes via `translatePathD` (Tier A), which keeps this
150
+ * helper independent of any browser-only path APIs.
151
+ */
152
+ export declare function bakeTextShapeTranslate(g: SVGElement, dx: number, dy: number): void;
153
+ /**
154
+ * Rebuild the callout tail <path> off the current bg <rect> bounds and
155
+ * the stored data-tail-x / data-tail-y. Call after any change that
156
+ * affects either input — resize (bg rect changed) or tail-tip drag
157
+ * (data-tail-* changed) — to keep the visual consistent.
158
+ *
159
+ * No-op for non-callout shapes (or callouts missing the tail path).
160
+ */
161
+ export declare function rebuildCalloutTail(g: SVGElement): void;
162
+ /**
163
+ * Update a callout's tail-tip position. Writes the new coords to the
164
+ * data-tail-* attributes and rebuilds the tail <path>. Coords are in
165
+ * the textbox's LOCAL space — the caller is responsible for subtracting
166
+ * any group transform (e.g. translate from a previous drag).
167
+ */
168
+ export declare function setCalloutTail(g: SVGElement, localTailX: number, localTailY: number): void;
169
+ /** Wrap a bare `<rect>` (drawn by ShapeTool — sharp, rounded, or
170
+ * highlight) into the unified `<g data-type="shape">` skeleton so
171
+ * the user can add text to it. The original geometry element
172
+ * becomes the first child of the wrapper unchanged; we layer a
173
+ * `<clipPath>` + an initially empty `<text>` on top so the
174
+ * TextTool's edit flow reads / writes through the same skeleton
175
+ * it uses for plain / sticky / callout textboxes.
176
+ *
177
+ * Text-on-shape entry path — landed in Phase 3 of
178
+ * `docs/plans/_done/rich-text-and-shape-text.md`. The wrapper
179
+ * REPLACES the original element in its parent; the caller
180
+ * should reassign any selection / undo state to the returned
181
+ * `<g>`. See `isTextOnShape` for the runtime predicate that
182
+ * matches the resulting wrapper kinds.
183
+ *
184
+ * The matching {@link unwrapBareTextShape} reverses this
185
+ * transformation when the user cancels a freshly-opened text
186
+ * edit without typing. */
187
+ export declare function wrapBareRectForText(rect: SVGRectElement): SVGGElement;
188
+ /** Replace the `<text>` content of an existing text-bearing shape
189
+ * in-place. Used by every TextTool re-edit commit and by the
190
+ * PropertyPanel's text effects (color / variant / autofit /
191
+ * margins). The wrapper's other children (geometry, clipPath,
192
+ * callout tail) are left untouched.
193
+ *
194
+ * Tspan layout follows the same anchor / line-height rules as
195
+ * `createTextShape`. Layout reads its origin from the FIRST
196
+ * `<rect>` direct child (the geometry primitive — the user's
197
+ * drawn rect for text-on-shape, or the auto-generated bg for
198
+ * the plain / sticky / callout variants). */
199
+ export declare function replaceRunsInPlace(g: SVGElement, runs: readonly TextRun[]): void;
200
+ /** Reverse a {@link wrapBareRectForText} promotion when the user
201
+ * cancels a freshly-opened text edit without typing anything.
202
+ * Returns the original geometry element after replacing the
203
+ * wrapper in its parent. Idempotent on a non-wrapper element. */
204
+ export declare function unwrapBareTextShape(g: SVGElement): SVGElement;
205
+ /**
206
+ * Convert an existing text-bearing shape to a different variant.
207
+ * Preserves position, size, runs, and all metadata. Replaces the
208
+ * old element in the DOM and returns the new element (caller must
209
+ * update SelectionManager refs via the PropertyPanel's
210
+ * onTargetReplaced callback).
211
+ */
212
+ export declare function convertTextVariant(oldG: SVGElement, newVariant: TextVariant): SVGElement;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Minimal contract a host must satisfy for a `ToolBase` subclass to
3
+ * draw onto its canvas.
4
+ *
5
+ * Three operations cover every tool in the editor:
6
+ *
7
+ * - {@link attachDraft} — mount an in-flight element onto the
8
+ * canvas without saving history. Used while the user is still
9
+ * dragging; the tool keeps a ref so it can mutate attributes
10
+ * each pointer-move and either {@link saveHistory} (commit) or
11
+ * `el.remove()` (discard) at pointer-up.
12
+ *
13
+ * - {@link addAnnotation} — atomic "append + save" used by
14
+ * click-to-create tools (counter, single-click placements) where
15
+ * the element is fully formed at attach time.
16
+ *
17
+ * - {@link saveHistory} — push a history snapshot without changing
18
+ * children. Used when the tool committed an in-flight element
19
+ * via {@link attachDraft} earlier and the gesture is now
20
+ * finished, or when an existing element was mutated in place.
21
+ */
22
+ export interface ToolDOMSurface {
23
+ attachDraft(el: SVGElement): void;
24
+ addAnnotation(el: SVGElement): void;
25
+ saveHistory(): void;
26
+ }
27
+ /**
28
+ * Test-only surface that records every call so tests can drive a
29
+ * tool against an inert sink and assert the resulting sequence of
30
+ * DOM mutations + history saves.
31
+ *
32
+ * `attachDraft` and `addAnnotation` BOTH append the element to the
33
+ * supplied `host` parent so the tool's subsequent attribute
34
+ * mutations on the element survive across pointer-move callbacks.
35
+ * `addAnnotation` additionally bumps `saveCount`. The mock therefore
36
+ * lets a tool's full pointer-event lifecycle execute without ever
37
+ * touching a real `CanvasManager`.
38
+ */
39
+ export interface MockToolSurface extends ToolDOMSurface {
40
+ /** All elements ever attached as drafts (not yet committed). */
41
+ readonly drafts: readonly SVGElement[];
42
+ /** All elements ever passed through `addAnnotation` — the
43
+ * click-to-create / atomic case. */
44
+ readonly committed: readonly SVGElement[];
45
+ /** Total count of `saveHistory` invocations, including those
46
+ * implicit in `addAnnotation`. Tests typically assert this hits
47
+ * exactly 1 after a successful single-shape gesture. */
48
+ saveCount: number;
49
+ }
50
+ /**
51
+ * Build a {@link MockToolSurface} that mounts attached / added
52
+ * elements into `host`. The host can be any `Element` that accepts
53
+ * `appendChild` — typically a freshly-created `<g>` SVG group from
54
+ * `document.createElementNS` under happy-dom.
55
+ */
56
+ export declare function createMockToolSurface(host: Element): MockToolSurface;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pure type / interface definitions for the editor's tool layer.
3
+ * Moved out of `tools/tool-base.ts` (now in
4
+ * `@ingcreators/annot-editor`) as part of Phase 2 of
5
+ * `docs/plans/three-package-split.md` — these types are shared
6
+ * between the editor (which contains the runtime `ToolBase`
7
+ * class + concrete tool classes) and the rest of `core/editor`
8
+ * (PropertyPanel was a temporary dependent; the helpers in
9
+ * `gradient-utils`, `redact-utils`, `shape-utils`, `text-utils`,
10
+ * `property-controls`, `property-panel-helpers` continue to read
11
+ * variant strings out of these types).
12
+ *
13
+ * Lives in core/editor (Tier B subpath) because consumers run
14
+ * under jsdom for tests and the types are used at the SVG
15
+ * element manipulation layer.
16
+ */
17
+ export type ShapeType = "rect" | "rounded" | "ellipse" | "highlight";
18
+ /** Shape preset for the Counter (Marker) tool. Shared vocabulary
19
+ * with ShapeType so users see the same "rect / rounded / circle"
20
+ * mental model across tools. */
21
+ export type MarkerShape = "circle" | "rect" | "rounded";
22
+ export type ArrowHead = "none" | "end" | "both";
23
+ export type TextVariant = "plain" | "sticky" | "callout";
24
+ export type DrawStyle = "pen" | "highlighter";
25
+ export type RedactStyle = "mosaic" | "solid" | "blur";
26
+ /** Arrow head shapes (per endpoint). Matches PowerPoint's line panel
27
+ * exactly — the six OOXML preset types defined in the ECMA-376
28
+ * standard:
29
+ * none — no marker
30
+ * arrow — open / outline triangle
31
+ * triangle — filled triangle
32
+ * stealth — narrow filled angular arrow
33
+ * diamond — filled diamond
34
+ * oval — filled circle / oval
35
+ * Rendering is path-based (see `arrow-markers.ts`): the head is
36
+ * baked into the line's `<path>` data, not a separate `<marker>`
37
+ * element in `<defs>`. */
38
+ export type ArrowShape = "none" | "arrow" | "triangle" | "stealth" | "diamond" | "oval";
39
+ /** Per-dimension size preset — matches OOXML's `w` / `len` attribute
40
+ * granularity (sm / med / lg). Width is perpendicular to the stem,
41
+ * length is along the stem. */
42
+ export type ArrowDim = "sm" | "md" | "lg";
43
+ export type LineCap = "butt" | "round" | "square";
44
+ export type LineJoin = "miter" | "round" | "bevel";
45
+ /** Horizontal text alignment inside a text-bearing shape. Mirrors
46
+ * the SELECTION-side `data-text-anchor` (start / middle / end)
47
+ * exactly; the Text tool seeds new shapes with this value when set. */
48
+ export type TextAnchor = "start" | "middle" | "end";
49
+ /** Vertical text alignment inside a text-bearing shape. Mirrors
50
+ * the SELECTION-side `data-text-vanchor` (top / middle / bottom). */
51
+ export type TextVerticalAnchor = "top" | "middle" | "bottom";
52
+ /** A single gradient stop — color + 0..1 position along the gradient,
53
+ * optional transparency. The 2-stop case covers the common PowerPoint
54
+ * "fade from A to B" workflow; 3+ stops render fine too. */
55
+ export interface GradientStop {
56
+ color: string;
57
+ offset: number;
58
+ opacity?: number;
59
+ }
60
+ /** Linear-gradient stroke descriptor. When set on an element, the
61
+ * serializer emits a <linearGradient> into the SVG defs and references
62
+ * it via `stroke="url(#...)"`. */
63
+ export interface GradientSpec {
64
+ type: "linear";
65
+ stops: GradientStop[];
66
+ /** Direction in degrees (0 = left→right, 90 = top→bottom, etc). */
67
+ angle: number;
68
+ }
69
+ export interface ToolOptions {
70
+ strokeColor: string;
71
+ fillColor: string;
72
+ strokeWidth: number;
73
+ fontSize: number;
74
+ strokeDasharray: string;
75
+ fillOpacity: number;
76
+ /** Subtype for the unified Shape tool ("rect" / "rounded" / "ellipse").
77
+ * Ignored by tools that don't have a shape-family choice. */
78
+ shapeType?: ShapeType;
79
+ /** Head configuration for the unified Line/Arrow tool.
80
+ * "none" = plain line, "end" = arrow at endpoint (default),
81
+ * "both" = bi-directional arrow. Ignored by non-line tools. */
82
+ arrowHead?: ArrowHead;
83
+ /** Variant for the unified Text tool.
84
+ * "plain" = text only (no background)
85
+ * "sticky" = text with colored background (default, classic sticky note)
86
+ * "callout" = text with pointer tail (speech-bubble-like) */
87
+ textVariant?: TextVariant;
88
+ /** Font family CSS value for the Text tool. Defaults to "sans-serif"
89
+ * when not specified. Stored on the textbox as `data-font-family`
90
+ * so it survives save / reopen / Office paste. */
91
+ fontFamily?: string;
92
+ /** Horizontal text alignment for newly-drawn Text shapes. Defaults
93
+ * to "start" (left-aligned) when not specified. */
94
+ textAnchor?: TextAnchor;
95
+ /** Vertical text alignment for newly-drawn Text shapes. Defaults
96
+ * to "top" when not specified. */
97
+ textVerticalAnchor?: TextVerticalAnchor;
98
+ /** Style for the Draw tool: normal pen vs highlighter. */
99
+ drawStyle?: DrawStyle;
100
+ /** Style for the unified Redact tool. */
101
+ redactStyle?: RedactStyle;
102
+ /** Per-end arrow head shapes. When set, override the simpler
103
+ * `arrowHead` (which only distinguishes none / end / both). */
104
+ arrowHeadStart?: ArrowShape;
105
+ arrowHeadEnd?: ArrowShape;
106
+ /** Per-end arrow widths (perpendicular to stem) — sm / md / lg. */
107
+ arrowWidthStart?: ArrowDim;
108
+ arrowWidthEnd?: ArrowDim;
109
+ /** Per-end arrow lengths (along stem) — sm / md / lg. */
110
+ arrowLengthStart?: ArrowDim;
111
+ arrowLengthEnd?: ArrowDim;
112
+ /** Stroke opacity (0..1). Separate from fill-opacity. */
113
+ strokeOpacity?: number;
114
+ /** SVG stroke-linecap: butt / round / square. */
115
+ strokeLinecap?: LineCap;
116
+ /** SVG stroke-linejoin for shapes with corners (rect, path). */
117
+ strokeLinejoin?: LineJoin;
118
+ /** Optional gradient override for stroke. */
119
+ strokeGradient?: GradientSpec;
120
+ /** Optional gradient override for fill. */
121
+ fillGradient?: GradientSpec;
122
+ /** Color for the Highlight shape variant. */
123
+ highlightColor?: string;
124
+ /** Shape variant for the Counter (Marker) tool. */
125
+ markerShape?: MarkerShape;
126
+ }
@@ -0,0 +1,105 @@
1
+ import { PropertyControlId, PropertyControlOption } from './property-schema.js';
2
+ import { ToolOptions } from './tool-options.js';
3
+ import { ToolPanelExtraControlId } from './tool-registry.js';
4
+ /** Every id a Tool-side `panelControls` entry can reference, plus
5
+ * the Tool-only extras. Drives the type of `TOOL_PANEL_ADAPTERS`'s
6
+ * key. */
7
+ export type ToolPanelAdapterId = PropertyControlId | ToolPanelExtraControlId;
8
+ /**
9
+ * Read / write closure pair routing a single Tool-panel control's
10
+ * value onto the active preset.
11
+ *
12
+ * Most adapters genuinely don't need `toolId` — they target a fixed
13
+ * `ToolOptions` field regardless of which tool's panel is rendering.
14
+ * The exception is `tool.typeChips`, whose target field varies by
15
+ * tool (`shape.shapeType`, `arrow.arrowHead`, …) and is resolved
16
+ * dynamically via `TOOL_REGISTRY[toolId].variantField`. To keep the
17
+ * adapter shape uniform, every read / write takes `toolId` and the
18
+ * implementations that don't need it simply ignore the argument.
19
+ *
20
+ * Values are typed `unknown` at the registry level (a generic `T`
21
+ * runs into the contravariant-position assignability problem when
22
+ * mixed adapters share a single `Record` literal — the same reason
23
+ * the SELECTION-side `PropertyControlDef` keeps T at its default).
24
+ * Each adapter narrows internally via a single `String(value)` /
25
+ * `Number(value)` / `value as Shape` coercion at the write site;
26
+ * Phase 2's renderer is responsible for passing the right value
27
+ * type for the id it's dispatching.
28
+ *
29
+ * `selectionDef` (Phase 4 wiring): names the matching SELECTION-side
30
+ * `PropertyControlId` whose `PROPERTY_CONTROLS[…]` entry supplies the
31
+ * label / options / min / max / step / unit / allowNone metadata.
32
+ * `null` for Tool-only ids whose metadata has no SELECTION-side
33
+ * analogue. Phase 2's renderer doesn't read this yet — Phase 4 is
34
+ * where the metadata reuse lands.
35
+ */
36
+ export interface ToolPanelAdapter {
37
+ /** Read the current value off the preset. */
38
+ read: (preset: ToolOptions, toolId: string) => unknown;
39
+ /** Mutate the preset in place. Returning the value or anything
40
+ * else is ignored — the caller is responsible for persistence
41
+ * (re-saving the preset, re-syncing `ctx.options`, etc.). */
42
+ write: (preset: ToolOptions, value: unknown, toolId: string) => void;
43
+ /** SELECTION-side def whose metadata (label / options / ranges)
44
+ * this adapter borrows. `null` for Tool-only ids. */
45
+ selectionDef?: PropertyControlId | null;
46
+ }
47
+ /**
48
+ * Maps every `panelControls` id to its read / write contract.
49
+ *
50
+ * Adapter coverage rules (validated by `tool-panel-adapter.test.ts`):
51
+ * 1. Every `ToolPanelExtraControlId` has an entry.
52
+ * 2. Every id used by any tool's `panelControls` array has an entry.
53
+ * 3. Round-trip per adapter is a no-op: with a fully-populated preset,
54
+ * `write(preset, read(preset, toolId), toolId)` leaves the preset
55
+ * semantically unchanged (every field the adapter touches reads
56
+ * back the same value).
57
+ *
58
+ * SELECTION-side ids that aren't (yet) used on the Tool side don't
59
+ * need an adapter; only the ones the registry actually references.
60
+ */
61
+ export declare const TOOL_PANEL_ADAPTERS: Readonly<Partial<Record<ToolPanelAdapterId, ToolPanelAdapter>>>;
62
+ /** Static metadata an adapter borrows from its SELECTION-side def.
63
+ * Phase 4 makes this the single source of truth for option arrays
64
+ * (dash / cap / font), labels, number-input ranges, and the
65
+ * `allowNone` color-picker flag — so a UX edit to (say)
66
+ * `PROPERTY_CONTROLS.strokeStyle.options` flows through to BOTH
67
+ * the SELECTION-side panel AND the Tool-side panel without a
68
+ * parallel edit.
69
+ *
70
+ * Per-tool divergences (marker's narrower strokeWidth + fontSize
71
+ * ranges, redact's `allowNone: false`, shape's "Fill" label
72
+ * override) stay in the renderer as documented exceptions on top
73
+ * of this metadata — there's no SELECTION-side equivalent to
74
+ * override against. */
75
+ export interface ToolPanelAdapterMetadata {
76
+ /** SELECTION-side label (e.g. "Color", "Width", "Dash type"). */
77
+ label: string;
78
+ /** Option list for `select` / `variantPicker` controls. Tool-side
79
+ * consumers decorate each option with a per-id preview SVG
80
+ * (dash sample, cap sample, etc.) at render time. */
81
+ options?: ReadonlyArray<PropertyControlOption>;
82
+ /** Inclusive lower bound for a number input. */
83
+ min?: number;
84
+ /** Inclusive upper bound for a number input. */
85
+ max?: number;
86
+ /** Spinner / arrow-key step granularity. */
87
+ step?: number;
88
+ /** Trailing unit label (e.g. "pt", "%"). */
89
+ unit?: string;
90
+ /** Whether a "No fill" sentinel is offered for color pickers. */
91
+ allowNone?: boolean;
92
+ }
93
+ /** Look up the SELECTION-side metadata an adapter borrows. Returns
94
+ * `null` when the adapter has no `selectionDef` (Tool-only ids
95
+ * like `tool.typeChips` / `tool.freehandDone`) or when the named
96
+ * def doesn't exist (which would be a registry typo — guarded
97
+ * defensively rather than throwing).
98
+ *
99
+ * Tier B — pure: no DOM access, no Element-taking; the returned
100
+ * metadata is just the static fields off `PROPERTY_CONTROLS`. */
101
+ export declare function selectionDefMetadata(id: ToolPanelAdapterId): ToolPanelAdapterMetadata | null;
102
+ /** Convenience: list of every adapter id present in the registry.
103
+ * Used by the test file to enumerate the adapters without depending
104
+ * on `Object.keys` order. */
105
+ export declare const TOOL_PANEL_ADAPTER_IDS: ReadonlyArray<ToolPanelAdapterId>;
@@ -0,0 +1,43 @@
1
+ import { ToolOptions } from './tool-options.js';
2
+ /** Wire format selector. `snake` rewrites camelCase keys to
3
+ * snake_case via the table below (matches the Tauri YAML schema in
4
+ * `packages/desktop/src-tauri/src/commands/settings.rs` and the
5
+ * TypeScript-side `ToolPreset` interface). `camel` passes the keys
6
+ * through unchanged (matches the localStorage / chrome.storage
7
+ * blobs, which historically just `JSON.stringify`'d the live
8
+ * `ToolOptions`). */
9
+ export type PresetWireFormat = "snake" | "camel";
10
+ /**
11
+ * Convert a preset object to its wire form. Walks `presetFields` and
12
+ * copies each defined value to the output, optionally rewriting the
13
+ * key to snake_case. `undefined` values are dropped (matches
14
+ * `JSON.stringify`'s behaviour, plus keeps the on-disk YAML diff-
15
+ * friendly).
16
+ *
17
+ * Caller decides which fields are part of the wire schema by passing
18
+ * the tool's `TOOL_REGISTRY[id].presetFields`. Adding a new field to
19
+ * a tool means one entry in that array — no edit to this file
20
+ * required (unless the field is brand-new and missing from the
21
+ * `FIELD_TO_SNAKE` table, in which case `format: "snake"` will fall
22
+ * through to camelCase for that field).
23
+ */
24
+ export declare function presetToWire(opts: Partial<ToolOptions>, presetFields: ReadonlyArray<keyof ToolOptions>, format: PresetWireFormat): Record<string, unknown>;
25
+ /**
26
+ * Inverse of `presetToWire`. Reads each `presetFields` entry off
27
+ * `record` (looking under the appropriate wire-format key) and
28
+ * returns a partial `ToolOptions` containing the present values.
29
+ *
30
+ * Empty strings on optional fields are treated as "absent" — the old
31
+ * imperative loader applied `|| undefined` to most variant fields
32
+ * (`shape_type`, `arrow_head`, …) so a corrupted-empty value didn't
33
+ * pollute the in-memory preset. We preserve that here uniformly so
34
+ * the helper is safe to use on every field, not just the ones that
35
+ * had the explicit `|| undefined` in the old code.
36
+ */
37
+ export declare function presetFromWire(record: Record<string, unknown>, presetFields: ReadonlyArray<keyof ToolOptions>, format: PresetWireFormat): Partial<ToolOptions>;
38
+ /** Translate a snake_case wire key back to its camelCase
39
+ * `ToolOptions` field name, or `undefined` if the key isn't in the
40
+ * shared SerDe table. Exposed as a low-level helper for callers
41
+ * (e.g. preset-key migration paths) that operate on raw wire
42
+ * records without the tool-fields filter. */
43
+ export declare function fieldForSnakeKey(snakeKey: string): keyof ToolOptions | undefined;