@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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +201 -0
- package/README.md +56 -0
- package/dist/auto-capture-options.d.ts +78 -0
- package/dist/editor/arrow-markers.d.ts +142 -0
- package/dist/editor/bake-translate.d.ts +192 -0
- package/dist/editor/font-registry.d.ts +58 -0
- package/dist/editor/gradient-utils.d.ts +23 -0
- package/dist/editor/history-core.d.ts +48 -0
- package/dist/editor/icons/brand-icons.d.ts +53 -0
- package/dist/editor/icons/material-symbols.d.ts +105 -0
- package/dist/editor/icons/registry.d.ts +179 -0
- package/dist/editor/icons/render.d.ts +22 -0
- package/dist/editor/icons/sanitize.d.ts +60 -0
- package/dist/editor/index.d.ts +13 -0
- package/dist/editor/path-utils.d.ts +32 -0
- package/dist/editor/property-schema.d.ts +263 -0
- package/dist/editor/rich-text-mapper.d.ts +8 -0
- package/dist/editor/selection-geometry.d.ts +66 -0
- package/dist/editor/shape-utils.d.ts +27 -0
- package/dist/editor/svg-format.d.ts +68 -0
- package/dist/editor/svg-id-utils.d.ts +37 -0
- package/dist/editor/svg-to-annotation-shapes.d.ts +34 -0
- package/dist/editor/text-utils.d.ts +212 -0
- package/dist/editor/tool-lifecycle.d.ts +56 -0
- package/dist/editor/tool-options.d.ts +126 -0
- package/dist/editor/tool-panel-adapter.d.ts +105 -0
- package/dist/editor/tool-preset-serde.d.ts +43 -0
- package/dist/editor/tool-registry.d.ts +320 -0
- package/dist/editor/tool-style-reader.d.ts +36 -0
- package/dist/editor/tool-style-writer.d.ts +33 -0
- package/dist/editor/toolbar-icons.d.ts +84 -0
- package/dist/editor/transform-utils.d.ts +127 -0
- package/dist/editor/viewport-math.d.ts +69 -0
- package/dist/encode/index.d.ts +4 -0
- package/dist/encode/options.d.ts +79 -0
- package/dist/encode/png8.d.ts +10 -0
- package/dist/headless.d.ts +29 -0
- package/dist/icons/index.d.ts +15 -0
- package/dist/icons/types.d.ts +108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3164 -0
- package/dist/storage/errors.d.ts +59 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/metadata-cache.d.ts +231 -0
- package/dist/storage/path.d.ts +49 -0
- package/dist/storage/thumbnail-cache.d.ts +110 -0
- package/dist/storage/thumbnail.d.ts +31 -0
- package/dist/storage/types.d.ts +677 -0
- package/dist/utils/assert.d.ts +31 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/dash-utils.d.ts +6 -0
- package/dist/utils/desktop-bridge.d.ts +349 -0
- package/dist/utils/filename.d.ts +48 -0
- package/dist/utils/id.d.ts +21 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/types.d.ts +20 -0
- package/dist/xmp/xmp-browser.d.ts +39 -0
- package/dist/zip/zip-builder.d.ts +8 -0
- package/dist/zip/zip-bytes.d.ts +22 -0
- package/package.json +58 -0
- package/styles/editor.css +1912 -0
- package/styles/fonts.css +46 -0
- package/styles/property-panel.css +779 -0
- 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;
|