@grida/svg-editor 1.0.0-alpha.4 → 1.0.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 (41) hide show
  1. package/README.md +365 -384
  2. package/dist/dom-BEjG2bSw.d.mts +320 -0
  3. package/dist/dom-DYD1BHTo.js +6050 -0
  4. package/dist/dom-Y2QR7QSi.d.ts +318 -0
  5. package/dist/dom-tM3Dr1EK.mjs +5971 -0
  6. package/dist/dom.d.mts +3 -2
  7. package/dist/dom.d.ts +3 -2
  8. package/dist/dom.js +13 -1
  9. package/dist/dom.mjs +2 -2
  10. package/dist/editor-CWNtt1vu.mjs +2998 -0
  11. package/dist/editor-CZgwg8BK.d.mts +2537 -0
  12. package/dist/editor-CesShk9o.js +3004 -0
  13. package/dist/editor-kFTYSb_8.d.ts +2537 -0
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +5 -2
  17. package/dist/index.mjs +3 -2
  18. package/dist/model-D0iEDcg3.mjs +5451 -0
  19. package/dist/model-tywvTGZv.js +5640 -0
  20. package/dist/presets.d.mts +17 -2
  21. package/dist/presets.d.ts +17 -2
  22. package/dist/presets.js +20 -15
  23. package/dist/presets.mjs +19 -15
  24. package/dist/react.d.mts +88 -10
  25. package/dist/react.d.ts +88 -10
  26. package/dist/react.js +169 -14
  27. package/dist/react.mjs +159 -16
  28. package/package.json +35 -9
  29. package/dist/dom-CmOu0HvI.mjs +0 -1623
  30. package/dist/dom-Cn-RtjRL.d.ts +0 -48
  31. package/dist/dom-CoVZzFqy.js +0 -1672
  32. package/dist/dom-DJnZhtOd.d.mts +0 -48
  33. package/dist/editor-CjK56cgb.mjs +0 -1823
  34. package/dist/editor-D2l_CDr0.d.ts +0 -818
  35. package/dist/editor-D2zZAyny.js +0 -1835
  36. package/dist/editor-Uu6dZX4y.d.mts +0 -818
  37. package/dist/index-CHiXYO9-.d.ts +0 -1
  38. package/dist/index-ThDLM4Am.d.mts +0 -1
  39. package/dist/paint-Cfiw4g_J.mjs +0 -477
  40. package/dist/paint-dDV-Trt9.js +0 -531
  41. /package/dist/{chunk-CfYAbeIz.mjs → chunk-D7D4PA-g.mjs} +0 -0
