@grida/svg-editor 1.0.0-alpha.1 → 1.0.0-alpha.11
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/README.md +184 -185
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/dom-BlMk07oX.mjs +3515 -0
- package/dist/dom-Cvm9Towu.js +3545 -0
- package/dist/dom-DCX-a8Kr.d.ts +57 -0
- package/dist/dom-DgB4f-TE.d.mts +59 -0
- package/dist/dom.d.mts +3 -16
- package/dist/dom.d.ts +3 -16
- package/dist/dom.js +5 -1
- package/dist/dom.mjs +2 -2
- package/dist/editor-BH03X8cX.d.mts +1139 -0
- package/dist/editor-Bd4-VCEJ.d.ts +1139 -0
- package/dist/{editor-DQWUWrVZ.js → editor-CdyC3uAe.js} +1205 -388
- package/dist/{editor-B5z-gTML.mjs → editor-DtuRIs-Q.mjs} +1195 -372
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/dist/index.mjs +3 -2
- package/dist/insertions-BJ-6o6o5.js +2399 -0
- package/dist/insertions-Okcuo-Ck.mjs +2176 -0
- package/dist/presets.d.mts +61 -0
- package/dist/presets.d.ts +61 -0
- package/dist/presets.js +61 -0
- package/dist/presets.mjs +55 -0
- package/dist/react.d.mts +94 -9
- package/dist/react.d.ts +94 -9
- package/dist/react.js +157 -19
- package/dist/react.mjs +147 -21
- package/package.json +11 -6
- package/dist/dom-CfP_ZURh.js +0 -963
- package/dist/dom-kA8NDuVh.mjs +0 -929
- package/dist/editor-CTtU2gu4.d.ts +0 -607
- package/dist/editor-JY7AQrR1.d.mts +0 -607
- package/dist/paint-DHq_3iwU.js +0 -509
- package/dist/paint-DuCg6Y-K.mjs +0 -461
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
import cmath from "@grida/cmath";
|
|
2
|
+
import { AnyNode } from "@grida/svg/parser";
|
|
3
|
+
import * as _$_grida_history0 from "@grida/history";
|
|
4
|
+
import { SelectMode } from "@grida/hud";
|
|
5
|
+
import { Keybinding, Platform } from "@grida/keybinding";
|
|
6
|
+
|
|
7
|
+
//#region src/types.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Stable identifier for a node in the editor's document model.
|
|
10
|
+
*
|
|
11
|
+
* Independent of any backing representation. Generated when the document is
|
|
12
|
+
* parsed.
|
|
13
|
+
*/
|
|
14
|
+
type NodeId = string;
|
|
15
|
+
type Vec2 = {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
};
|
|
19
|
+
type Rect = {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
};
|
|
25
|
+
type Mode = "select" | "edit-content";
|
|
26
|
+
/**
|
|
27
|
+
* SVG element tags supported by the insertion subsystem. Closed set; adding
|
|
28
|
+
* a new insertable tag requires a PR.
|
|
29
|
+
*
|
|
30
|
+
* `text` is deliberately out of v1: `<text>` has no intrinsic size and any
|
|
31
|
+
* usable text-insert UX must immediately mount the inline content-editor on
|
|
32
|
+
* the new node rather than dropping a placeholder. Reserved for a future PR.
|
|
33
|
+
*/
|
|
34
|
+
type InsertableTag = "rect" | "ellipse" | "line";
|
|
35
|
+
/**
|
|
36
|
+
* Active tool — orthogonal to `Mode`. `Mode` is what the editor is doing
|
|
37
|
+
* (interacting normally vs. inline text edit); `Tool` is what pointer-down
|
|
38
|
+
* does while in `select` mode.
|
|
39
|
+
*
|
|
40
|
+
* - `cursor` (default): pointer-down selects / starts marquee / drags
|
|
41
|
+
* selection (existing HUD-driven behavior).
|
|
42
|
+
* - `insert`: pointer-down opens an insertion-preview gesture for the
|
|
43
|
+
* given tag. Click-no-drag inserts a default-sized node; drag sizes
|
|
44
|
+
* the new node. Tool reverts to `cursor` after commit.
|
|
45
|
+
*/
|
|
46
|
+
type Tool = {
|
|
47
|
+
type: "cursor";
|
|
48
|
+
} | {
|
|
49
|
+
type: "insert";
|
|
50
|
+
tag: InsertableTag;
|
|
51
|
+
};
|
|
52
|
+
declare const TOOL_CURSOR: Tool;
|
|
53
|
+
type Provenance = {
|
|
54
|
+
/** CSS cascade origin (css-cascade-5 §6.2). */origin: "author" | "user_agent"; /** Editor metadata — where in the file the winning declaration lives. */
|
|
55
|
+
carrier: "presentation_attribute" | "inline_style" | "stylesheet" | "inherited" | "defaulted";
|
|
56
|
+
};
|
|
57
|
+
type InvalidComputedValue = {
|
|
58
|
+
error: "invalid_at_computed_value_time";
|
|
59
|
+
reason: string;
|
|
60
|
+
};
|
|
61
|
+
type PropertyValue<T = string | number> = {
|
|
62
|
+
declared: string | null;
|
|
63
|
+
computed: T | InvalidComputedValue | null;
|
|
64
|
+
provenance: Provenance;
|
|
65
|
+
};
|
|
66
|
+
type Color = {
|
|
67
|
+
kind: "rgb";
|
|
68
|
+
value: string;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "current_color";
|
|
71
|
+
};
|
|
72
|
+
type PaintFallback = {
|
|
73
|
+
kind: "none";
|
|
74
|
+
} | {
|
|
75
|
+
kind: "color";
|
|
76
|
+
value: Color;
|
|
77
|
+
};
|
|
78
|
+
type Paint = {
|
|
79
|
+
kind: "none";
|
|
80
|
+
} | {
|
|
81
|
+
kind: "color";
|
|
82
|
+
value: Color;
|
|
83
|
+
} | {
|
|
84
|
+
kind: "ref";
|
|
85
|
+
id: string;
|
|
86
|
+
fallback?: PaintFallback;
|
|
87
|
+
} | {
|
|
88
|
+
kind: "context_fill";
|
|
89
|
+
} | {
|
|
90
|
+
kind: "context_stroke";
|
|
91
|
+
};
|
|
92
|
+
type PaintValue = {
|
|
93
|
+
declared: string | null;
|
|
94
|
+
computed: Paint | InvalidComputedValue | null;
|
|
95
|
+
provenance: Provenance;
|
|
96
|
+
};
|
|
97
|
+
type GradientStop = {
|
|
98
|
+
offset: number;
|
|
99
|
+
color: string;
|
|
100
|
+
opacity?: number;
|
|
101
|
+
};
|
|
102
|
+
type LinearGradientDefinition = {
|
|
103
|
+
kind: "linear";
|
|
104
|
+
stops: GradientStop[];
|
|
105
|
+
x1?: number;
|
|
106
|
+
y1?: number;
|
|
107
|
+
x2?: number;
|
|
108
|
+
y2?: number;
|
|
109
|
+
gradient_units?: "user_space_on_use" | "object_bounding_box";
|
|
110
|
+
spread_method?: "pad" | "reflect" | "repeat";
|
|
111
|
+
};
|
|
112
|
+
type RadialGradientDefinition = {
|
|
113
|
+
kind: "radial";
|
|
114
|
+
stops: GradientStop[];
|
|
115
|
+
cx?: number;
|
|
116
|
+
cy?: number;
|
|
117
|
+
r?: number;
|
|
118
|
+
fx?: number;
|
|
119
|
+
fy?: number;
|
|
120
|
+
gradient_units?: "user_space_on_use" | "object_bounding_box";
|
|
121
|
+
spread_method?: "pad" | "reflect" | "repeat";
|
|
122
|
+
};
|
|
123
|
+
type GradientDefinition = LinearGradientDefinition | RadialGradientDefinition;
|
|
124
|
+
type GradientEntry = {
|
|
125
|
+
id: string;
|
|
126
|
+
definition: GradientDefinition;
|
|
127
|
+
ref_count: number;
|
|
128
|
+
};
|
|
129
|
+
type EditorStyle = {
|
|
130
|
+
chrome_color: string;
|
|
131
|
+
handle_size: number;
|
|
132
|
+
handle_fill: string;
|
|
133
|
+
handle_stroke: string;
|
|
134
|
+
endpoint_dot_radius: number;
|
|
135
|
+
selection_outline_width: number;
|
|
136
|
+
/**
|
|
137
|
+
* Color for measurement guides (distance lines + numeric pills). Distinct
|
|
138
|
+
* from `chrome_color` so the user can tell at a glance whether something
|
|
139
|
+
* is selection chrome or a measurement readout.
|
|
140
|
+
*/
|
|
141
|
+
measurement_color: string; /** `W × H` pill under each selected node, in `chrome_color`. */
|
|
142
|
+
show_size_meter: boolean;
|
|
143
|
+
/** Snap to neighbor edges, centers, and equidistant spacing during
|
|
144
|
+
* translate. Both behavior and guides are off when `false`. */
|
|
145
|
+
snap_enabled: boolean;
|
|
146
|
+
/** Snap activation distance in HUD container pixels. Touch UIs may
|
|
147
|
+
* prefer larger (~10–12); precision UIs smaller (~3–4). */
|
|
148
|
+
snap_threshold_px: number;
|
|
149
|
+
/** Hit-test tolerance in screen CSS pixels. The picker selects a node
|
|
150
|
+
* whose rendered geometry is within this many pixels of the pointer,
|
|
151
|
+
* making thin elements (1-px lines, hairline strokes) selectable
|
|
152
|
+
* without pixel-perfect aiming. Tolerance is screen-space, not
|
|
153
|
+
* world-space — the band stays the same width on screen regardless
|
|
154
|
+
* of zoom. `0` disables fat-hit and falls back to elementFromPoint
|
|
155
|
+
* exact-pixel selection. */
|
|
156
|
+
hit_tolerance_px: number;
|
|
157
|
+
/** Snap-to-pixel-grid quantization for translate. When `true`, every
|
|
158
|
+
* translate (drag, nudge, RPC) snaps the agent-union origin to integer
|
|
159
|
+
* multiples of `pixel_grid_size` as the final pipeline stage — composes
|
|
160
|
+
* on top of snap-to-geometry, which still emits guides as usual. Default
|
|
161
|
+
* `false` for SVG-fidelity: SVG paths are unitless and frequently
|
|
162
|
+
* fractional; forcing integers would corrupt authored geometry.
|
|
163
|
+
*
|
|
164
|
+
* Naming: this is the *action* flag (snap to the pixel grid). The
|
|
165
|
+
* "pixel grid" itself — a visual integer-pixel overlay — is a separate
|
|
166
|
+
* feature (see `@grida/canvas-pixelgrid`) and is unrelated. */
|
|
167
|
+
snap_to_pixel_grid: boolean;
|
|
168
|
+
/** Quantum size in HUD container pixels (`1` = integer grid). Ignored
|
|
169
|
+
* when `snap_to_pixel_grid` is false. */
|
|
170
|
+
pixel_grid_size: number;
|
|
171
|
+
/** Show the visual pixel-grid overlay on the HUD. Independent of
|
|
172
|
+
* `snap_to_pixel_grid` — this is the *display* flag, that one is the
|
|
173
|
+
* *action* flag. The grid is zoom-gated; it only paints at high zoom
|
|
174
|
+
* (see `@grida/hud` `setPixelGrid` for the threshold). Default `true`. */
|
|
175
|
+
pixel_grid: boolean;
|
|
176
|
+
/** Shift-drag snap step for rotation, in radians. Default π/12 (15°).
|
|
177
|
+
* When `null` or `<= 0`, shift-rotate is free (no quantization). */
|
|
178
|
+
angle_snap_step_radians: number | null;
|
|
179
|
+
};
|
|
180
|
+
declare const DEFAULT_STYLE: EditorStyle;
|
|
181
|
+
type ClipboardProvider = {
|
|
182
|
+
read(): Promise<string | null>;
|
|
183
|
+
write(text: string): Promise<void>;
|
|
184
|
+
};
|
|
185
|
+
type FontResolver = {
|
|
186
|
+
resolve(family: string): Promise<{
|
|
187
|
+
available: boolean;
|
|
188
|
+
metrics?: {
|
|
189
|
+
ascent: number;
|
|
190
|
+
descent: number;
|
|
191
|
+
unitsPerEm: number;
|
|
192
|
+
};
|
|
193
|
+
}>;
|
|
194
|
+
};
|
|
195
|
+
type FileIOProvider = {
|
|
196
|
+
openSvg(): Promise<string | null>;
|
|
197
|
+
saveSvg(svg: string, suggestedName?: string): Promise<void>;
|
|
198
|
+
};
|
|
199
|
+
type Providers = {
|
|
200
|
+
clipboard?: ClipboardProvider;
|
|
201
|
+
fonts?: FontResolver;
|
|
202
|
+
file_io?: FileIOProvider;
|
|
203
|
+
};
|
|
204
|
+
type EditorState = {
|
|
205
|
+
readonly selection: ReadonlyArray<NodeId>;
|
|
206
|
+
readonly scope: NodeId | null;
|
|
207
|
+
readonly mode: Mode;
|
|
208
|
+
/**
|
|
209
|
+
* Active tool — orthogonal to `mode`. Default `{ type: "cursor" }`. While
|
|
210
|
+
* in `select` mode + `insert` tool, pointer-down on the surface starts
|
|
211
|
+
* an insertion gesture for the configured tag instead of selection.
|
|
212
|
+
* Switched via `editor.set_tool(...)` or the bundled `tool.set` keymap
|
|
213
|
+
* (V/R/O/L). Bumps `state.version` only.
|
|
214
|
+
*/
|
|
215
|
+
readonly tool: Tool;
|
|
216
|
+
readonly dirty: boolean;
|
|
217
|
+
readonly can_undo: boolean;
|
|
218
|
+
readonly can_redo: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Bumps on every editor emission. Use this when you need to react to
|
|
221
|
+
* any change — selection, history, mutation. NOT a good cache key for
|
|
222
|
+
* tree-shape views because it fires on attribute writes too (e.g. x/y
|
|
223
|
+
* during a drag).
|
|
224
|
+
*/
|
|
225
|
+
readonly version: number;
|
|
226
|
+
/**
|
|
227
|
+
* Bumps only when the document's tree shape or display-label-affecting
|
|
228
|
+
* data changes — node added/removed/reordered, text content, or the
|
|
229
|
+
* `id` attribute. Stable across pure presentation-attribute writes.
|
|
230
|
+
*
|
|
231
|
+
* The right cache key for hierarchy / layers panels: snapshot once per
|
|
232
|
+
* `structure_version` so a drag doesn't invalidate the tree view.
|
|
233
|
+
*/
|
|
234
|
+
readonly structure_version: number;
|
|
235
|
+
/**
|
|
236
|
+
* Bumps when any change occurs that could shift a node's world-space
|
|
237
|
+
* bounds — geometry-affecting attribute writes (x, y, d, transform,
|
|
238
|
+
* font-size, …), text content, or structure (insert/remove). Stable
|
|
239
|
+
* across pure presentation writes (fill, stroke-color, opacity).
|
|
240
|
+
*
|
|
241
|
+
* Cache key for `GeometryProvider` — bounds caches snapshot on this
|
|
242
|
+
* so a fill-color change doesn't invalidate them.
|
|
243
|
+
*/
|
|
244
|
+
readonly geometry_version: number;
|
|
245
|
+
/**
|
|
246
|
+
* Bumps once per `editor.load(svg)` call. Distinct from
|
|
247
|
+
* `structure_version` (which bumps on edits too). Starts at 0; the
|
|
248
|
+
* constructor's initial SVG does NOT count as a load. Use this when
|
|
249
|
+
* you want to react to "a new document was loaded" — e.g. refit
|
|
250
|
+
* camera to the new root, reset host-side UI state, clear per-file
|
|
251
|
+
* scratch — without firing on text edits, reorders, or deletes.
|
|
252
|
+
*
|
|
253
|
+
* Monotonic, never resets.
|
|
254
|
+
*/
|
|
255
|
+
readonly load_version: number;
|
|
256
|
+
};
|
|
257
|
+
type Unsubscribe = () => void;
|
|
258
|
+
type ReorderDirection = "bring_forward" | "send_backward" | "bring_to_front" | "send_to_back";
|
|
259
|
+
type PreviewSession = {
|
|
260
|
+
update(value: string): void;
|
|
261
|
+
commit(): void;
|
|
262
|
+
discard(): void;
|
|
263
|
+
};
|
|
264
|
+
type PaintPreviewSession = {
|
|
265
|
+
update(paint: Paint): void;
|
|
266
|
+
commit(): void;
|
|
267
|
+
discard(): void;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Preview-bracketed insertion gesture. Returned by
|
|
271
|
+
* `editor.commands.insert_preview(...)`. The pending node is created and
|
|
272
|
+
* inserted immediately (so the HUD selection chrome renders); per-frame
|
|
273
|
+
* geometry writes call `update(attrs)`; `commit()` collapses the gesture
|
|
274
|
+
* into a single undo step; `discard()` rolls back as if the gesture never
|
|
275
|
+
* happened.
|
|
276
|
+
*/
|
|
277
|
+
type InsertPreviewSession = {
|
|
278
|
+
/** The live node, addressable during drag. */readonly id: NodeId;
|
|
279
|
+
update(attrs: Readonly<Record<string, string>>): void;
|
|
280
|
+
commit(): void;
|
|
281
|
+
discard(): void;
|
|
282
|
+
};
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/core/camera.d.ts
|
|
285
|
+
/**
|
|
286
|
+
* Returns world-space bounds for the given target, or `null` when
|
|
287
|
+
* unresolvable (e.g. empty selection, unknown node id). Implemented by the
|
|
288
|
+
* surface — the camera itself has no view into the document.
|
|
289
|
+
*
|
|
290
|
+
* Only string targets are passed to the resolver — `Rect` targets are
|
|
291
|
+
* handled by the camera as identity (the rect IS its own bounds).
|
|
292
|
+
*/
|
|
293
|
+
type BoundsResolver = (target: "<root>" | "<selection>" | NodeId) => Rect | null;
|
|
294
|
+
type CameraOptions = {
|
|
295
|
+
resolve_bounds: BoundsResolver;
|
|
296
|
+
initial?: cmath.Transform;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Camera viewport constraint. Discriminated union with `type` so future
|
|
300
|
+
* variants (`'contain'`, `'pan-region'`) can be added without breaking
|
|
301
|
+
* existing call sites — each future variant has its own payload shape.
|
|
302
|
+
*
|
|
303
|
+
* v1.1 ships only `'cover'`. CSS analogy: `object-fit: cover` — the
|
|
304
|
+
* bounds rect covers the viewport edge-to-edge. Zoom is lower-bounded
|
|
305
|
+
* at fit-with-padding; pan is clamped so the bounds always covers the
|
|
306
|
+
* viewport. Use for slide / page / kiosk UX where the user should
|
|
307
|
+
* never see past the artwork.
|
|
308
|
+
*/
|
|
309
|
+
type CameraConstraints = {
|
|
310
|
+
/** Bounds cover viewport (viewport ⊆ bounds). Keynote / slide UX. */type: "cover"; /** World-space rect, or `"<root>"` to resolve via BoundsResolver. */
|
|
311
|
+
bounds: Rect | "<root>"; /** Screen-pixel breathing room between bounds and viewport edge. */
|
|
312
|
+
padding?: number;
|
|
313
|
+
/**
|
|
314
|
+
* Screen-pixel scroll slack past the bounds edge when zoomed in past fit.
|
|
315
|
+
* Applies only on axes where the bounds strictly exceed the viewport
|
|
316
|
+
* (`sw > vp_w` / `sh > vp_h`); the centered branch is unchanged, so a
|
|
317
|
+
* fitted axis stays locked at center. Hard clamp — no elasticity, no
|
|
318
|
+
* bounce-back. Default 0 (strict cover behavior). Negative values are
|
|
319
|
+
* clamped to 0. Values approaching or exceeding the bounds extent are
|
|
320
|
+
* permitted but produce visually degenerate behavior; the constraint
|
|
321
|
+
* makes no attempt to cap.
|
|
322
|
+
*/
|
|
323
|
+
pan_overshoot?: number;
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Surface-scoped pan/zoom state.
|
|
327
|
+
*
|
|
328
|
+
* The public shape leads with the peer convention (`center` / `zoom` /
|
|
329
|
+
* `bounds`) and keeps the matrix as an advanced read. Methods mirror
|
|
330
|
+
* Figma/Penpot where they overlap.
|
|
331
|
+
*/
|
|
332
|
+
declare class Camera {
|
|
333
|
+
private _transform;
|
|
334
|
+
private viewport_w;
|
|
335
|
+
private viewport_h;
|
|
336
|
+
private listeners;
|
|
337
|
+
private resolve_bounds;
|
|
338
|
+
private _constraints;
|
|
339
|
+
constructor(opts: CameraOptions);
|
|
340
|
+
/**
|
|
341
|
+
* Current viewport constraint, or `null` for free pan/zoom. Set with
|
|
342
|
+
* `camera.constraints = { type: 'cover', bounds: '<root>', padding: 80 }`
|
|
343
|
+
* to clamp zoom + pan; assign `null` to clear.
|
|
344
|
+
*
|
|
345
|
+
* Constraints are applied synchronously inside `set_transform` (and
|
|
346
|
+
* `_set_viewport_size`), so every public mutation respects them
|
|
347
|
+
* automatically — the host never needs to subscribe-and-clamp itself.
|
|
348
|
+
*/
|
|
349
|
+
get constraints(): CameraConstraints | null;
|
|
350
|
+
set constraints(c: CameraConstraints | null);
|
|
351
|
+
/** Underlying 2D affine. World→screen. */
|
|
352
|
+
get transform(): cmath.Transform;
|
|
353
|
+
/** Uniform scale factor. 1 = 100 %. */
|
|
354
|
+
get zoom(): number;
|
|
355
|
+
/** World-space point currently at viewport center. */
|
|
356
|
+
get center(): Vec2;
|
|
357
|
+
/** World-space rectangle visible in the viewport. */
|
|
358
|
+
get bounds(): Rect;
|
|
359
|
+
/** Translate the camera by a screen-space delta. */
|
|
360
|
+
pan(delta_screen: Vec2): void;
|
|
361
|
+
/**
|
|
362
|
+
* Multiply zoom by `factor` keeping `origin_screen` fixed in world space.
|
|
363
|
+
* Used by wheel-zoom-at-cursor and pinch-zoom.
|
|
364
|
+
*/
|
|
365
|
+
zoom_at(factor: number, origin_screen: Vec2): void;
|
|
366
|
+
/** Pan so `c` lands at the viewport center. Zoom unchanged. */
|
|
367
|
+
set_center(c: Vec2): void;
|
|
368
|
+
/** Set zoom directly; pivot defaults to viewport center. */
|
|
369
|
+
set_zoom(z: number, pivot_screen?: Vec2): void;
|
|
370
|
+
/**
|
|
371
|
+
* Replace the entire transform.
|
|
372
|
+
*
|
|
373
|
+
* Idempotent: when the new transform is element-wise equal to the current
|
|
374
|
+
* one, this is a no-op (no notification fires). This is the seam that
|
|
375
|
+
* makes external constraint loops (e.g. "subscribe → compute clamped →
|
|
376
|
+
* set_transform") terminate: the clamp re-emits the same transform on
|
|
377
|
+
* the second pass, set_transform short-circuits, no recursion.
|
|
378
|
+
*
|
|
379
|
+
* When `camera.constraints` is non-null, the input transform is clamped
|
|
380
|
+
* synchronously before being stored — every public mutation respects the
|
|
381
|
+
* constraint automatically.
|
|
382
|
+
*/
|
|
383
|
+
set_transform(t: cmath.Transform): void;
|
|
384
|
+
/** Viewport size in screen pixels. Read by host code computing constraints. */
|
|
385
|
+
get viewport_size(): {
|
|
386
|
+
width: number;
|
|
387
|
+
height: number;
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Fit a target into the viewport.
|
|
391
|
+
*
|
|
392
|
+
* - `"<root>"` — the document root's content bounds (host-resolved).
|
|
393
|
+
* - `"<selection>"` — current editor.state.selection's union bounds.
|
|
394
|
+
* - `NodeId` — that node's content bounds.
|
|
395
|
+
* - `Rect` — an explicit world-space rectangle.
|
|
396
|
+
*
|
|
397
|
+
* No-ops if the target resolves to `null` (e.g. empty selection) or if
|
|
398
|
+
* the viewport size is 0 (no container).
|
|
399
|
+
*/
|
|
400
|
+
fit(target: "<root>" | "<selection>" | NodeId | Rect, opts?: {
|
|
401
|
+
margin?: number;
|
|
402
|
+
}): void;
|
|
403
|
+
/** Snap back to identity. */
|
|
404
|
+
reset(): void;
|
|
405
|
+
/**
|
|
406
|
+
* Subscribe to camera changes. Fires on every mutation. Cheap channel —
|
|
407
|
+
* does NOT bump `editor.state.version`. Same pattern as
|
|
408
|
+
* `editor.subscribe_surface_hover`.
|
|
409
|
+
*/
|
|
410
|
+
subscribe(cb: () => void): Unsubscribe;
|
|
411
|
+
/** @internal Surface drives this on container resize. */
|
|
412
|
+
_set_viewport_size(w: number, h: number): void;
|
|
413
|
+
/** Convert a screen-space point to world-space. */
|
|
414
|
+
screen_to_world(p: Vec2): Vec2;
|
|
415
|
+
/** Convert a world-space point to screen-space. */
|
|
416
|
+
world_to_screen(p: Vec2): Vec2;
|
|
417
|
+
/**
|
|
418
|
+
* Apply the current constraint (if any) to a candidate transform.
|
|
419
|
+
* Pure: returns the clamped result, never mutates state. Returns the
|
|
420
|
+
* input unchanged when constraints are null / bounds are unresolvable /
|
|
421
|
+
* viewport is 0.
|
|
422
|
+
*/
|
|
423
|
+
private apply_constraints;
|
|
424
|
+
/**
|
|
425
|
+
* Re-clamp the stored transform against the current constraint. Called
|
|
426
|
+
* from the `constraints` setter; `_set_viewport_size` has its own
|
|
427
|
+
* notify-inclusive path.
|
|
428
|
+
*/
|
|
429
|
+
private reenforce;
|
|
430
|
+
private notify;
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/core/align.d.ts
|
|
434
|
+
type AlignDirection = "left" | "right" | "top" | "bottom" | "horizontal_centers" | "vertical_centers";
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/core/document.d.ts
|
|
437
|
+
interface DocumentEvents {
|
|
438
|
+
/** Fires after any structural mutation. */
|
|
439
|
+
on_change(fn: () => void): () => void;
|
|
440
|
+
}
|
|
441
|
+
declare class SvgDocument implements DocumentEvents {
|
|
442
|
+
private nodes;
|
|
443
|
+
private prolog;
|
|
444
|
+
private epilog;
|
|
445
|
+
/** Snapshot of the input string, used for `reset()`. */
|
|
446
|
+
private source;
|
|
447
|
+
/** Original parse result, for `reset()`. */
|
|
448
|
+
private original;
|
|
449
|
+
readonly root: NodeId;
|
|
450
|
+
private listeners;
|
|
451
|
+
/**
|
|
452
|
+
* Counter that bumps ONLY when something the hierarchy view cares about
|
|
453
|
+
* changes — tree topology (`insert`/`remove`), text-node content
|
|
454
|
+
* (`set_text`), or the `id` attribute (which feeds display labels). Pure
|
|
455
|
+
* presentation-attribute writes (x, y, fill, …) do NOT bump it.
|
|
456
|
+
*
|
|
457
|
+
* Why a separate counter: consumers like the layers panel cache snapshots
|
|
458
|
+
* keyed on this. During a drag, x/y writes fire `emit()` repeatedly but
|
|
459
|
+
* `structure_version` stays stable, so the panel's snapshot reference
|
|
460
|
+
* stays the same and React skips the re-render of the whole tree.
|
|
461
|
+
*/
|
|
462
|
+
private _structure_version;
|
|
463
|
+
/** Bumps on writes that can shift world-space bounds (`GEOMETRY_ATTRS`,
|
|
464
|
+
* `set_text`, `insert`, `remove`). Cache key for `GeometryProvider`;
|
|
465
|
+
* see docs/wg/feat-svg-editor/geometry.md. */
|
|
466
|
+
private _geometry_version;
|
|
467
|
+
constructor(svg: string);
|
|
468
|
+
static parse(svg: string): SvgDocument;
|
|
469
|
+
/** Reload from the original parse, discarding all edits. */
|
|
470
|
+
reset_to_original(): void;
|
|
471
|
+
/** Replace document with new svg source (clears edits + history-owned state). */
|
|
472
|
+
load(svg: string): void;
|
|
473
|
+
on_change(fn: () => void): () => void;
|
|
474
|
+
/** See `_structure_version` for what this counter signals. */
|
|
475
|
+
get structure_version(): number;
|
|
476
|
+
/** See `_geometry_version` for what this counter signals. */
|
|
477
|
+
get geometry_version(): number;
|
|
478
|
+
private emit;
|
|
479
|
+
/** Notify subscribers — for callers that mutate directly via setAttr/etc. */
|
|
480
|
+
notify(): void;
|
|
481
|
+
get(id: NodeId): AnyNode | null;
|
|
482
|
+
is_element(id: NodeId): boolean;
|
|
483
|
+
parent_of(id: NodeId): NodeId | null;
|
|
484
|
+
children_of(id: NodeId): readonly NodeId[];
|
|
485
|
+
/** Element children only — text/comment/cdata filtered out. */
|
|
486
|
+
element_children_of(id: NodeId): readonly NodeId[];
|
|
487
|
+
next_sibling_of(id: NodeId): NodeId | null;
|
|
488
|
+
next_element_sibling_of(id: NodeId): NodeId | null;
|
|
489
|
+
tag_of(id: NodeId): string;
|
|
490
|
+
contains(ancestor: NodeId, descendant: NodeId): boolean;
|
|
491
|
+
/**
|
|
492
|
+
* Filter a selection down to its **subtree roots** — drop any id whose
|
|
493
|
+
* ancestor is also in the input set.
|
|
494
|
+
*
|
|
495
|
+
* Mirrors `pruneNestedNodes` in the main canvas editor's query module
|
|
496
|
+
* ([editor/grida-canvas/query/index.ts:138](../../../../editor/grida-canvas/query/index.ts)) and shares its UX motivation:
|
|
497
|
+
* when a parent and a descendant are both selected, only the parent
|
|
498
|
+
* should drive multi-node mutations — otherwise the descendant
|
|
499
|
+
* accumulates the transform twice (once via the parent's `transform`,
|
|
500
|
+
* once via its own attribute write). Required for `commands.remove`
|
|
501
|
+
* (avoids re-attaching detached descendants on undo) and any multi-
|
|
502
|
+
* member translate path (avoids 2× drift for the Bar-chart marquee
|
|
503
|
+
* case).
|
|
504
|
+
*
|
|
505
|
+
* Order: preserves the input order for retained ids. Duplicates in
|
|
506
|
+
* the input are not deduplicated — callers are responsible (the
|
|
507
|
+
* editor's `commands.select` already dedupes).
|
|
508
|
+
*
|
|
509
|
+
* Performance: `O(n × depth)`. Builds a `Set` over the input once,
|
|
510
|
+
* then walks each id's ancestor chain at most once. The main editor's
|
|
511
|
+
* version is `O(n² × depth)` (per-pair `isAncestor`) — fine at typical
|
|
512
|
+
* selection sizes (a few dozen), worth winning here for free since
|
|
513
|
+
* `parent_of` is `O(1)` on our parent-map.
|
|
514
|
+
*/
|
|
515
|
+
prune_nested_nodes(ids: ReadonlyArray<NodeId>): NodeId[];
|
|
516
|
+
all_nodes(): readonly NodeId[];
|
|
517
|
+
all_elements(): readonly NodeId[];
|
|
518
|
+
find_by_tag(ancestor: NodeId, tag: string): readonly NodeId[];
|
|
519
|
+
/** Read attribute by local name, optionally namespace-filtered. */
|
|
520
|
+
get_attr(id: NodeId, name: string, ns?: string | null): string | null;
|
|
521
|
+
/**
|
|
522
|
+
* Set / remove an attribute. If the attribute exists, it is mutated in place
|
|
523
|
+
* (preserving source position). If it doesn't, it's appended.
|
|
524
|
+
*/
|
|
525
|
+
set_attr(id: NodeId, name: string, value: string | null, ns?: string | null): void;
|
|
526
|
+
attributes_of(id: NodeId): {
|
|
527
|
+
name: string;
|
|
528
|
+
ns: string | null;
|
|
529
|
+
value: string;
|
|
530
|
+
}[];
|
|
531
|
+
get_style(id: NodeId, property: string): string | null;
|
|
532
|
+
set_style(id: NodeId, property: string, value: string | null): void;
|
|
533
|
+
get_all_styles(id: NodeId): Array<{
|
|
534
|
+
property: string;
|
|
535
|
+
value: string;
|
|
536
|
+
}>;
|
|
537
|
+
/**
|
|
538
|
+
* Whether `id` can be opened in the flat-string text editor.
|
|
539
|
+
*
|
|
540
|
+
* v1 contract: the editor only operates on a *single flat text run*. That
|
|
541
|
+
* means the target must be a `<text>` or `<tspan>` whose direct children
|
|
542
|
+
* are all text nodes (or it has no children). A `<text>` containing a
|
|
543
|
+
* `<tspan>` is *not* honestly editable — `text_of` would drop the tspan
|
|
544
|
+
* content from the editor's view, and a flat-text write would leave the
|
|
545
|
+
* tspan dangling. Tspan-as-target is fine and well-defined when it's a
|
|
546
|
+
* leaf; only the host decides whether to route double-click to a tspan
|
|
547
|
+
* or its parent text.
|
|
548
|
+
*/
|
|
549
|
+
is_text_edit_target(id: NodeId): boolean;
|
|
550
|
+
text_of(id: NodeId): string;
|
|
551
|
+
/** Replace all direct text children with a single text node carrying `value`. */
|
|
552
|
+
set_text(id: NodeId, value: string): void;
|
|
553
|
+
insert(id: NodeId, parent: NodeId, before: NodeId | null): void;
|
|
554
|
+
remove(id: NodeId): void;
|
|
555
|
+
/** Create a new element node and register it (not yet inserted). */
|
|
556
|
+
create_element(local: string, opts?: {
|
|
557
|
+
prefix?: string | null;
|
|
558
|
+
ns?: string | null;
|
|
559
|
+
}): NodeId;
|
|
560
|
+
serialize(): string;
|
|
561
|
+
private emit_node;
|
|
562
|
+
private emit_attr;
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/core/defs.d.ts
|
|
566
|
+
interface GradientsApi {
|
|
567
|
+
list(): ReadonlyArray<GradientEntry>;
|
|
568
|
+
get(id: string): GradientEntry | null;
|
|
569
|
+
upsert(definition: GradientDefinition, opts?: {
|
|
570
|
+
id?: string;
|
|
571
|
+
}): string;
|
|
572
|
+
remove(id: string): void;
|
|
573
|
+
subscribe(fn: (entries: ReadonlyArray<GradientEntry>) => void): Unsubscribe;
|
|
574
|
+
}
|
|
575
|
+
type Defs = {
|
|
576
|
+
gradients: GradientsApi;
|
|
577
|
+
};
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/commands/registry.d.ts
|
|
580
|
+
/**
|
|
581
|
+
* Command registry.
|
|
582
|
+
*
|
|
583
|
+
* A passive id-keyed registry of handlers. Built so that:
|
|
584
|
+
*
|
|
585
|
+
* - keybindings (in `src/keymap`) can address commands by stable id;
|
|
586
|
+
* - new commands can be added in ONE place (`src/commands/defaults.ts`)
|
|
587
|
+
* without growing the public surface of the editor;
|
|
588
|
+
* - "one key, many meanings" can be expressed via chain semantics: a
|
|
589
|
+
* handler returns `true` if it consumed, `false`/`void` otherwise,
|
|
590
|
+
* and the dispatcher tries the next candidate in the chain.
|
|
591
|
+
*
|
|
592
|
+
* Handlers are plain closures — they capture whatever editor reference
|
|
593
|
+
* they need. The registry itself stays unaware of the editor's type,
|
|
594
|
+
* which avoids a circular type dependency between editor and registry.
|
|
595
|
+
*/
|
|
596
|
+
/** Stable, dotted id for a command, e.g. `"history.undo"`. */
|
|
597
|
+
type CommandId = string;
|
|
598
|
+
/**
|
|
599
|
+
* A command handler.
|
|
600
|
+
*
|
|
601
|
+
* Return `true` if the handler consumed the invocation. Return `false`
|
|
602
|
+
* or `undefined` to signal "did not apply" — the dispatcher will try
|
|
603
|
+
* the next candidate registered for the same key.
|
|
604
|
+
*
|
|
605
|
+
* Handlers are closures: they capture their editor reference. No
|
|
606
|
+
* editor parameter is passed — keep handlers self-contained.
|
|
607
|
+
*/
|
|
608
|
+
type CommandHandler = (args?: unknown) => boolean | void;
|
|
609
|
+
declare class CommandRegistry {
|
|
610
|
+
private readonly map;
|
|
611
|
+
/**
|
|
612
|
+
* Register a command. Returns an unregister function. Re-registering
|
|
613
|
+
* the same id replaces the previous handler (last writer wins).
|
|
614
|
+
*/
|
|
615
|
+
register(id: CommandId, handler: CommandHandler): () => void;
|
|
616
|
+
/**
|
|
617
|
+
* Invoke a command by id. Returns `true` if the handler consumed,
|
|
618
|
+
* `false` otherwise (including unknown ids and handlers that returned
|
|
619
|
+
* `false`/`undefined`).
|
|
620
|
+
*/
|
|
621
|
+
invoke(id: CommandId, args?: unknown): boolean;
|
|
622
|
+
has(id: CommandId): boolean;
|
|
623
|
+
/** All registered ids, for debugging / introspection. */
|
|
624
|
+
ids(): readonly CommandId[];
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/keymap/keymap.d.ts
|
|
628
|
+
type KeymapBinding = {
|
|
629
|
+
/** Declarative key combination. Build with `kb()` / `c()` / `seq()`. */keybinding: Keybinding; /** Command id to invoke on match. */
|
|
630
|
+
command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
|
|
631
|
+
args?: unknown; /** Higher priorities run first in the chain. Default 0. */
|
|
632
|
+
priority?: number;
|
|
633
|
+
/**
|
|
634
|
+
* Reserved for V2; not honored by the V1 dispatcher. When added, this
|
|
635
|
+
* will be evaluated before the handler runs; if false, the binding is
|
|
636
|
+
* skipped without invoking the handler.
|
|
637
|
+
*/
|
|
638
|
+
when?: (ctx: unknown) => boolean;
|
|
639
|
+
};
|
|
640
|
+
declare class Keymap {
|
|
641
|
+
private readonly commands;
|
|
642
|
+
private readonly platformGetter;
|
|
643
|
+
/**
|
|
644
|
+
* Bindings bucketed by canonical chunk-key hash, computed per
|
|
645
|
+
* `@grida/keybinding`'s `chunkKey`. Each list is the chain for that
|
|
646
|
+
* key, sorted in dispatch order (priority desc, then registration
|
|
647
|
+
* order).
|
|
648
|
+
*/
|
|
649
|
+
private readonly buckets;
|
|
650
|
+
/** Insert order, so ties on priority are deterministic. */
|
|
651
|
+
private seq;
|
|
652
|
+
constructor(commands: CommandRegistry, platformGetter?: () => Platform);
|
|
653
|
+
/**
|
|
654
|
+
* Bind a key combination to a command. Returns an unbind function.
|
|
655
|
+
* The same `Keybinding` can be bound to multiple commands — they will
|
|
656
|
+
* all be tried in chain order on dispatch.
|
|
657
|
+
*/
|
|
658
|
+
bind(binding: KeymapBinding): () => void;
|
|
659
|
+
/**
|
|
660
|
+
* Remove bindings matching the spec. If both filters are passed, only
|
|
661
|
+
* bindings that match BOTH are removed.
|
|
662
|
+
*/
|
|
663
|
+
unbind(spec: {
|
|
664
|
+
keybinding?: Keybinding;
|
|
665
|
+
command?: CommandId;
|
|
666
|
+
}): void;
|
|
667
|
+
/** All registered bindings, for introspection. Order is not guaranteed. */
|
|
668
|
+
bindings(): readonly KeymapBinding[];
|
|
669
|
+
/**
|
|
670
|
+
* Does the keymap have a binding that matches this event's chord —
|
|
671
|
+
* regardless of whether any handler would consume it? Hosts use this
|
|
672
|
+
* to decide whether to swallow the platform's default action (e.g.
|
|
673
|
+
* `event.preventDefault()` in the browser), so that an advertised
|
|
674
|
+
* shortcut like `Cmd+G` doesn't fall through to the browser's find
|
|
675
|
+
* bar even when the binding's handler rejects.
|
|
676
|
+
*
|
|
677
|
+
* Pure read; runs no handlers, no side effects. Honors the same
|
|
678
|
+
* text-input-focused guard `dispatch` uses, so a typing user's
|
|
679
|
+
* keystroke isn't "claimed" by an unrelated unmodified key.
|
|
680
|
+
*/
|
|
681
|
+
claims(event: KeyboardEvent): boolean;
|
|
682
|
+
/**
|
|
683
|
+
* Match the event against bound chunks, then run candidates in chain
|
|
684
|
+
* order. Returns `true` on the first handler that consumes; returns
|
|
685
|
+
* `false` if nothing matched or all matches fell through.
|
|
686
|
+
*
|
|
687
|
+
* `dispatch` is browser-agnostic: it does NOT call `preventDefault()`
|
|
688
|
+
* or touch the event in any way. The host decides what to do with the
|
|
689
|
+
* platform default — typically `if (keymap.claims(e)) e.preventDefault()`,
|
|
690
|
+
* which prevents the platform default for advertised shortcuts even
|
|
691
|
+
* when the chain rejects. See README → `editor.keymap`.
|
|
692
|
+
*/
|
|
693
|
+
dispatch(event: KeyboardEvent): boolean;
|
|
694
|
+
/**
|
|
695
|
+
* Compute the set of canonical hashes a `Keybinding` lights up. A
|
|
696
|
+
* binding with aliases (multiple sequences) contributes one hash per
|
|
697
|
+
* single-chunk alias; multi-chunk sequences (chords) are skipped in
|
|
698
|
+
* V1.
|
|
699
|
+
*/
|
|
700
|
+
private chunkKeysFor;
|
|
701
|
+
private has_safe_mod;
|
|
702
|
+
}
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region src/core/geometry.d.ts
|
|
705
|
+
/**
|
|
706
|
+
* Read-only access to world-space node bounds and hit-tests.
|
|
707
|
+
*
|
|
708
|
+
* Implementations should be cheap to call: snap (the main consumer)
|
|
709
|
+
* will query dozens of nodes per pointermove. The driver wraps SVG
|
|
710
|
+
* `getBBox` + `getCTM`; the memoizer caches per-`NodeId` to survive
|
|
711
|
+
* the surface re-rendering the SVG tree every editor tick.
|
|
712
|
+
*/
|
|
713
|
+
interface GeometryProvider {
|
|
714
|
+
/**
|
|
715
|
+
* World-space bounding rect for a single node, or `null` when the
|
|
716
|
+
* node has no rendered geometry (orphan, detached, unsupported tag).
|
|
717
|
+
*/
|
|
718
|
+
bounds_of(id: NodeId): Rect | null;
|
|
719
|
+
/**
|
|
720
|
+
* Bulk read. Missing nodes are simply absent from the result map.
|
|
721
|
+
* Drivers should be free to batch / parallelize internally.
|
|
722
|
+
*/
|
|
723
|
+
bounds_of_many(ids: ReadonlyArray<NodeId>): Map<NodeId, Rect>;
|
|
724
|
+
/**
|
|
725
|
+
* Ids of nodes whose world-space bounds intersect `rect`. Order is
|
|
726
|
+
* implementation-defined.
|
|
727
|
+
*/
|
|
728
|
+
nodes_in_rect(rect: Rect): NodeId[];
|
|
729
|
+
/**
|
|
730
|
+
* Topmost node id at world-space point `p`, or `null` when no node
|
|
731
|
+
* is hit. "Topmost" is defined by the renderer's z-order.
|
|
732
|
+
*/
|
|
733
|
+
node_at_point(p: Vec2): NodeId | null;
|
|
734
|
+
}
|
|
735
|
+
type GeometrySignals = {
|
|
736
|
+
/** Fires when tree shape changes (insert/remove/reorder). */subscribe_structure: (cb: () => void) => Unsubscribe; /** Fires when any bounds-affecting change occurs. */
|
|
737
|
+
subscribe_geometry: (cb: () => void) => Unsubscribe;
|
|
738
|
+
};
|
|
739
|
+
/**
|
|
740
|
+
* Caches `bounds_of` results keyed on `NodeId`; full-clears on either
|
|
741
|
+
* `structure_version` or `geometry_version` bump. See docs/wg/feat-svg-editor/geometry.md for
|
|
742
|
+
* why the cache is load-bearing under the surface's per-tick re-render.
|
|
743
|
+
*/
|
|
744
|
+
declare class MemoizedGeometryProvider implements GeometryProvider {
|
|
745
|
+
private readonly driver;
|
|
746
|
+
private readonly unsubscribers;
|
|
747
|
+
private cache;
|
|
748
|
+
constructor(driver: GeometryProvider, signals: GeometrySignals);
|
|
749
|
+
bounds_of(id: NodeId): Rect | null;
|
|
750
|
+
bounds_of_many(ids: ReadonlyArray<NodeId>): Map<NodeId, Rect>;
|
|
751
|
+
/**
|
|
752
|
+
* Pass-through. These are less hot than `bounds_of` (called once per
|
|
753
|
+
* gesture frame at most) and their result is sensitive to current
|
|
754
|
+
* viewport state, so caching them would be a footgun.
|
|
755
|
+
*/
|
|
756
|
+
nodes_in_rect(rect: Rect): NodeId[];
|
|
757
|
+
node_at_point(p: Vec2): NodeId | null;
|
|
758
|
+
/** Unsubscribe from both signals. Call on surface detach. */
|
|
759
|
+
dispose(): void;
|
|
760
|
+
}
|
|
761
|
+
//#endregion
|
|
762
|
+
//#region src/gestures/gestures.d.ts
|
|
763
|
+
/** Stable identifier for a gesture binding. Used by `unbind({ id })`. */
|
|
764
|
+
type GestureId = string;
|
|
765
|
+
/**
|
|
766
|
+
* Context passed to every installer. Exposes the seams a gesture needs:
|
|
767
|
+
* the container element to listen on, the camera to mutate, and the
|
|
768
|
+
* editor for keymap dispatch / state reads.
|
|
769
|
+
*
|
|
770
|
+
* Surface authors construct this once at attach; bindings receive it on
|
|
771
|
+
* every `install(...)` call.
|
|
772
|
+
*/
|
|
773
|
+
type GestureContext = {
|
|
774
|
+
/** Container element listeners attach to. */container: HTMLElement; /** SVG element being framed by the camera. Useful for hit-testing. */
|
|
775
|
+
svg_root: () => SVGSVGElement | null; /** HUD canvas overlay; sits on top of the SVG. */
|
|
776
|
+
hud_canvas: HTMLCanvasElement; /** Camera the binding mutates. */
|
|
777
|
+
camera: Camera; /** Editor for keymap dispatch / state reads. */
|
|
778
|
+
editor: SvgEditor; /** Handle for advanced bindings (e.g. wanting `camera.fit("<selection>")`). */
|
|
779
|
+
handle: SurfaceHandle;
|
|
780
|
+
};
|
|
781
|
+
type GestureBinding = {
|
|
782
|
+
/** Stable id used by `unbind` / `bindings()`. */id: GestureId;
|
|
783
|
+
/**
|
|
784
|
+
* Wire DOM listeners (or any side-effect) needed for this gesture.
|
|
785
|
+
* Returns the uninstaller — called on `unbind` or surface detach.
|
|
786
|
+
*/
|
|
787
|
+
install(ctx: GestureContext): () => void;
|
|
788
|
+
};
|
|
789
|
+
/**
|
|
790
|
+
* Sibling to `Keymap`. Owns a list of installed gesture bindings; each
|
|
791
|
+
* binding's `install(ctx)` is called eagerly when bound and uninstalled
|
|
792
|
+
* on `unbind` or surface detach.
|
|
793
|
+
*/
|
|
794
|
+
declare class Gestures {
|
|
795
|
+
private readonly ctx;
|
|
796
|
+
private entries;
|
|
797
|
+
constructor(ctx: GestureContext);
|
|
798
|
+
/**
|
|
799
|
+
* Install a gesture binding. Returns an unbind function.
|
|
800
|
+
* Re-binding the same `id` does NOT replace — both will be active.
|
|
801
|
+
* Use `unbind({ id })` first if you want a clean swap.
|
|
802
|
+
*/
|
|
803
|
+
bind(binding: GestureBinding): () => void;
|
|
804
|
+
/**
|
|
805
|
+
* Remove bindings matching the spec. With `{ id }`, all bindings with
|
|
806
|
+
* that id are uninstalled. With no spec, this is a no-op (use
|
|
807
|
+
* `dispose()` to nuke everything).
|
|
808
|
+
*/
|
|
809
|
+
unbind(spec: {
|
|
810
|
+
id?: GestureId;
|
|
811
|
+
}): void;
|
|
812
|
+
/** All currently installed bindings. Order is registration order. */
|
|
813
|
+
bindings(): readonly GestureBinding[];
|
|
814
|
+
/** @internal Uninstall every binding. Surface calls on detach. */
|
|
815
|
+
_dispose(): void;
|
|
816
|
+
}
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/core/editor.d.ts
|
|
819
|
+
/** Resolved paint from the DOM-attached cascade. `resolved_paint` mirrors the
|
|
820
|
+
* shape of `PaintValue.computed` so consumers can treat it uniformly with
|
|
821
|
+
* the headless cascade. */
|
|
822
|
+
type DomComputedPaint = {
|
|
823
|
+
computed: string;
|
|
824
|
+
resolved_paint: Paint | InvalidComputedValue | null;
|
|
825
|
+
};
|
|
826
|
+
/** Contract the DOM surface implements to delegate cascade resolution to
|
|
827
|
+
* `getComputedStyle()`. Registered via `editor._internal.set_computed_resolver`
|
|
828
|
+
* on attach. */
|
|
829
|
+
type DomComputedResolver = {
|
|
830
|
+
computed_property(id: NodeId, name: string): string | null;
|
|
831
|
+
computed_paint(id: NodeId, channel: "fill" | "stroke"): DomComputedPaint | null;
|
|
832
|
+
};
|
|
833
|
+
type CreateSvgEditorOptions = {
|
|
834
|
+
svg: string;
|
|
835
|
+
providers?: Providers;
|
|
836
|
+
style?: Partial<EditorStyle>;
|
|
837
|
+
};
|
|
838
|
+
type SvgEditor = ReturnType<typeof createSvgEditor>;
|
|
839
|
+
type Surface = {
|
|
840
|
+
paint(snapshot: unknown): void;
|
|
841
|
+
hit_test(x: number, y: number): NodeId | null;
|
|
842
|
+
on_input(listener: (event: unknown) => void): Unsubscribe;
|
|
843
|
+
dispose(): void;
|
|
844
|
+
};
|
|
845
|
+
type SurfaceHandle = {
|
|
846
|
+
detach(): void;
|
|
847
|
+
};
|
|
848
|
+
type Commands = {
|
|
849
|
+
select(target: NodeId | ReadonlyArray<NodeId>, opts?: {
|
|
850
|
+
mode?: SelectMode;
|
|
851
|
+
}): void;
|
|
852
|
+
deselect(): void;
|
|
853
|
+
/**
|
|
854
|
+
* Replace the selection with every element-child of the current scope
|
|
855
|
+
* (or `doc.root` when no scope is entered). Returns `false` when the
|
|
856
|
+
* scope has no element children — letting the keymap chain fall through
|
|
857
|
+
* unchanged.
|
|
858
|
+
*/
|
|
859
|
+
select_all(): boolean;
|
|
860
|
+
/**
|
|
861
|
+
* Rotate the selection to the next or previous sibling within the
|
|
862
|
+
* current single-selection's parent. Wraps at both ends. With an empty
|
|
863
|
+
* or multi-selection, falls back to selecting the first / last child of
|
|
864
|
+
* the current scope (so Tab from "nothing selected" picks something).
|
|
865
|
+
* Returns `false` when no candidate exists (empty scope).
|
|
866
|
+
*/
|
|
867
|
+
select_sibling(direction: "next" | "prev"): boolean;
|
|
868
|
+
enter_scope(group: NodeId): void;
|
|
869
|
+
exit_scope(): void;
|
|
870
|
+
set_mode(mode: Mode): void;
|
|
871
|
+
set_property(name: string, value: string | null): void;
|
|
872
|
+
preview_property(name: string): PreviewSession;
|
|
873
|
+
set_paint(channel: "fill" | "stroke", paint: Paint): void;
|
|
874
|
+
preview_paint(channel: "fill" | "stroke"): PaintPreviewSession;
|
|
875
|
+
set_paint_from_gradient(channel: "fill" | "stroke", definition: GradientDefinition, opts?: {
|
|
876
|
+
reuse_existing?: boolean;
|
|
877
|
+
}): {
|
|
878
|
+
gradient_id: string;
|
|
879
|
+
};
|
|
880
|
+
translate(delta: {
|
|
881
|
+
dx: number;
|
|
882
|
+
dy: number;
|
|
883
|
+
}): void;
|
|
884
|
+
nudge(delta: {
|
|
885
|
+
dx: number;
|
|
886
|
+
dy: number;
|
|
887
|
+
}): void;
|
|
888
|
+
/**
|
|
889
|
+
* Map the selection's current union-bbox to `target` as a single atomic
|
|
890
|
+
* step. Maps width/height/x/y simultaneously — each member scales
|
|
891
|
+
* around the union NW anchor, then the result is translated so the
|
|
892
|
+
* union NW lands at `target.{x, y}`. Per-tag constraints (circle
|
|
893
|
+
* uniform, text edge no-op) execute inside `apply_resize` for each
|
|
894
|
+
* member.
|
|
895
|
+
*
|
|
896
|
+
* The default selection is `state.selection`. Pass `opts.ids` to
|
|
897
|
+
* override. Members whose tag is not resizable
|
|
898
|
+
* (e.g. `<g>`) are skipped silently; the gesture is a no-op when no
|
|
899
|
+
* resizable member remains. Returns `true` when a history step was
|
|
900
|
+
* pushed.
|
|
901
|
+
*/
|
|
902
|
+
resize_to(target: {
|
|
903
|
+
x: number;
|
|
904
|
+
y: number;
|
|
905
|
+
width: number;
|
|
906
|
+
height: number;
|
|
907
|
+
}, opts?: {
|
|
908
|
+
ids?: ReadonlyArray<NodeId>;
|
|
909
|
+
}): boolean;
|
|
910
|
+
/**
|
|
911
|
+
* Rotate the selection by `angle` radians around the union-bbox center
|
|
912
|
+
* (or `opts.pivot` if provided). One atomic history step. Returns
|
|
913
|
+
* `false` and a no-op when any member's transform isn't rotatable (see
|
|
914
|
+
* `is_rotatable` — refuses non-trivial transforms, `<text rotate>`,
|
|
915
|
+
* CSS-property transforms, animated transforms).
|
|
916
|
+
*
|
|
917
|
+
* Pivot defaults to bbox-center of the live selection via the attached
|
|
918
|
+
* surface's `geometry_provider`. With no surface attached, the function
|
|
919
|
+
* uses local-attribute bbox approximations (less precise for transformed
|
|
920
|
+
* ancestors, but correct for flat docs).
|
|
921
|
+
*/
|
|
922
|
+
rotate(angle: number, opts?: {
|
|
923
|
+
ids?: ReadonlyArray<NodeId>;
|
|
924
|
+
pivot?: {
|
|
925
|
+
x: number;
|
|
926
|
+
y: number;
|
|
927
|
+
};
|
|
928
|
+
}): boolean;
|
|
929
|
+
/**
|
|
930
|
+
* Set the absolute rotation of each member to `angle` radians. Computes
|
|
931
|
+
* the per-member delta from each baseline's current rotation. Pivot
|
|
932
|
+
* defaults to union-bbox center. Same refusal semantics as `rotate`.
|
|
933
|
+
* One atomic history step.
|
|
934
|
+
*/
|
|
935
|
+
rotate_to(angle: number, opts?: {
|
|
936
|
+
ids?: ReadonlyArray<NodeId>;
|
|
937
|
+
pivot?: {
|
|
938
|
+
x: number;
|
|
939
|
+
y: number;
|
|
940
|
+
};
|
|
941
|
+
}): boolean;
|
|
942
|
+
/**
|
|
943
|
+
* Collapse each selected member's `transform=` to a single `matrix(...)`
|
|
944
|
+
* token, baking accumulated translates / rotates / scales / skews into
|
|
945
|
+
* the equivalent affine. After flatten, the element's transform list
|
|
946
|
+
* classifies as `mixed` from the parser's view — but `rotate` will then
|
|
947
|
+
* refuse it. The point isn't to enable further rotation; it's to give
|
|
948
|
+
* the user an explicit pre-rotation reset path so accumulated trig
|
|
949
|
+
* drift has a recovery option.
|
|
950
|
+
*
|
|
951
|
+
* Returns `false` when nothing was changed (selection empty or every
|
|
952
|
+
* member already has no transform / a single matrix).
|
|
953
|
+
*/
|
|
954
|
+
flatten_transform(opts?: {
|
|
955
|
+
ids?: ReadonlyArray<NodeId>;
|
|
956
|
+
}): boolean;
|
|
957
|
+
/**
|
|
958
|
+
* Translate each member so its bbox aligns with the requested edge or
|
|
959
|
+
* center of the selection's union bbox. Single atomic history step.
|
|
960
|
+
* Refuses (returns `false`) when fewer than two members have a world-
|
|
961
|
+
* bbox available — alignment with a single reference is undefined here
|
|
962
|
+
* (target-to-canvas / target-to-parent semantics are not yet designed).
|
|
963
|
+
*/
|
|
964
|
+
align(direction: AlignDirection, opts?: {
|
|
965
|
+
ids?: ReadonlyArray<NodeId>;
|
|
966
|
+
}): boolean;
|
|
967
|
+
reorder(direction: ReorderDirection): void;
|
|
968
|
+
remove(): void;
|
|
969
|
+
/**
|
|
970
|
+
* Wrap the current selection in a new plain `<g>`. Returns `true` if
|
|
971
|
+
* the wrap was performed (a history step was pushed and the new group
|
|
972
|
+
* is the active selection); `false` if the policy in `GROUPING.md`
|
|
973
|
+
* rejected the call.
|
|
974
|
+
*/
|
|
975
|
+
group(): boolean;
|
|
976
|
+
/**
|
|
977
|
+
* Atomic one-shot insertion. Creates a new element of the given SVG
|
|
978
|
+
* tag with the supplied attributes (merged on top of the package's
|
|
979
|
+
* default paint attrs for `rect` / `ellipse` / `line`), inserts it at
|
|
980
|
+
* the given parent (default: root), and selects it. One undo step.
|
|
981
|
+
* Returns the new node id.
|
|
982
|
+
*
|
|
983
|
+
* Use this for paste, programmatic creation, and any non-pointer
|
|
984
|
+
* insertion path. The DOM surface's drag-to-size gesture uses
|
|
985
|
+
* `insert_preview` instead so it can bracket per-frame attr writes.
|
|
986
|
+
*/
|
|
987
|
+
insert(tag: string, attrs: Readonly<Record<string, string>>, opts?: {
|
|
988
|
+
parent?: NodeId;
|
|
989
|
+
index?: number;
|
|
990
|
+
select?: boolean;
|
|
991
|
+
}): NodeId;
|
|
992
|
+
/**
|
|
993
|
+
* Preview-bracketed insertion for drag-to-size gestures. Creates and
|
|
994
|
+
* inserts the node immediately (so HUD selection chrome renders);
|
|
995
|
+
* per-frame `update(attrs)` writes geometry; `commit()` collapses the
|
|
996
|
+
* gesture into one undo step; `discard()` rolls back as if the gesture
|
|
997
|
+
* never happened.
|
|
998
|
+
*/
|
|
999
|
+
insert_preview(tag: string, initial: Readonly<Record<string, string>>, opts?: {
|
|
1000
|
+
parent?: NodeId;
|
|
1001
|
+
index?: number;
|
|
1002
|
+
}): InsertPreviewSession;
|
|
1003
|
+
set_text(value: string): void;
|
|
1004
|
+
load_svg(svg: string): void;
|
|
1005
|
+
serialize_svg(): string;
|
|
1006
|
+
undo(): void;
|
|
1007
|
+
redo(): void;
|
|
1008
|
+
/**
|
|
1009
|
+
* Register a command handler under a stable id. Returns an unregister
|
|
1010
|
+
* function. Re-registering the same id replaces the previous handler.
|
|
1011
|
+
*
|
|
1012
|
+
* Handlers return `true` if they consumed the invocation; `false` or
|
|
1013
|
+
* `undefined` signal "did not apply" — the keymap dispatcher will try
|
|
1014
|
+
* the next candidate in the chain.
|
|
1015
|
+
*/
|
|
1016
|
+
register(id: CommandId, handler: CommandHandler): () => void;
|
|
1017
|
+
/**
|
|
1018
|
+
* Invoke a registered command by id. Returns `true` if a handler
|
|
1019
|
+
* consumed the invocation, `false` otherwise (including unknown ids).
|
|
1020
|
+
*/
|
|
1021
|
+
invoke(id: CommandId, args?: unknown): boolean; /** Whether an id has a registered handler. */
|
|
1022
|
+
has(id: CommandId): boolean;
|
|
1023
|
+
};
|
|
1024
|
+
declare function createSvgEditor(opts: CreateSvgEditorOptions): {
|
|
1025
|
+
/**
|
|
1026
|
+
* Low-level IR handle. Mutating directly bypasses history; prefer
|
|
1027
|
+
* `editor.commands` for app code.
|
|
1028
|
+
*/
|
|
1029
|
+
document: SvgDocument;
|
|
1030
|
+
readonly state: EditorState;
|
|
1031
|
+
subscribe: (fn: (state: EditorState) => void) => Unsubscribe;
|
|
1032
|
+
subscribe_with_selector: <T>(selector: (s: EditorState) => T, fn: (value: T, prev: T) => void, equals?: (a: T, b: T) => boolean) => Unsubscribe;
|
|
1033
|
+
node_properties: (id: NodeId, names: ReadonlyArray<string>) => {
|
|
1034
|
+
readonly [name: string]: PropertyValue;
|
|
1035
|
+
};
|
|
1036
|
+
node_paint: (id: NodeId, channel: "fill" | "stroke") => PaintValue;
|
|
1037
|
+
dom_computed_property: (id: NodeId, name: string) => string | null;
|
|
1038
|
+
dom_computed_paint: (id: NodeId, channel: "fill" | "stroke") => DomComputedPaint | null;
|
|
1039
|
+
/**
|
|
1040
|
+
* Enter content-edit mode on a `<text>` node. Returns `false` (no-op)
|
|
1041
|
+
* when no DOM surface is attached.
|
|
1042
|
+
*/
|
|
1043
|
+
enter_content_edit: (target?: NodeId) => boolean;
|
|
1044
|
+
defs: Defs;
|
|
1045
|
+
commands: Commands;
|
|
1046
|
+
/**
|
|
1047
|
+
* Human-readable label for hierarchy panels. SVG has no native "name";
|
|
1048
|
+
* this is the package's single source of truth so panels don't reinvent
|
|
1049
|
+
* the rule.
|
|
1050
|
+
*
|
|
1051
|
+
* Rule:
|
|
1052
|
+
* - `<text>` → text content, whitespace-collapsed and truncated at
|
|
1053
|
+
* ~40 chars (falls back to `"text"` for empty content).
|
|
1054
|
+
* - Otherwise → tag name, suffixed with `#id` when the `id` attribute
|
|
1055
|
+
* is present (e.g. `"rect #sun"`).
|
|
1056
|
+
*
|
|
1057
|
+
* `opts.tagLabel` lets callers substitute a friendlier or localized
|
|
1058
|
+
* term for the raw tag (e.g. `"rect"` → `"Rectangle"`). Only invoked
|
|
1059
|
+
* on the non-text branch.
|
|
1060
|
+
*/
|
|
1061
|
+
display_label(id: NodeId, opts?: {
|
|
1062
|
+
tagLabel?: (tag: string) => string;
|
|
1063
|
+
}): string;
|
|
1064
|
+
tree(): {
|
|
1065
|
+
root: NodeId;
|
|
1066
|
+
nodes: ReadonlyMap<NodeId, {
|
|
1067
|
+
id: NodeId;
|
|
1068
|
+
tag: string;
|
|
1069
|
+
name?: string;
|
|
1070
|
+
parent: NodeId | null;
|
|
1071
|
+
children: ReadonlyArray<NodeId>;
|
|
1072
|
+
}>;
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* The effective hover from the attached HUD surface — what's under the
|
|
1076
|
+
* pointer, OR whatever `set_surface_hover_override` last pushed. Used
|
|
1077
|
+
* by out-of-canvas UI (layers panel, breadcrumbs) to mirror the canvas
|
|
1078
|
+
* highlight. Returns `null` when nothing is hovered.
|
|
1079
|
+
*/
|
|
1080
|
+
surface_hover(): NodeId | null;
|
|
1081
|
+
/**
|
|
1082
|
+
* Push a hover override into the HUD surface — e.g. when the user
|
|
1083
|
+
* hovers a row in a layers panel. The HUD will render the override's
|
|
1084
|
+
* outline and (when applicable) drive measurement to that node.
|
|
1085
|
+
* Pass `null` to clear and let the pointer pick take over again.
|
|
1086
|
+
*/
|
|
1087
|
+
set_surface_hover_override(id: NodeId | null): void;
|
|
1088
|
+
/**
|
|
1089
|
+
* Subscribe to changes in the effective surface hover. Fires when the
|
|
1090
|
+
* HUD reports a new pointer pick AND when an override is set/cleared.
|
|
1091
|
+
* Cheap channel — does NOT bump `state.version`.
|
|
1092
|
+
*/
|
|
1093
|
+
subscribe_surface_hover(cb: () => void): () => void;
|
|
1094
|
+
/**
|
|
1095
|
+
* Subscribe to bounds-affecting changes. Fires when any document
|
|
1096
|
+
* mutation advances `state.geometry_version` — drag, resize, text
|
|
1097
|
+
* edit, structural insert/remove. Skips presentation-only writes
|
|
1098
|
+
* (fill, opacity, stroke-color).
|
|
1099
|
+
*/
|
|
1100
|
+
subscribe_geometry(cb: () => void): () => void;
|
|
1101
|
+
/**
|
|
1102
|
+
* World-space geometry queries. Non-null when a DOM surface is
|
|
1103
|
+
* attached; null otherwise (queries need a renderer to read bbox
|
|
1104
|
+
* from). Read-only — never mutates document state.
|
|
1105
|
+
*/
|
|
1106
|
+
readonly geometry: GeometryProvider | null;
|
|
1107
|
+
modes: readonly Mode[]; /** Switch the active tool. No history entry; bumps `state.version`. */
|
|
1108
|
+
set_tool: (next: Tool) => void;
|
|
1109
|
+
readonly style: Readonly<EditorStyle>;
|
|
1110
|
+
set_style: (partial: Partial<EditorStyle>) => void;
|
|
1111
|
+
load: (svg: string) => void;
|
|
1112
|
+
serialize: () => string;
|
|
1113
|
+
reset: () => void;
|
|
1114
|
+
attach: (surface: Surface) => SurfaceHandle;
|
|
1115
|
+
detach: () => void;
|
|
1116
|
+
dispose: () => void;
|
|
1117
|
+
providers: Providers;
|
|
1118
|
+
_internal: {
|
|
1119
|
+
doc: SvgDocument;
|
|
1120
|
+
history: {
|
|
1121
|
+
preview: (label: string) => _$_grida_history0.Preview;
|
|
1122
|
+
};
|
|
1123
|
+
emit: () => void;
|
|
1124
|
+
/** Fires after a drag-commit (via orchestrator), `commands.nudge`, or
|
|
1125
|
+
* `commands.translate`. The nudge-dwell watcher subscribes here. */
|
|
1126
|
+
subscribe_translate_commit(cb: () => void): () => void; /** Drag-commit publisher; nudge/translate commands publish directly. */
|
|
1127
|
+
notify_translate_commit: () => void;
|
|
1128
|
+
set_content_edit_driver(fn: ((target: NodeId) => boolean) | null): void;
|
|
1129
|
+
/** Fires the driver immediately with the current override so the
|
|
1130
|
+
* surface can sync state on attach. */
|
|
1131
|
+
set_surface_hover_override_driver(fn: ((id: NodeId | null) => void) | null): void;
|
|
1132
|
+
push_surface_hover(id: NodeId | null): void;
|
|
1133
|
+
set_computed_resolver(fn: DomComputedResolver | null): void; /** Surface registers its geometry provider on attach; clears on detach. */
|
|
1134
|
+
set_geometry(p: GeometryProvider | null): void;
|
|
1135
|
+
};
|
|
1136
|
+
keymap: Keymap;
|
|
1137
|
+
};
|
|
1138
|
+
//#endregion
|
|
1139
|
+
export { GradientEntry as A, PaintPreviewSession as B, Color as C, FileIOProvider as D, EditorStyle as E, LinearGradientDefinition as F, Providers as G, PreviewSession as H, Mode as I, ReorderDirection as J, RadialGradientDefinition as K, NodeId as L, InsertPreviewSession as M, InsertableTag as N, FontResolver as O, InvalidComputedValue as P, Vec2 as Q, Paint as R, ClipboardProvider as S, EditorState as T, PropertyValue as U, PaintValue as V, Provenance as W, Tool as X, TOOL_CURSOR as Y, Unsubscribe as Z, AlignDirection as _, SelectMode as a, CameraConstraints as b, SvgEditor as c, GestureContext as d, GestureId as f, MemoizedGeometryProvider as g, GeometrySignals as h, DomComputedResolver as i, GradientStop as j, GradientDefinition as k, createSvgEditor as l, GeometryProvider as m, CreateSvgEditorOptions as n, Surface as o, Gestures as p, Rect as q, DomComputedPaint as r, SurfaceHandle as s, Commands as t, GestureBinding as u, BoundsResolver as v, DEFAULT_STYLE as w, CameraOptions as x, Camera as y, PaintFallback as z };
|