@@ -0,0 +1,320 @@
1
+ import { c as SvgEditor, p as Gestures, s as SurfaceHandle, w as Camera } from "./editor-CZgwg8BK.mjs";
2
+ import cmath from "@grida/cmath";
3
+ import { guide } from "@grida/cmath/_snap";
4
+
5
+ //#region src/util/attention.d.ts
6
+ /** The runtime handle returned by `create_attention_tracker`. */
7
+ interface AttentionTracker {
8
+ /**
9
+ * `true` iff focus is inside the attention scope (the container's
10
+ * subtree or a registered element's subtree) OR the pointer is
11
+ * currently over any element of the scope. See module doc for the
12
+ * rationale.
13
+ *
14
+ * Pure read; no DOM mutation. Cheap enough to call once per keydown.
15
+ */
16
+ is_attended(): boolean;
17
+ /**
18
+ * `true` iff focus is inside the attention scope — the focus arm of
19
+ * {@link is_attended} alone, WITHOUT the pointer-over arm.
20
+ *
21
+ * Exists for the native clipboard gate (`dom.ts`): pointer-over is a
22
+ * sufficient signal to claim a keystroke (worst case: a stolen scroll)
23
+ * but NOT a copy/cut/paste gesture (worst case: destroying what the
24
+ * user believed they copied, or routing a paste meant for a host text
25
+ * field into the document). See docs/wg/feat-svg-editor/clipboard.md
26
+ * §Transport "Gating the native events".
27
+ */
28
+ is_focus_within(): boolean;
29
+ /**
30
+ * Register a host-chrome element into the attention scope. Editor-
31
+ * adjacent chrome — an inspector, a toolbar, a zoom menu; anything that
32
+ * drives `commands.*` — is a DOM *sibling* of the container (the
33
+ * container is exclusively surface-owned), so without registration the
34
+ * tracker cannot tell it apart from unrelated page surface: clicking
35
+ * its buttons moves focus out of the container, and hovering it fires
36
+ * the container's `pointerleave`, blacking out the whole keymap.
37
+ * Registered elements count for both arms — focus-within and
38
+ * pointer-over. Idempotent; re-adding a registered element is a no-op.
39
+ */
40
+ add(element: Element): void;
41
+ /**
42
+ * Unregister an element added via {@link add}. Also clears any
43
+ * still-latched pointer-over contribution from it (the element may be
44
+ * unmounted mid-hover — e.g. a popover closing under the cursor).
45
+ * Unknown elements are a no-op. The container itself cannot be
46
+ * removed; it is the scope's fixed root, not a registered extra.
47
+ */
48
+ remove(element: Element): void;
49
+ /** Detach the internal pointer-tracking listeners (container and every
50
+ * registered element). */
51
+ dispose(): void;
52
+ }
53
+ //#endregion
54
+ //#region src/core/snap/options.d.ts
55
+ type SnapOptions = {
56
+ /** When false, snap behavior and snap-guide rendering are both off. */enabled: boolean;
57
+ /** Snap threshold in HUD canvas pixels (container CSS px in svg-editor;
58
+ * whatever space the consumer feeds in). Constant across zoom because
59
+ * the input rects are already in screen-equivalent units. */
60
+ threshold_px: number;
61
+ };
62
+ declare const DEFAULT_SNAP_OPTIONS: SnapOptions;
63
+ //#endregion
64
+ //#region src/dom.d.ts
65
+ /**
66
+ * Wire a web-font settle source to the editor's geometry channel.
67
+ *
68
+ * The DOM surface re-serializes the `<svg>` on every editor tick, but a
69
+ * `<text>` / `<tspan>` bbox can change with NO attribute write: a web font
70
+ * finishing load AFTER its `font-family` / `font-size` was already written.
71
+ * The IR never sees that reflow, so nothing bumps `geometry_version` and
72
+ * every bounds-keyed consumer (snap, HUD chrome, size meter) stays stuck at
73
+ * the fallback-face metrics until the next real edit.
74
+ *
75
+ * Listens for `loadingdone` on `source` (a `FontFaceSet`, or any injected
76
+ * `EventTarget` in tests) and calls `bump` once per settle. COARSE on
77
+ * purpose: one bump clears the WHOLE bounds cache, not just text nodes —
78
+ * consistent with the package's pessimistic-invalidation stance, and far
79
+ * cheaper than scoping the bump to the (possibly many) reflowed runs.
80
+ *
81
+ * Also bumps once when `source.ready` resolves (when present): fonts that
82
+ * settled before attach — a cache hit, or `font-display` resolving the same
83
+ * tick the surface mounts — never re-fire `loadingdone`, so a document
84
+ * mounted post-settle still needs one bump to re-read at the real metrics.
85
+ *
86
+ * Returns a teardown that removes the listener and neutralizes the pending
87
+ * `ready` bump (leak guard) — call it on surface detach.
88
+ *
89
+ * Factored out of the surface so it can be unit-tested with a fake
90
+ * `EventTarget` in the node-only test env (jsdom's `FontFaceSet` is
91
+ * incomplete); never a real font / network.
92
+ */
93
+ declare function install_font_load_geometry_bump(source: EventTarget | null, bump: () => void): () => void;
94
+ type DomSurfaceOptions = {
95
+ /** Mount the SVG inside this container. */container: HTMLElement;
96
+ /**
97
+ * Install the default gesture set (wheel-pan/zoom, space-drag, middle-mouse,
98
+ * keyboard zoom). Default `true`. Pass `false` to start blank and bind à la
99
+ * carte via `handle.gestures.bind(...)`.
100
+ */
101
+ gestures?: boolean;
102
+ /**
103
+ * Wire native ClipboardEvent transport — `copy` / `cut` / `paste`
104
+ * listeners on the owner document, gated by the clipboard attention
105
+ * discipline. Default `true`. Pass `false` to route ALL clipboard
106
+ * traffic through the `ClipboardProvider` seam instead (the
107
+ * configuration under which a host's paste-time screening governs
108
+ * every path) — see docs/wg/feat-svg-editor/clipboard.md §Transport
109
+ * "Host control over the native path". Focus management (the container
110
+ * focusing on pointerdown) stays either way — it is a general canvas
111
+ * mitigation, not a clipboard feature.
112
+ */
113
+ clipboard?: boolean;
114
+ /**
115
+ * Auto-fit the document into the viewport on initial attach. Default
116
+ * `false`. Mirrors Excalidraw's `initialData.scrollToContent`.
117
+ * Subsequent `editor.load()` calls do NOT re-fit — call
118
+ * `handle.camera.fit("<root>")` yourself if you want that behavior.
119
+ */
120
+ fit?: boolean;
121
+ /**
122
+ * Initial camera transform. Default `cmath.transform.identity`. Ignored
123
+ * when `fit: true`.
124
+ */
125
+ initial_camera?: cmath.Transform;
126
+ /**
127
+ * Font-load settle source — the `EventTarget` whose `loadingdone` event
128
+ * signals "web fonts finished loading, text may have reflowed." Defaults
129
+ * to `container.ownerDocument.fonts` (the live `FontFaceSet`). The
130
+ * surface installs a `loadingdone` listener that advances the editor's
131
+ * geometry channel so text bounds re-read at the settled glyph metrics
132
+ * (see ../docs/geometry.md §Limitations "Text bbox depends on font").
133
+ *
134
+ * Injectable as a DOM seam: jsdom's `FontFaceSet` is incomplete, so
135
+ * tests pass a plain `EventTarget` stub and `dispatchEvent(new Event(
136
+ * "loadingdone"))` to simulate a settle without a real font / network.
137
+ */
138
+ font_load_source?: EventTarget;
139
+ };
140
+ /**
141
+ * Host-extendable attention scope — public via `handle.attention`.
142
+ *
143
+ * The document-level keymap (undo / redo / delete / tool keys) is gated on
144
+ * attention: focus inside the scope, or pointer over it. The scope starts
145
+ * as the container alone, which makes editor-adjacent host chrome — an
146
+ * inspector, a toolbar, a zoom menu; anything that drives `commands.*` —
147
+ * indistinguishable from unrelated page surface (chrome must be a DOM
148
+ * *sibling* of the container, never a child). Registering a chrome element
149
+ * keeps the full keymap live while the user works in it, with
150
+ * text-input focus still excluded by the keymap's own guard. The native
151
+ * clipboard gate (deliberately stricter: focus-only, never pointer-over)
152
+ * honors the registered set's focus arm the same way.
153
+ *
154
+ * `add` is idempotent; `remove` of an unregistered element is a no-op.
155
+ * Registrations live for the surface's lifetime — `detach()` drops them
156
+ * with the tracker. Hosts with mounting/unmounting chrome (popovers,
157
+ * panels) pair `add` on mount with `remove` on unmount.
158
+ */
159
+ type AttentionScope = Pick<AttentionTracker, "add" | "remove">;
160
+ /**
161
+ * Surface handle for the DOM surface. Extends the editor's core
162
+ * `SurfaceHandle` with the viewport-scoped concerns: pan/zoom (`camera`),
163
+ * pointer/wheel/keyboard gesture bindings (`gestures`), and the
164
+ * host-extendable attention scope (`attention`).
165
+ *
166
+ * Camera, gestures, and attention are **surface-scoped**: detaching the
167
+ * surface drops all three. They never appear on the headless `SvgEditor`.
168
+ */
169
+ type DomSurfaceHandle = SurfaceHandle & {
170
+ camera: Camera;
171
+ gestures: Gestures;
172
+ attention: AttentionScope;
173
+ };
174
+ /**
175
+ * Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
176
+ * whose `detach()` is the inverse — DOM cleared, listeners removed,
177
+ * gestures uninstalled.
178
+ *
179
+ * Usage is one-shot per container: the surface owns the container's children
180
+ * for its lifetime, and `detach()` restores it to empty.
181
+ */
182
+ declare function attach_dom_surface(editor: SvgEditor, options: DomSurfaceOptions): DomSurfaceHandle;
183
+ /**
184
+ * Affine projection of a point through a 2×3 CTM, then offset by a
185
+ * container origin (in page CSS-px). Mirrors how `line_endpoints_in_container`
186
+ * and `shape_of` (transformed branch) bridge from local SVG coords to the
187
+ * HUD's container-CSS-px space (HUD keeps its own transform at identity;
188
+ * the SVG carries the camera as a CSS transform, which getScreenCTM
189
+ * folds in).
190
+ *
191
+ * Exported for headless test coverage — pure function, no DOM types.
192
+ */
193
+ declare function project_point_through_ctm(px: number, py: number, ctm: {
194
+ a: number;
195
+ b: number;
196
+ c: number;
197
+ d: number;
198
+ e: number;
199
+ f: number;
200
+ }, container_offset: readonly [number, number]): [number, number];
201
+ /**
202
+ * The matrix form of {@link project_point_through_ctm}: the full
203
+ * `local → container-px` affine for a CTM + container offset, as a
204
+ * `cmath.Transform`. Use when handing the transform to a downstream projector
205
+ * (e.g. HUD chrome) instead of projecting a single point.
206
+ *
207
+ * Pure function, no DOM types.
208
+ */
209
+ declare function ctm_to_container_transform(ctm: {
210
+ a: number;
211
+ b: number;
212
+ c: number;
213
+ d: number;
214
+ e: number;
215
+ f: number;
216
+ }, container_offset: readonly [number, number]): cmath.Transform;
217
+ /**
218
+ * Inverse of the CTM's linear part applied to a delta vector. Drops
219
+ * translation. Used to convert a HUD-reported container-space drag delta
220
+ * back to the path's local coord space for `PathModel.translateVertices`.
221
+ *
222
+ * Throws on a degenerate (det = 0) matrix — the caller is expected to
223
+ * have a non-singular CTM for any visible element.
224
+ */
225
+ declare function project_delta_inverse_ctm(dx: number, dy: number, ctm: {
226
+ a: number;
227
+ b: number;
228
+ c: number;
229
+ d: number;
230
+ }): [number, number];
231
+ /**
232
+ * Map a transform-box reduced affine (box-relative, as emitted by the HUD
233
+ * `transform_box` intent) into the element's path-LOCAL coordinate space,
234
+ * ready to feed {@link PathModel.transformVertices} (Vertex Transform Box,
235
+ * gridaco/grida#881).
236
+ *
237
+ * The transform box lives in container CSS-px: a rect at `box_origin` of
238
+ * dimensions `box_size`, rotated by `box_rotation` degrees (CCW) about
239
+ * `box_origin` — the box's own frame, which may be non-axis-aligned once the
240
+ * session has rotated it. The HUD reports its gesture as a box-relative 2×3
241
+ * whose translation column is normalized to `[0..1]` against `box_size` (see
242
+ * `@grida/hud` transform-box). The mapping composes three stages — right-to-
243
+ * left, so a local point is taken to container space first:
244
+ *
245
+ * M_local = CTM⁻¹ · ( B · P · B⁻¹ ) · CTM, B = T(o) · R(box_rotation)
246
+ *
247
+ * - `CTM` = `local_to_container` (the element's CTM folded with the
248
+ * container offset; see {@link ctm_to_container_transform}).
249
+ * - `P` = `box_transform` with its translation de-normalized back to
250
+ * container-px (`tx·w`, `ty·h`) — the box's transform in its own un-rotated
251
+ * "box pixel" space (origin at the box's `[0,0]`).
252
+ * - `B` re-anchors that box-pixel transform to the box's placement in
253
+ * container space: translate to `box_origin`, rotated by `box_rotation`.
254
+ * The HUD de-rotates the cursor by `-box_rotation` before reducing, so a
255
+ * scale on a rotated box scales along the box's rotated axes.
256
+ *
257
+ * The result is the world→local round-trip of the box affine, so a rotate /
258
+ * scale composes correctly into local geometry through a scaled, rotated, or
259
+ * y-flipped element CTM. Pure, no DOM types. A zero-`box_size` axis is harmless
260
+ * (its translation contribution is `0`); guarding the gesture against a
261
+ * collapsed axis is the caller's job. `local_to_container` must be non-singular
262
+ * (any visible element's CTM is).
263
+ */
264
+ declare function box_transform_to_local_affine(box_transform: cmath.Transform, box_origin: readonly [number, number], box_size: readonly [number, number], local_to_container: cmath.Transform, box_rotation?: number): cmath.Transform;
265
+ /**
266
+ * Derive an oriented-bbox frame `{ origin, size, rotation }` (HUD transform-box
267
+ * shape; `rotation` in degrees CCW) from a rectangle's four container-px
268
+ * corners, given in `[nw, ne, se, sw]` order (Vertex Transform Box,
269
+ * gridaco/grida#881). The box's width axis is `ne − nw` and its height axis is
270
+ * `sw − nw`; assumes a (near-)rectangular quad — true under a similarity CTM,
271
+ * an approximation under a sheared one. Pure, no DOM types.
272
+ */
273
+ declare function obb_frame_from_corners(corners: {
274
+ nw: readonly [number, number];
275
+ ne: readonly [number, number];
276
+ sw: readonly [number, number];
277
+ }): {
278
+ origin: [number, number];
279
+ size: [number, number];
280
+ rotation: number;
281
+ };
282
+ /**
283
+ * Grow a baseline (local-space) bbox so each axis spans at least `min_px`
284
+ * container pixels, given the current local→container per-axis scale. A
285
+ * degenerate axis (a line, where one axis collapses) becomes a grabbable,
286
+ * rotatable strip; padding is centered so the strip stays on the points.
287
+ *
288
+ * Returns `null` when BOTH axes are below `min_px` (a point — no box).
289
+ *
290
+ * Pure, and evaluated against the CURRENT scale at projection time (NOT baked
291
+ * into the persisted session), so a line box stays a stable container-px strip
292
+ * across zoom / CTM changes rather than going stale at the establishment zoom
293
+ * (gridaco/grida#881 review).
294
+ */
295
+ declare function pad_box_min_axis(bbox: cmath.Rectangle, scale_x: number, scale_y: number, min_px: number): cmath.Rectangle | null;
296
+ /**
297
+ * Inverse-project a doc-space rect through a CTM + container offset back
298
+ * into the element's local frame. The output is the AABB of the four
299
+ * inverse-projected corners — when the CTM has a rotation component the
300
+ * AABB is an approximation, but it matches what the user visually
301
+ * expects from a screen-aligned marquee drag.
302
+ *
303
+ * Returns `null` when the CTM's linear part is singular (degenerate
304
+ * camera) — the caller should skip any test that needs the local rect.
305
+ */
306
+ declare function inverse_project_rect(rect: {
307
+ x: number;
308
+ y: number;
309
+ width: number;
310
+ height: number;
311
+ }, ctm: {
312
+ a: number;
313
+ b: number;
314
+ c: number;
315
+ d: number;
316
+ e: number;
317
+ f: number;
318
+ }, offset: readonly [number, number]): cmath.Rectangle | null;
319
+ //#endregion
320
+ export { box_transform_to_local_affine as a, inverse_project_rect as c, project_delta_inverse_ctm as d, project_point_through_ctm as f, attach_dom_surface as i, obb_frame_from_corners as l, SnapOptions as m, DomSurfaceHandle as n, ctm_to_container_transform as o, DEFAULT_SNAP_OPTIONS as p, DomSurfaceOptions as r, install_font_load_geometry_bump as s, AttentionScope as t, pad_box_min_axis as u };