@grida/svg-editor 1.0.0-alpha.15 → 1.0.0-alpha.17

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.
@@ -1,4 +1,5 @@
1
1
  import cmath from "@grida/cmath";
2
+ import { SVG_NS, XLINK_NS as XLINK_NS$1, XMLNS_NS, encode_attr_value, encode_text, parse_svg } from "@grida/svg/parser";
2
3
  import { svg_parse } from "@grida/svg/parse";
3
4
  import { SVGPathData, SVGPathDataTransformer, encodeSVGPath } from "@grida/svg/pathdata";
4
5
  import vn from "@grida/vn";
@@ -26,75 +27,1126 @@ function array_shallow_equal(a, b) {
26
27
  return true;
27
28
  }
28
29
  //#endregion
29
- //#region src/core/group.ts
30
- let group;
31
- (function(_group) {
32
- const STRUCTURAL_GRAPHICS = _group.STRUCTURAL_GRAPHICS = new Set([
33
- "g",
34
- "defs",
35
- "svg",
36
- "use",
37
- "image",
38
- "switch",
39
- "foreignObject",
40
- "path",
41
- "rect",
42
- "circle",
43
- "ellipse",
44
- "line",
45
- "polyline",
46
- "polygon",
47
- "text",
48
- "a"
49
- ]);
50
- const CONSTRAINED_PARENT = _group.CONSTRAINED_PARENT = new Set([
51
- "text",
52
- "tspan",
53
- "defs",
54
- "clipPath",
55
- "mask",
56
- "pattern",
57
- "marker",
58
- "symbol",
59
- "filter",
60
- "linearGradient",
61
- "radialGradient",
62
- "animateMotion",
63
- "switch"
64
- ]);
65
- function plan(doc, ids) {
66
- if (ids.length === 0) return null;
67
- const parent = doc.parent_of(ids[0]);
30
+ //#region src/core/document.ts
31
+ /** The native vector tags `retype_to_path` can re-type, keyed by tag → the
32
+ * native geometry attributes it consumes (so no orphaned geometry attr
33
+ * survives on the resulting `<path>`). Covers the geometry primitives
34
+ * (rect / circle / ellipse — always re-typed) and the vertex tags (line /
35
+ * polyline / polygon — re-typed only when an edit escapes their native
36
+ * form). */
37
+ const RETYPABLE_GEOMETRY_ATTRS = {
38
+ line: new Set([
39
+ "x1",
40
+ "y1",
41
+ "x2",
42
+ "y2"
43
+ ]),
44
+ polyline: new Set(["points"]),
45
+ polygon: new Set(["points"]),
46
+ rect: new Set([
47
+ "x",
48
+ "y",
49
+ "width",
50
+ "height",
51
+ "rx",
52
+ "ry"
53
+ ]),
54
+ circle: new Set([
55
+ "cx",
56
+ "cy",
57
+ "r"
58
+ ]),
59
+ ellipse: new Set([
60
+ "cx",
61
+ "cy",
62
+ "rx",
63
+ "ry"
64
+ ])
65
+ };
66
+ /**
67
+ * Parse a single SVG length attribute as a plain user-unit number. Returns
68
+ * `null` for absent, non-finite, or unit/percentage values (`50%`, `5px`,
69
+ * `5em`) — those are an out-of-scope geometry gap, and refusing them here
70
+ * means the editor never offers a promotion it cannot perform faithfully.
71
+ */
72
+ function parse_user_unit(raw) {
73
+ if (raw === null) return null;
74
+ const s = raw.trim();
75
+ if (s === "") return null;
76
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) return null;
77
+ const n = Number(s);
78
+ return Number.isFinite(n) ? n : null;
79
+ }
80
+ /**
81
+ * Attribute names whose writes can shift a node's rendered bounds.
82
+ * Membership drives `_geometry_version` bumps in `set_attr`. Only
83
+ * non-namespaced attribute names — namespaced writes (xlink:href, etc.)
84
+ * never bump because they're references, not geometry.
85
+ *
86
+ * Includes text-shaping attributes (font-*) because they re-shape glyph
87
+ * runs and change `<text>` bbox.
88
+ */
89
+ const GEOMETRY_ATTRS = new Set([
90
+ "x",
91
+ "y",
92
+ "x1",
93
+ "y1",
94
+ "x2",
95
+ "y2",
96
+ "cx",
97
+ "cy",
98
+ "width",
99
+ "height",
100
+ "r",
101
+ "rx",
102
+ "ry",
103
+ "points",
104
+ "d",
105
+ "transform",
106
+ "viewBox",
107
+ "font-size",
108
+ "font-family",
109
+ "font-weight",
110
+ "font-style",
111
+ "text-anchor",
112
+ "dx",
113
+ "dy",
114
+ "rotate",
115
+ "textLength",
116
+ "lengthAdjust",
117
+ "pathLength",
118
+ "marker-start",
119
+ "marker-mid",
120
+ "marker-end"
121
+ ]);
122
+ /** `transform:` CSS property at the start of a declaration list or after `;`. */
123
+ const CSS_TRANSFORM_PROPERTY = /(?:^|;)\s*transform\s*:/i;
124
+ /** Trivia shape for editor-synthesized attribute tokens — parser-authored
125
+ * tokens carry their source trivia verbatim; tokens the editor appends get
126
+ * this one canonical shape (` name="value"`). */
127
+ const SYNTH_ATTR_TRIVIA = {
128
+ pre: " ",
129
+ eq_trivia: "",
130
+ eq_trailing: "",
131
+ quote: "\""
132
+ };
133
+ var SvgDocument = class SvgDocument {
134
+ constructor(svg) {
135
+ this.listeners = /* @__PURE__ */ new Set();
136
+ this._structure_version = 0;
137
+ this._revision = 0;
138
+ this._geometry_version = 0;
139
+ if (typeof svg !== "string") throw new TypeError(`new SvgDocument(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
140
+ this.source = svg;
141
+ const parsed = parse_svg(svg);
142
+ this.original = parsed;
143
+ this.nodes = parsed.nodes;
144
+ this.prolog = parsed.prolog;
145
+ this.epilog = parsed.epilog;
146
+ this.root = parsed.root;
147
+ }
148
+ static parse(svg) {
149
+ return new SvgDocument(svg);
150
+ }
151
+ /** Reload from the original parse, discarding all edits. */
152
+ reset_to_original() {
153
+ const parsed = parse_svg(this.source);
154
+ this.original = parsed;
155
+ this.nodes = parsed.nodes;
156
+ this.prolog = parsed.prolog;
157
+ this.epilog = parsed.epilog;
158
+ this.root = parsed.root;
159
+ this._structure_version++;
160
+ this._geometry_version++;
161
+ this.emit();
162
+ }
163
+ /** Replace document with new svg source (clears edits + history-owned state). */
164
+ load(svg) {
165
+ if (typeof svg !== "string") throw new TypeError(`SvgDocument.load(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
166
+ this.source = svg;
167
+ const parsed = parse_svg(svg);
168
+ this.original = parsed;
169
+ this.nodes = parsed.nodes;
170
+ this.prolog = parsed.prolog;
171
+ this.epilog = parsed.epilog;
172
+ this.root = parsed.root;
173
+ this._structure_version++;
174
+ this._geometry_version++;
175
+ this.emit();
176
+ }
177
+ on_change(fn) {
178
+ this.listeners.add(fn);
179
+ return () => this.listeners.delete(fn);
180
+ }
181
+ /** See `_structure_version` for what this counter signals. */
182
+ get structure_version() {
183
+ return this._structure_version;
184
+ }
185
+ /**
186
+ * Total mutation counter — advances on EVERY listener-visible mutation
187
+ * (attribute, style, text, topology, load/reset), unlike the selective
188
+ * `structure_version` / `geometry_version` channels. The single
189
+ * edit-version source: anything derived from this document — the
190
+ * editor's `content_version` / `dirty`, memoized reads, a rendered
191
+ * projection — answers "am I current?" by comparing values, with no
192
+ * event-ordering dependence. Advances BEFORE listeners fire, so a
193
+ * read issued from inside a change listener already sees the new
194
+ * value.
195
+ */
196
+ get revision() {
197
+ return this._revision;
198
+ }
199
+ /** See `_geometry_version` for what this counter signals. */
200
+ get geometry_version() {
201
+ return this._geometry_version;
202
+ }
203
+ /**
204
+ * Advance `_geometry_version` by exactly 1 WITHOUT touching the tree,
205
+ * any attribute, `structure_version`, or the `on_change` listeners.
206
+ *
207
+ * The one geometry mutation with no attribute write: a `<text>` /
208
+ * `<tspan>` reflow the IR cannot see — a web font finishing load AFTER
209
+ * the `font-family` / `font-size` write was already serialized. The DOM
210
+ * surface observes the reflow (`document.fonts` `loadingdone`) and asks
211
+ * the geometry channel to advance so the bounds cache re-reads the
212
+ * settled glyph metrics. See ../../docs/geometry.md §Limitations.
213
+ *
214
+ * Deliberately does NOT call `emit()`: this is not a document edit, so
215
+ * `revision` must not advance — no dirty flag, no undo, no render
216
+ * flush. The editor's `_internal.bump_geometry` advances
217
+ * `geometry_version` here and fans out the geometry listeners itself.
218
+ */
219
+ bump_geometry() {
220
+ this._geometry_version++;
221
+ }
222
+ emit() {
223
+ this._revision++;
224
+ for (const fn of this.listeners) fn();
225
+ }
226
+ /** Notify subscribers — for callers that mutate directly via setAttr/etc. */
227
+ notify() {
228
+ this.emit();
229
+ }
230
+ get(id) {
231
+ return this.nodes.get(id) ?? null;
232
+ }
233
+ is_element(id) {
234
+ return this.nodes.get(id)?.kind === "element";
235
+ }
236
+ parent_of(id) {
237
+ return this.nodes.get(id)?.parent ?? null;
238
+ }
239
+ children_of(id) {
240
+ const n = this.nodes.get(id);
241
+ if (!n || n.kind !== "element") return [];
242
+ return n.children;
243
+ }
244
+ /** Element children only — text/comment/cdata filtered out. */
245
+ element_children_of(id) {
246
+ return this.children_of(id).filter((c) => this.is_element(c));
247
+ }
248
+ next_sibling_of(id) {
249
+ const parent = this.parent_of(id);
68
250
  if (parent === null) return null;
69
- for (const id of ids) {
70
- if (doc.parent_of(id) !== parent) return null;
71
- if (!STRUCTURAL_GRAPHICS.has(doc.tag_of(id))) return null;
251
+ const siblings = this.children_of(parent);
252
+ const i = siblings.indexOf(id);
253
+ return i >= 0 && i + 1 < siblings.length ? siblings[i + 1] : null;
254
+ }
255
+ next_element_sibling_of(id) {
256
+ const parent = this.parent_of(id);
257
+ if (parent === null) return null;
258
+ const siblings = this.element_children_of(parent);
259
+ const i = siblings.indexOf(id);
260
+ return i >= 0 && i + 1 < siblings.length ? siblings[i + 1] : null;
261
+ }
262
+ tag_of(id) {
263
+ const n = this.nodes.get(id);
264
+ return n && n.kind === "element" ? n.local : "";
265
+ }
266
+ contains(ancestor, descendant) {
267
+ if (ancestor === descendant) return true;
268
+ let cur = this.parent_of(descendant);
269
+ while (cur !== null) {
270
+ if (cur === ancestor) return true;
271
+ cur = this.parent_of(cur);
72
272
  }
73
- if (CONSTRAINED_PARENT.has(doc.tag_of(parent))) return null;
74
- const siblings = doc.element_children_of(parent);
75
- const sibling_index = /* @__PURE__ */ new Map();
76
- for (let i = 0; i < siblings.length; i++) sibling_index.set(siblings[i], i);
77
- const indices = [];
273
+ return false;
274
+ }
275
+ /**
276
+ * Filter a selection down to its **subtree roots** drop any id whose
277
+ * ancestor is also in the input set.
278
+ *
279
+ * Mirrors `pruneNestedNodes` in the main canvas editor's query module
280
+ * ([editor/grida-canvas/query/index.ts:138](../../../../editor/grida-canvas/query/index.ts)) and shares its UX motivation:
281
+ * when a parent and a descendant are both selected, only the parent
282
+ * should drive multi-node mutations — otherwise the descendant
283
+ * accumulates the transform twice (once via the parent's `transform`,
284
+ * once via its own attribute write). Required for `commands.remove`
285
+ * (avoids re-attaching detached descendants on undo) and any multi-
286
+ * member translate path (avoids 2× drift for the Bar-chart marquee
287
+ * case).
288
+ *
289
+ * Order: preserves the input order for retained ids. Duplicates in
290
+ * the input are not deduplicated — callers are responsible (the
291
+ * editor's `commands.select` already dedupes).
292
+ *
293
+ * Performance: `O(n × depth)`. Builds a `Set` over the input once,
294
+ * then walks each id's ancestor chain at most once. The main editor's
295
+ * version is `O(n² × depth)` (per-pair `isAncestor`) — fine at typical
296
+ * selection sizes (a few dozen), worth winning here for free since
297
+ * `parent_of` is `O(1)` on our parent-map.
298
+ */
299
+ prune_nested_nodes(ids) {
300
+ if (ids.length <= 1) return [...ids];
301
+ const set = new Set(ids);
302
+ const out = [];
78
303
  for (const id of ids) {
79
- const i = sibling_index.get(id);
80
- if (i === void 0) return null;
81
- indices.push(i);
304
+ let nested = false;
305
+ let cur = this.parent_of(id);
306
+ while (cur !== null) {
307
+ if (set.has(cur)) {
308
+ nested = true;
309
+ break;
310
+ }
311
+ cur = this.parent_of(cur);
312
+ }
313
+ if (!nested) out.push(id);
82
314
  }
83
- const sorted = Array.from(new Set(indices)).sort((a, b) => a - b);
84
- for (let i = 1; i < sorted.length; i++) if (sorted[i] !== sorted[i - 1] + 1) return null;
85
- const children = sorted.map((i) => siblings[i]);
86
- const last_index = sorted[sorted.length - 1];
87
- const original_positions = /* @__PURE__ */ new Map();
88
- for (const i of sorted) original_positions.set(siblings[i], siblings[i + 1] ?? null);
315
+ return out;
316
+ }
317
+ all_nodes() {
318
+ const out = [];
319
+ const walk = (id) => {
320
+ out.push(id);
321
+ const c = this.children_of(id);
322
+ for (const ch of c) walk(ch);
323
+ };
324
+ walk(this.root);
325
+ return out;
326
+ }
327
+ all_elements() {
328
+ return this.all_nodes().filter((id) => this.is_element(id));
329
+ }
330
+ find_by_tag(ancestor, tag) {
331
+ const out = [];
332
+ const walk = (id) => {
333
+ if (id !== ancestor && this.is_element(id) && this.tag_of(id) === tag) out.push(id);
334
+ for (const c of this.children_of(id)) walk(c);
335
+ };
336
+ walk(ancestor);
337
+ return out;
338
+ }
339
+ /** Read attribute by local name, optionally namespace-filtered. */
340
+ get_attr(id, name, ns = null) {
341
+ const n = this.nodes.get(id);
342
+ if (!n || n.kind !== "element") return null;
343
+ for (const a of n.attrs) if (a.local === name && (ns === null || a.ns === ns)) return a.value;
344
+ return null;
345
+ }
346
+ /**
347
+ * Set / remove an attribute. If the attribute exists, it is mutated in place
348
+ * (preserving source position). If it doesn't, it's appended.
349
+ */
350
+ set_attr(id, name, value, ns = null) {
351
+ const n = this.nodes.get(id);
352
+ if (!n || n.kind !== "element") return;
353
+ const structural = name === "id";
354
+ const geometry = ns === null && GEOMETRY_ATTRS.has(name);
355
+ for (let i = 0; i < n.attrs.length; i++) {
356
+ const a = n.attrs[i];
357
+ if (a.local === name && (ns === null || a.ns === ns)) {
358
+ if (value === null) n.attrs.splice(i, 1);
359
+ else a.value = value;
360
+ if (structural) this._structure_version++;
361
+ if (geometry) this._geometry_version++;
362
+ this.emit();
363
+ return;
364
+ }
365
+ }
366
+ if (value !== null) {
367
+ const prefix = ns === XLINK_NS$1 ? "xlink" : ns === XMLNS_NS ? "xmlns" : null;
368
+ n.attrs.push({
369
+ raw_name: prefix ? `${prefix}:${name}` : name,
370
+ prefix,
371
+ local: name,
372
+ ns,
373
+ value,
374
+ ...SYNTH_ATTR_TRIVIA
375
+ });
376
+ if (structural) this._structure_version++;
377
+ if (geometry) this._geometry_version++;
378
+ this.emit();
379
+ }
380
+ }
381
+ attributes_of(id) {
382
+ const n = this.nodes.get(id);
383
+ if (!n || n.kind !== "element") return [];
384
+ return n.attrs.map((a) => ({
385
+ name: a.local,
386
+ ns: a.ns,
387
+ value: a.value
388
+ }));
389
+ }
390
+ get_style(id, property) {
391
+ const style = this.get_attr(id, "style");
392
+ if (!style) return null;
393
+ const decls = parse_inline_style(style);
394
+ for (const d of decls) if (d.property === property) return d.value;
395
+ return null;
396
+ }
397
+ set_style(id, property, value) {
398
+ const decls = parse_inline_style(this.get_attr(id, "style") ?? "");
399
+ const idx = decls.findIndex((d) => d.property === property);
400
+ if (value === null) {
401
+ if (idx === -1) return;
402
+ decls.splice(idx, 1);
403
+ } else if (idx === -1) decls.push({
404
+ property,
405
+ value
406
+ });
407
+ else decls[idx].value = value;
408
+ const next = decls.map((d) => `${d.property}: ${d.value}`).join("; ");
409
+ this.set_attr(id, "style", next === "" ? null : next);
410
+ }
411
+ get_all_styles(id) {
412
+ const style = this.get_attr(id, "style");
413
+ if (!style) return [];
414
+ return parse_inline_style(style);
415
+ }
416
+ /**
417
+ * Whether `id` can be opened in the flat-string text editor.
418
+ *
419
+ * v1 contract: the editor only operates on a *single flat text run*. That
420
+ * means the target must be a `<text>` or `<tspan>` whose direct children
421
+ * are all text nodes (or it has no children). A `<text>` containing a
422
+ * `<tspan>` is *not* honestly editable — `text_of` would drop the tspan
423
+ * content from the editor's view, and a flat-text write would leave the
424
+ * tspan dangling. Tspan-as-target is fine and well-defined when it's a
425
+ * leaf; only the host decides whether to route double-click to a tspan
426
+ * or its parent text.
427
+ */
428
+ is_text_edit_target(id) {
429
+ const n = this.nodes.get(id);
430
+ if (!n || n.kind !== "element") return false;
431
+ if (n.local !== "text" && n.local !== "tspan") return false;
432
+ for (const c of n.children) if (this.nodes.get(c)?.kind !== "text") return false;
433
+ return true;
434
+ }
435
+ /**
436
+ * Returns a tag-discriminated snapshot of the authored geometry attrs
437
+ * if this node is eligible for vector (vertex) editing — else `null`.
438
+ *
439
+ * Eligibility:
440
+ * - `<path>` — requires non-empty `d`.
441
+ * - `<line>` — requires two distinct finite user-unit endpoints.
442
+ * - `<polyline>` — requires `points` parseable to ≥ 2 vertices.
443
+ * - `<polygon>` — same as polyline.
444
+ * - `<rect>` — requires finite user-unit `width`/`height` > 0.
445
+ * - `<circle>` — requires finite user-unit `r` > 0.
446
+ * - `<ellipse>` — requires finite user-unit `rx`/`ry` > 0.
447
+ *
448
+ * The vertex tags (`line` / `polyline` / `polygon`) write edits back to
449
+ * their native attributes while the geometry stays expressible there; an
450
+ * edit that escapes the native form (a curve, or a topology change that
451
+ * leaves the canonical chain) re-types the element to `<path>`. The
452
+ * geometry primitives (`rect` / `circle` / `ellipse`) have no native
453
+ * vector form, so any vector edit re-types them. In all cases the native
454
+ * tag is preserved byte-for-byte until the first re-typing edit commits
455
+ * (see `retype_to_path`). Design:
456
+ * `docs/wg/feat-svg-editor/promote-to-path.md`.
457
+ *
458
+ * Geometry that is not a plain user-unit number (`%`, `px`, `em`, …) is
459
+ * an out-of-scope gap, so such an element returns `null` rather than
460
+ * advertising an edit the editor cannot perform faithfully.
461
+ *
462
+ * Rejects `<image>` / `<use>` (raster / reference bounding boxes, no
463
+ * editable outline).
464
+ */
465
+ /**
466
+ * Parse an optional SVG geometry coordinate (`x`/`y`, `cx`/`cy`, the line
467
+ * endpoints). An **absent** attribute takes the SVG default (`0`); a
468
+ * **present** attribute that is not a plain user-unit number (`%`, `px`,
469
+ * `em`, …) is out of scope and yields `null` so the caller refuses the
470
+ * element — the same gate required attrs (width / radius) already apply.
471
+ *
472
+ * The absent-vs-present distinction is the point: a bare `?? 0` would
473
+ * silently coerce an authored `x1="5px"` to `0`, then the first native
474
+ * writeback would overwrite that authored value. Refusing keeps the
475
+ * editor from misrepresenting geometry it cannot read faithfully.
476
+ */
477
+ optional_user_unit_coord(id, name) {
478
+ const raw = this.get_attr(id, name);
479
+ if (raw === null) return 0;
480
+ return parse_user_unit(raw);
481
+ }
482
+ is_vector_edit_target(id) {
483
+ const n = this.nodes.get(id);
484
+ if (!n || n.kind !== "element") return null;
485
+ if (RETYPABLE_GEOMETRY_ATTRS[n.local] && n.attrs.some((a) => a.prefix === null && a.ns === null && a.local === "d")) return null;
486
+ switch (n.local) {
487
+ case "path": {
488
+ const d = this.get_attr(id, "d");
489
+ if (d === null || d.trim().length === 0) return null;
490
+ return {
491
+ kind: "path",
492
+ d
493
+ };
494
+ }
495
+ case "line": {
496
+ const x1 = this.optional_user_unit_coord(id, "x1");
497
+ const y1 = this.optional_user_unit_coord(id, "y1");
498
+ const x2 = this.optional_user_unit_coord(id, "x2");
499
+ const y2 = this.optional_user_unit_coord(id, "y2");
500
+ if (x1 === null || y1 === null || x2 === null || y2 === null) return null;
501
+ if (x1 === x2 && y1 === y2) return null;
502
+ return {
503
+ kind: "line",
504
+ x1,
505
+ y1,
506
+ x2,
507
+ y2
508
+ };
509
+ }
510
+ case "polyline":
511
+ case "polygon": {
512
+ const raw = this.get_attr(id, "points") ?? "";
513
+ const parsed = svg_parse.parse_points(raw);
514
+ if (parsed.length < 2) return null;
515
+ const points = parsed.map((p) => [p.x, p.y]);
516
+ return n.local === "polyline" ? {
517
+ kind: "polyline",
518
+ points
519
+ } : {
520
+ kind: "polygon",
521
+ points
522
+ };
523
+ }
524
+ case "rect": {
525
+ const x = this.optional_user_unit_coord(id, "x");
526
+ const y = this.optional_user_unit_coord(id, "y");
527
+ if (x === null || y === null) return null;
528
+ const width = parse_user_unit(this.get_attr(id, "width"));
529
+ const height = parse_user_unit(this.get_attr(id, "height"));
530
+ if (width === null || height === null) return null;
531
+ if (width <= 0 || height <= 0) return null;
532
+ const rx_attr = this.get_attr(id, "rx");
533
+ const ry_attr = this.get_attr(id, "ry");
534
+ const rx_parsed = rx_attr === null ? null : parse_user_unit(rx_attr);
535
+ const ry_parsed = ry_attr === null ? null : parse_user_unit(ry_attr);
536
+ if (rx_attr !== null && rx_parsed === null) return null;
537
+ if (ry_attr !== null && ry_parsed === null) return null;
538
+ let rx = rx_parsed ?? ry_parsed ?? 0;
539
+ let ry = ry_parsed ?? rx_parsed ?? 0;
540
+ rx = Math.max(0, Math.min(rx, width / 2));
541
+ ry = Math.max(0, Math.min(ry, height / 2));
542
+ return {
543
+ kind: "rect",
544
+ x,
545
+ y,
546
+ width,
547
+ height,
548
+ rx,
549
+ ry
550
+ };
551
+ }
552
+ case "circle": {
553
+ const cx = this.optional_user_unit_coord(id, "cx");
554
+ const cy = this.optional_user_unit_coord(id, "cy");
555
+ if (cx === null || cy === null) return null;
556
+ const r = parse_user_unit(this.get_attr(id, "r"));
557
+ if (r === null || r <= 0) return null;
558
+ return {
559
+ kind: "circle",
560
+ cx,
561
+ cy,
562
+ r
563
+ };
564
+ }
565
+ case "ellipse": {
566
+ const cx = this.optional_user_unit_coord(id, "cx");
567
+ const cy = this.optional_user_unit_coord(id, "cy");
568
+ if (cx === null || cy === null) return null;
569
+ const rx = parse_user_unit(this.get_attr(id, "rx"));
570
+ const ry = parse_user_unit(this.get_attr(id, "ry"));
571
+ if (rx === null || ry === null) return null;
572
+ if (rx <= 0 || ry <= 0) return null;
573
+ return {
574
+ kind: "ellipse",
575
+ cx,
576
+ cy,
577
+ rx,
578
+ ry
579
+ };
580
+ }
581
+ default: return null;
582
+ }
583
+ }
584
+ /**
585
+ * Re-type a native vector element (`<line>` / `<polyline>` / `<polygon>` /
586
+ * `<rect>` / `<circle>` / `<ellipse>`) into a `<path>` in place, consuming
587
+ * its native geometry attributes and setting `d`. A structural mutation:
588
+ * this layer executes the re-type; it does not decide when one is
589
+ * warranted.
590
+ *
591
+ * Idempotent: returns `null` if `id` is not currently one of those tags
592
+ * (so it is safe to call repeatedly — once re-typed, e.g. already a
593
+ * `<path>`, further calls are no-ops). Otherwise mutates the node and
594
+ * returns an opaque {@link RetypeRecord} reversal token.
595
+ *
596
+ * Identity, children, `self_closing`, non-geometry attributes, and all
597
+ * source trivia are preserved unchanged — only the tag and the geometry
598
+ * attributes move. Pass the token to {@link revert_retype} to restore
599
+ * the original primitive byte-for-byte.
600
+ *
601
+ * (see test/svg-editor-vector-promote-to-path.md)
602
+ */
603
+ retype_to_path(id, d) {
604
+ const n = this.nodes.get(id);
605
+ if (!n || n.kind !== "element") return null;
606
+ const geom = RETYPABLE_GEOMETRY_ATTRS[n.local];
607
+ if (!geom) return null;
608
+ const prev_local = n.local;
609
+ const prev_raw_tag = n.raw_tag;
610
+ const removed = [];
611
+ for (let i = n.attrs.length - 1; i >= 0; i--) {
612
+ const a = n.attrs[i];
613
+ if (a.prefix === null && a.ns === null && geom.has(a.local)) {
614
+ removed.push({
615
+ index: i,
616
+ token: a
617
+ });
618
+ n.attrs.splice(i, 1);
619
+ }
620
+ }
621
+ removed.reverse();
622
+ n.local = "path";
623
+ n.raw_tag = n.prefix ? `${n.prefix}:path` : "path";
624
+ n.attrs.push({
625
+ raw_name: "d",
626
+ prefix: null,
627
+ local: "d",
628
+ ns: null,
629
+ value: d,
630
+ ...SYNTH_ATTR_TRIVIA
631
+ });
632
+ let added_fill_none = false;
633
+ if (prev_local === "line" && this.get_attr(id, "fill") === null && this.get_style(id, "fill") === null) {
634
+ n.attrs.push({
635
+ raw_name: "fill",
636
+ prefix: null,
637
+ local: "fill",
638
+ ns: null,
639
+ value: "none",
640
+ ...SYNTH_ATTR_TRIVIA
641
+ });
642
+ added_fill_none = true;
643
+ }
644
+ this._structure_version++;
645
+ this._geometry_version++;
646
+ this.emit();
89
647
  return {
90
- parent,
91
- insert_before: siblings[last_index + 1] ?? null,
92
- children,
93
- original_positions
648
+ prev_local,
649
+ prev_raw_tag,
650
+ removed,
651
+ added_fill_none
94
652
  };
95
653
  }
96
- _group.plan = plan;
97
- })(group || (group = {}));
654
+ /**
655
+ * Reverse a {@link retype_to_path}: restore the original tag, remove the
656
+ * `d` attribute the promotion added, and splice the captured geometry
657
+ * attribute tokens back at their original positions (preserving their
658
+ * trivia, so a later `serialize()` is byte-equal to the pre-promotion
659
+ * source).
660
+ */
661
+ revert_retype(id, token) {
662
+ const n = this.nodes.get(id);
663
+ if (!n || n.kind !== "element") return;
664
+ for (let i = n.attrs.length - 1; i >= 0; i--) {
665
+ const a = n.attrs[i];
666
+ if (a.prefix === null && a.ns === null && a.local === "d") {
667
+ n.attrs.splice(i, 1);
668
+ break;
669
+ }
670
+ }
671
+ if (token.added_fill_none) for (let i = n.attrs.length - 1; i >= 0; i--) {
672
+ const a = n.attrs[i];
673
+ if (a.prefix === null && a.ns === null && a.local === "fill") {
674
+ n.attrs.splice(i, 1);
675
+ break;
676
+ }
677
+ }
678
+ n.local = token.prev_local;
679
+ n.raw_tag = token.prev_raw_tag;
680
+ for (const { index, token: t } of token.removed) n.attrs.splice(index, 0, t);
681
+ this._structure_version++;
682
+ this._geometry_version++;
683
+ this.emit();
684
+ }
685
+ /**
686
+ * True iff this `<text>` / `<tspan>` carries a non-empty `rotate=""`
687
+ * per-glyph attribute (which conflicts with element-level rotation).
688
+ */
689
+ has_glyph_rotate(id) {
690
+ const tag = this.tag_of(id);
691
+ if (tag !== "text" && tag !== "tspan") return false;
692
+ const value = this.get_attr(id, "rotate");
693
+ if (value === null) return false;
694
+ return value.trim() !== "";
695
+ }
696
+ /**
697
+ * True iff this element's inline `style=""` declares a `transform:`
698
+ * CSS property (which would shadow the editor's `transform=` writes).
699
+ */
700
+ has_inline_css_transform(id) {
701
+ const style = this.get_attr(id, "style");
702
+ if (!style) return false;
703
+ return CSS_TRANSFORM_PROPERTY.test(style);
704
+ }
705
+ /**
706
+ * True iff this element has a direct `<animateTransform>` child
707
+ * (which produces a time-varying transform invisible to attribute writes).
708
+ * Only direct children are checked — nested cases attach to the nearer ancestor.
709
+ */
710
+ has_animate_transform_child(id) {
711
+ for (const c of this.children_of(id)) {
712
+ const n = this.nodes.get(c);
713
+ if (n?.kind === "element" && n.local === "animateTransform") return true;
714
+ }
715
+ return false;
716
+ }
717
+ text_of(id) {
718
+ const n = this.nodes.get(id);
719
+ if (!n || n.kind !== "element") return "";
720
+ let out = "";
721
+ for (const c of n.children) {
722
+ const cn = this.nodes.get(c);
723
+ if (cn?.kind === "text") out += cn.value;
724
+ }
725
+ return out;
726
+ }
727
+ /** Replace all direct text children with a single text node carrying `value`. */
728
+ set_text(id, value) {
729
+ const n = this.nodes.get(id);
730
+ if (!n || n.kind !== "element") return;
731
+ n.children = n.children.filter((c) => this.nodes.get(c)?.kind !== "text");
732
+ if (value !== "") {
733
+ const text_id = `t${Math.random().toString(36).slice(2, 10)}`;
734
+ const text_node = {
735
+ kind: "text",
736
+ id: text_id,
737
+ parent: id,
738
+ value
739
+ };
740
+ this.nodes.set(text_id, text_node);
741
+ n.children.push(text_id);
742
+ }
743
+ this._structure_version++;
744
+ this._geometry_version++;
745
+ this.emit();
746
+ }
747
+ insert(id, parent, before) {
748
+ const node = this.nodes.get(id);
749
+ const parent_node = this.nodes.get(parent);
750
+ if (!node || !parent_node || parent_node.kind !== "element") return;
751
+ if (node.parent !== null) {
752
+ const old_parent = this.nodes.get(node.parent);
753
+ if (old_parent && old_parent.kind === "element") {
754
+ const i = old_parent.children.indexOf(id);
755
+ if (i >= 0) old_parent.children.splice(i, 1);
756
+ }
757
+ }
758
+ const ix = before === null ? -1 : parent_node.children.indexOf(before);
759
+ if (ix < 0) parent_node.children.push(id);
760
+ else parent_node.children.splice(ix, 0, id);
761
+ node.parent = parent;
762
+ this._structure_version++;
763
+ this._geometry_version++;
764
+ this.emit();
765
+ }
766
+ remove(id) {
767
+ const n = this.nodes.get(id);
768
+ if (!n || n.parent === null) return;
769
+ const parent = this.nodes.get(n.parent);
770
+ if (!parent || parent.kind !== "element") return;
771
+ const i = parent.children.indexOf(id);
772
+ if (i >= 0) parent.children.splice(i, 1);
773
+ n.parent = null;
774
+ this._structure_version++;
775
+ this._geometry_version++;
776
+ this.emit();
777
+ }
778
+ /** Create a new element node and register it (not yet inserted). */
779
+ create_element(local, opts) {
780
+ const id = this.fresh_node_id();
781
+ const prefix = opts?.prefix ?? null;
782
+ const ns = opts?.ns ?? null;
783
+ const node = {
784
+ kind: "element",
785
+ id,
786
+ parent: null,
787
+ raw_tag: prefix ? `${prefix}:${local}` : local,
788
+ prefix,
789
+ local,
790
+ ns,
791
+ attrs: [],
792
+ children: [],
793
+ self_closing: false,
794
+ open_tag_trailing: "",
795
+ close_tag_leading: "",
796
+ close_tag_trailing: ""
797
+ };
798
+ this.nodes.set(id, node);
799
+ return id;
800
+ }
801
+ /** Fresh internal NodeId, guaranteed unique within this document's node
802
+ * map. Shared by `create_element` and fragment adoption — collisions
803
+ * matter for the latter because the parser assigns sequential per-parse
804
+ * ids that a second parse would repeat. */
805
+ fresh_node_id() {
806
+ let id;
807
+ do
808
+ id = `e${Math.random().toString(36).slice(2, 10)}`;
809
+ while (this.nodes.has(id));
810
+ return id;
811
+ }
812
+ /**
813
+ * Parse an SVG **fragment** string and adopt its element subtrees into
814
+ * this document's node store — registered like {@link create_element}
815
+ * but NOT inserted into the tree (no version bump, no emit). Callers
816
+ * attach the returned roots via {@link insert}; the editor's
817
+ * `commands.insert_fragment` is the history-bracketed consumer.
818
+ *
819
+ * Input shapes:
820
+ * - A **bare fragment** — one or more sibling elements
821
+ * (`<path …/><path …/>`, or a single `<g>…</g>`). The top-level
822
+ * elements become the returned roots, in source order.
823
+ * - A **full SVG document** — when the input's only top-level element
824
+ * is an `<svg>`, that element is treated as a document SHELL, not
825
+ * content: its element children become the roots and the shell
826
+ * itself (viewBox, width/height, prolog, doctype) is discarded. Its
827
+ * `xmlns:*` prefix declarations are harvested into `xmlns` so the
828
+ * caller can re-declare prefixes the adopted content still uses.
829
+ * An `<svg>` that appears as one of SEVERAL top-level elements (or
830
+ * anywhere below the top level) is content, adopted as-is.
831
+ *
832
+ * Top-level non-element nodes (whitespace between roots, comments, PIs,
833
+ * doctype) are dropped — adoption takes elements, and the host
834
+ * document's own trivia stays untouched. WITHIN each adopted subtree
835
+ * every byte of source trivia survives verbatim (attribute order, quote
836
+ * styles, whitespace, comments), so the inserted markup serializes back
837
+ * exactly as authored — same rules as the initial parse.
838
+ *
839
+ * Authored `id=""` attributes are adopted verbatim — never rewritten,
840
+ * even when they collide with ids already in the document. Silent id
841
+ * renaming is exactly the proprietary noise this editor refuses (README
842
+ * "What clean means" §3); deduplication belongs to the explicit Tidy
843
+ * command. Internal NodeIds ARE freshly assigned (see
844
+ * {@link fresh_node_id}) so adopted nodes never collide in the id map.
845
+ *
846
+ * Throws `TypeError` on a non-string input and `Error` on markup the
847
+ * parser rejects (unclosed / mismatched tags, malformed attributes). An
848
+ * input with no top-level elements (empty string, whitespace, comments
849
+ * only) returns `{ roots: [], xmlns: [] }`.
850
+ */
851
+ create_fragment(markup) {
852
+ if (typeof markup !== "string") throw new TypeError(`create_fragment(markup) requires a string source, got ${markup === null ? "null" : typeof markup}`);
853
+ const parsed = parse_svg(`<svg xmlns="${SVG_NS}" xmlns:xlink="${XLINK_NS$1}">${markup}</svg>`);
854
+ const wrapper = parsed.nodes.get(parsed.root);
855
+ const element_children = (n) => n.children.map((c) => parsed.nodes.get(c)).filter((cn) => cn?.kind === "element");
856
+ let content = element_children(wrapper);
857
+ const xmlns = [];
858
+ if (content.length === 1 && content[0].local === "svg") {
859
+ const shell = content[0];
860
+ for (const a of shell.attrs) if (a.prefix === "xmlns") xmlns.push({
861
+ prefix: a.local,
862
+ uri: a.value
863
+ });
864
+ content = element_children(shell);
865
+ }
866
+ const roots = [];
867
+ for (const node of content) roots.push(this.adopt_parsed_subtree(node, parsed.nodes, null));
868
+ return {
869
+ roots,
870
+ xmlns
871
+ };
872
+ }
873
+ /**
874
+ * Register `node` and its whole subtree (from a foreign parse) into this
875
+ * document's node map under fresh NodeIds. The parser assigns sequential
876
+ * per-parse ids (`n0`, `n1`, …), so adopting without a remap would
877
+ * collide with this document's own nodes. Children links are rewritten;
878
+ * the subtree root arrives detached (`parent: null`), like
879
+ * `create_element`. Mutates the parsed nodes in place — a parse result
880
+ * is single-use.
881
+ */
882
+ adopt_parsed_subtree(node, source, parent) {
883
+ const id = this.fresh_node_id();
884
+ node.id = id;
885
+ node.parent = parent;
886
+ this.nodes.set(id, node);
887
+ if (node.kind === "element") {
888
+ const parsed_children = node.children;
889
+ node.children = [];
890
+ for (const c of parsed_children) {
891
+ const child = source.get(c);
892
+ if (!child) continue;
893
+ node.children.push(this.adopt_parsed_subtree(child, source, id));
894
+ }
895
+ }
896
+ return id;
897
+ }
898
+ /**
899
+ * Namespace prefixes USED within `id`'s subtree (element tags and
900
+ * attribute names) that are not DECLARED within the subtree itself —
901
+ * i.e. prefixes the subtree borrows from ancestor scope. `xml` and
902
+ * `xmlns` are excluded (bound by the XML spec, never declared).
903
+ * Declaration scoping is honored per use-site: a prefix declared on the
904
+ * using element or any of its ancestors up to (and including) the
905
+ * subtree root counts as declared.
906
+ *
907
+ * Structural fact only — the caller decides what an unbound prefix
908
+ * means (e.g. `commands.insert_fragment` hoists a resolvable
909
+ * declaration onto the document root).
910
+ */
911
+ undeclared_ns_prefixes(id) {
912
+ const out = /* @__PURE__ */ new Set();
913
+ const walk = (nid, declared) => {
914
+ const n = this.nodes.get(nid);
915
+ if (!n || n.kind !== "element") return;
916
+ const scope = new Set(declared);
917
+ for (const a of n.attrs) if (a.prefix === "xmlns") scope.add(a.local);
918
+ const need = (p) => {
919
+ if (p === null || p === "xml" || p === "xmlns") return;
920
+ if (!scope.has(p)) out.add(p);
921
+ };
922
+ need(n.prefix);
923
+ for (const a of n.attrs) if (a.prefix !== "xmlns") need(a.prefix);
924
+ for (const c of n.children) walk(c, scope);
925
+ };
926
+ walk(id, /* @__PURE__ */ new Set());
927
+ return out;
928
+ }
929
+ /**
930
+ * Declare a namespace prefix on the ROOT element: appends
931
+ * `xmlns:<prefix>="<uri>"` when the root doesn't already declare that
932
+ * prefix. An authored declaration always wins — this never rebinds.
933
+ * Policy wrapper over {@link set_attr} in the `XMLNS_NS` space; removal
934
+ * works through `set_attr(root, prefix, null, XMLNS_NS)` as usual.
935
+ */
936
+ declare_xmlns(prefix, uri) {
937
+ if (this.get_attr(this.root, prefix, XMLNS_NS) !== null) return;
938
+ this.set_attr(this.root, prefix, uri, XMLNS_NS);
939
+ }
940
+ serialize() {
941
+ let out = "";
942
+ for (const p of this.prolog) out += this.emit_node(p);
943
+ out += this.emit_node(this.nodes.get(this.root));
944
+ for (const e of this.epilog) out += this.emit_node(e);
945
+ return out;
946
+ }
947
+ /**
948
+ * Serialize a single element's subtree as an SVG **fragment**, using the
949
+ * same trivia-preserving rules as {@link serialize} (attribute order,
950
+ * quote style, whitespace, comments — emitted exactly as authored).
951
+ *
952
+ * This is NOT {@link serialize} scoped to a node — it is a deliberately
953
+ * weaker output (sdk-design D3, asymmetric outputs stay separate):
954
+ *
955
+ * - `serialize()` emits the whole document and carries the P1
956
+ * whole-document round-trip guarantee.
957
+ * - `serialize_node()` emits a fragment and does NOT. Namespace
958
+ * declarations that live on an ancestor (`xmlns:xlink` and friends,
959
+ * normally on the root `<svg>`) are NOT inlined — a node using
960
+ * `xlink:href` serializes without `xmlns:xlink`. The fragment is the
961
+ * element's markup as authored, not a standalone parseable document.
962
+ *
963
+ * Throws on an unknown id, a non-element node, or a node detached from
964
+ * the live tree: the contract is "the markup for a selected element,"
965
+ * selections are always live elements, and a string return of `""` for a
966
+ * bad id would hide consumer bugs. The detached case matters because
967
+ * `remove()` keeps the node in the id map for undo — a stale id from a
968
+ * removed node would otherwise serialize content no longer in the
969
+ * document, silently feeding a consumer deleted markup.
970
+ */
971
+ serialize_node(id) {
972
+ const n = this.nodes.get(id);
973
+ if (!n) throw new Error(`serialize_node: unknown node id ${JSON.stringify(id)}`);
974
+ if (n.kind !== "element") throw new Error(`serialize_node: node ${JSON.stringify(id)} is a ${n.kind} node, not an element`);
975
+ if (!this.contains(this.root, id)) throw new Error(`serialize_node: node ${JSON.stringify(id)} is detached from the current document`);
976
+ return this.emit_node(n);
977
+ }
978
+ emit_node(n) {
979
+ switch (n.kind) {
980
+ case "text": return encode_text(n.value);
981
+ case "comment": return `<!--${n.value}-->`;
982
+ case "cdata": return `<![CDATA[${n.value}]]>`;
983
+ case "pi": {
984
+ const pi = n;
985
+ return `<?${pi.target}${pi.value ? " " + pi.value : ""}?>`;
986
+ }
987
+ case "doctype": return `<!DOCTYPE${n.value}>`;
988
+ case "element": {
989
+ const e = n;
990
+ let s = `<${e.raw_tag}`;
991
+ for (const a of e.attrs) s += this.emit_attr(a);
992
+ if (e.children.length === 0 && e.self_closing) {
993
+ s += `${e.open_tag_trailing}/>`;
994
+ return s;
995
+ }
996
+ s += `${e.open_tag_trailing}>`;
997
+ for (const cid of e.children) {
998
+ const cn = this.nodes.get(cid);
999
+ if (cn) s += this.emit_node(cn);
1000
+ }
1001
+ s += `</${e.close_tag_leading}${e.raw_tag}${e.close_tag_trailing}>`;
1002
+ return s;
1003
+ }
1004
+ }
1005
+ }
1006
+ emit_attr(a) {
1007
+ return `${a.pre}${a.raw_name}${a.eq_trivia}=${a.eq_trailing}${a.quote}${encode_attr_value(a.value, a.quote)}${a.quote}`;
1008
+ }
1009
+ };
1010
+ function parse_inline_style(s) {
1011
+ const out = [];
1012
+ const decls = s.split(";");
1013
+ for (const decl of decls) {
1014
+ const colon = decl.indexOf(":");
1015
+ if (colon === -1) continue;
1016
+ const property = decl.slice(0, colon).trim();
1017
+ const value = decl.slice(colon + 1).trim();
1018
+ if (property) out.push({
1019
+ property,
1020
+ value
1021
+ });
1022
+ }
1023
+ return out;
1024
+ }
1025
+ /**
1026
+ * Namespace prefixes resolvable without a source declaration. ONE table,
1027
+ * shared by the paste-side hoist (`insert_fragment`'s xmlns plan) and the
1028
+ * copy-side shell repair (clipboard payload extraction) — the two sides
1029
+ * form a round-trip and must agree: a prefix only one side knows would
1030
+ * produce payloads the other can't honor.
1031
+ */
1032
+ const WELL_KNOWN_NS_PREFIXES = new Map([["xlink", XLINK_NS$1]]);
1033
+ //#endregion
1034
+ //#region src/core/subtree.ts
1035
+ /**
1036
+ * The selection → subtree algebra the two extraction operations share,
1037
+ * plus the clone-specific member (`clone_plan`).
1038
+ *
1039
+ * The clipboard FRD
1040
+ * ([docs/wg/feat-svg-editor/clipboard.md](../../../../docs/wg/feat-svg-editor/clipboard.md)
1041
+ * §Two extraction operations) names two operations over a normalized
1042
+ * selection: **payload extraction** (copy — `core/clipboard.ts`) and
1043
+ * **subtree clone** (duplicate / clone-drag — this module; design note:
1044
+ * [docs/wg/feat-svg-editor/subtree-clone.md](../../../../docs/wg/feat-svg-editor/subtree-clone.md)).
1045
+ * They share exactly two things — selection normalization
1046
+ * ({@link subtree.normalize_roots}) and verbatim subtree serialization
1047
+ * (`SvgDocument.serialize_node`) — and nothing else.
1048
+ *
1049
+ * Unlike copy's payload, a clone carries **no reference closure and no
1050
+ * namespace shell**: the destination is the source document, every
1051
+ * `url(#…)` / `href` reference still resolves against it, and carrying
1052
+ * definitions would deposit duplicate defs on every duplicate.
1053
+ *
1054
+ * Consumers: `commands.duplicate` (⌘D) and the translate orchestrator's
1055
+ * Alt-drag clone session (gridaco/grida#817).
1056
+ *
1057
+ * Verbatim-id policy: authored `id=""` attributes are cloned verbatim,
1058
+ * NEVER rewritten — same stance as `insert_fragment`. A clone of a node
1059
+ * carrying `id="x"` yields a second `id="x"`; reference resolution follows
1060
+ * the host renderer's first-in-document-order rule (a cloned subtree's
1061
+ * internal self-reference resolves to the ORIGINAL), and deduplication is
1062
+ * the explicit Tidy command's job.
1063
+ */
1064
+ let subtree;
1065
+ (function(_subtree) {
1066
+ function by_document_order(doc) {
1067
+ const index = /* @__PURE__ */ new Map();
1068
+ let i = 0;
1069
+ for (const id of doc.all_nodes()) index.set(id, i++);
1070
+ return (a, b) => (index.get(a) ?? 0) - (index.get(b) ?? 0);
1071
+ }
1072
+ _subtree.by_document_order = by_document_order;
1073
+ function normalize_roots(doc, selection, order) {
1074
+ const live = [...new Set(selection)].filter((id) => doc.is_element(id) && doc.contains(doc.root, id));
1075
+ const roots = doc.prune_nested_nodes(live);
1076
+ if (roots.length > 1) roots.sort(order ?? by_document_order(doc));
1077
+ return roots;
1078
+ }
1079
+ _subtree.normalize_roots = normalize_roots;
1080
+ function clone_plan(doc, selection) {
1081
+ const out = [];
1082
+ for (const origin of normalize_roots(doc, selection)) {
1083
+ const parent = doc.parent_of(origin);
1084
+ if (parent === null) continue;
1085
+ if (doc.tag_of(origin) === "svg") continue;
1086
+ const { roots } = doc.create_fragment(doc.serialize_node(origin));
1087
+ if (roots.length !== 1) throw new Error(`subtree.clone_plan: cloning ${JSON.stringify(origin)} yielded ${roots.length} roots`);
1088
+ out.push({
1089
+ origin,
1090
+ clone: roots[0],
1091
+ parent,
1092
+ before: doc.next_sibling_of(origin)
1093
+ });
1094
+ }
1095
+ return out;
1096
+ }
1097
+ _subtree.clone_plan = clone_plan;
1098
+ function insert_plan(doc, plan) {
1099
+ for (const p of plan) doc.insert(p.clone, p.parent, p.before);
1100
+ }
1101
+ _subtree.insert_plan = insert_plan;
1102
+ function remove_plan(doc, plan) {
1103
+ for (const p of plan) doc.remove(p.clone);
1104
+ }
1105
+ _subtree.remove_plan = remove_plan;
1106
+ /** Per-member rigidity tolerance for the rigid-translate witness in
1107
+ * {@link repeat_delta} — position drift from the shared delta, and
1108
+ * size drift from the origin. Generous against float noise from
1109
+ * re-encoded path data / `getBBox`, far below anything a user would
1110
+ * call a move or a resize. */
1111
+ const REPEAT_RIGID_EPSILON = .01;
1112
+ function repeat_delta(record, targets, bounds_of) {
1113
+ if (record === null) return null;
1114
+ if (record.origins.length === 0) return null;
1115
+ if (record.origins.length !== record.clones.length) return null;
1116
+ if (!array_shallow_equal(record.clones, targets)) return null;
1117
+ const origin_bounds = collect_bounds(record.origins, bounds_of);
1118
+ if (origin_bounds === null) return null;
1119
+ const clone_bounds = collect_bounds(record.clones, bounds_of);
1120
+ if (clone_bounds === null) return null;
1121
+ const a = cmath.rect.union(origin_bounds);
1122
+ const b = cmath.rect.union(clone_bounds);
1123
+ const dx = b.x - a.x;
1124
+ const dy = b.y - a.y;
1125
+ for (let i = 0; i < origin_bounds.length; i++) {
1126
+ const o = origin_bounds[i];
1127
+ const c = clone_bounds[i];
1128
+ if (Math.abs(c.x - o.x - dx) > REPEAT_RIGID_EPSILON || Math.abs(c.y - o.y - dy) > REPEAT_RIGID_EPSILON || Math.abs(c.width - o.width) > REPEAT_RIGID_EPSILON || Math.abs(c.height - o.height) > REPEAT_RIGID_EPSILON) return null;
1129
+ }
1130
+ if (Math.abs(dx) <= REPEAT_RIGID_EPSILON && Math.abs(dy) <= REPEAT_RIGID_EPSILON) return null;
1131
+ return {
1132
+ x: dx,
1133
+ y: dy
1134
+ };
1135
+ }
1136
+ _subtree.repeat_delta = repeat_delta;
1137
+ /** All-or-nothing bounds gather for {@link repeat_delta} — one
1138
+ * unmeasurable member and the record can't witness a rigid
1139
+ * translate. */
1140
+ function collect_bounds(ids, bounds_of) {
1141
+ const out = [];
1142
+ for (const id of ids) {
1143
+ const r = bounds_of(id);
1144
+ if (r === null) return null;
1145
+ out.push(r);
1146
+ }
1147
+ return out;
1148
+ }
1149
+ })(subtree || (subtree = {}));
98
1150
  //#endregion
99
1151
  //#region src/core/transform.ts
100
1152
  let transform;
@@ -332,8 +1384,186 @@ let transform;
332
1384
  } : op));
333
1385
  }
334
1386
  _transform.recompose = recompose;
1387
+ /** Epsilon for the identity-leading-matrix drop. Trig/compose noise on
1388
+ * a flip-then-flip round-trip lands well inside 1e-9; authored SVG
1389
+ * coordinates rarely carry more than 4 significant decimals, so this
1390
+ * never collapses a meaningful matrix. */
1391
+ const IDENTITY_EPSILON = 1e-9;
1392
+ /** A `cmath.Transform` (`[[a,c,e],[b,d,f]]`) as a `matrix` op
1393
+ * (`matrix(a b c d e f)` argument order). */
1394
+ function transform_to_matrix_op(m) {
1395
+ return {
1396
+ type: "matrix",
1397
+ a: m[0][0],
1398
+ b: m[1][0],
1399
+ c: m[0][1],
1400
+ d: m[1][1],
1401
+ e: m[0][2],
1402
+ f: m[1][2]
1403
+ };
1404
+ }
1405
+ function is_identity_matrix(m) {
1406
+ const id = cmath.transform.identity;
1407
+ return Math.abs(m[0][0] - id[0][0]) <= IDENTITY_EPSILON && Math.abs(m[0][1] - id[0][1]) <= IDENTITY_EPSILON && Math.abs(m[0][2] - id[0][2]) <= IDENTITY_EPSILON && Math.abs(m[1][0] - id[1][0]) <= IDENTITY_EPSILON && Math.abs(m[1][1] - id[1][1]) <= IDENTITY_EPSILON && Math.abs(m[1][2] - id[1][2]) <= IDENTITY_EPSILON;
1408
+ }
1409
+ function apply_affine(transform_str, effective) {
1410
+ const ops = parse(transform_str);
1411
+ if (ops === null) return transform_str;
1412
+ const has_leading_matrix = ops.length > 0 && ops[0].type === "matrix";
1413
+ const existing_leading = has_leading_matrix ? op_matrix(ops[0]) : cmath.transform.identity;
1414
+ const rest = has_leading_matrix ? ops.slice(1) : ops;
1415
+ const folded = cmath.transform.multiply(effective, existing_leading);
1416
+ if (is_identity_matrix(folded)) {
1417
+ if (rest.length === 0) return null;
1418
+ return emit(rest);
1419
+ }
1420
+ return emit([transform_to_matrix_op(folded), ...rest]);
1421
+ }
1422
+ _transform.apply_affine = apply_affine;
335
1423
  })(transform || (transform = {}));
336
1424
  //#endregion
1425
+ //#region src/core/group.ts
1426
+ let group;
1427
+ (function(_group) {
1428
+ const STRUCTURAL_GRAPHICS = _group.STRUCTURAL_GRAPHICS = new Set([
1429
+ "g",
1430
+ "defs",
1431
+ "svg",
1432
+ "use",
1433
+ "image",
1434
+ "switch",
1435
+ "foreignObject",
1436
+ "path",
1437
+ "rect",
1438
+ "circle",
1439
+ "ellipse",
1440
+ "line",
1441
+ "polyline",
1442
+ "polygon",
1443
+ "text",
1444
+ "a"
1445
+ ]);
1446
+ const CONSTRAINED_PARENT = _group.CONSTRAINED_PARENT = new Set([
1447
+ "text",
1448
+ "tspan",
1449
+ "defs",
1450
+ "clipPath",
1451
+ "mask",
1452
+ "pattern",
1453
+ "marker",
1454
+ "symbol",
1455
+ "filter",
1456
+ "linearGradient",
1457
+ "radialGradient",
1458
+ "animateMotion",
1459
+ "switch"
1460
+ ]);
1461
+ function plan(doc, ids) {
1462
+ if (ids.length === 0) return null;
1463
+ const parent = doc.parent_of(ids[0]);
1464
+ if (parent === null) return null;
1465
+ for (const id of ids) {
1466
+ if (doc.parent_of(id) !== parent) return null;
1467
+ if (!STRUCTURAL_GRAPHICS.has(doc.tag_of(id))) return null;
1468
+ }
1469
+ if (CONSTRAINED_PARENT.has(doc.tag_of(parent))) return null;
1470
+ const siblings = doc.element_children_of(parent);
1471
+ const sibling_index = /* @__PURE__ */ new Map();
1472
+ for (let i = 0; i < siblings.length; i++) sibling_index.set(siblings[i], i);
1473
+ const indices = [];
1474
+ for (const id of ids) {
1475
+ const i = sibling_index.get(id);
1476
+ if (i === void 0) return null;
1477
+ indices.push(i);
1478
+ }
1479
+ const sorted = Array.from(new Set(indices)).sort((a, b) => a - b);
1480
+ for (let i = 1; i < sorted.length; i++) if (sorted[i] !== sorted[i - 1] + 1) return null;
1481
+ const children = sorted.map((i) => siblings[i]);
1482
+ const last_index = sorted[sorted.length - 1];
1483
+ const original_positions = /* @__PURE__ */ new Map();
1484
+ for (const i of sorted) original_positions.set(siblings[i], siblings[i + 1] ?? null);
1485
+ return {
1486
+ parent,
1487
+ insert_before: siblings[last_index + 1] ?? null,
1488
+ children,
1489
+ original_positions
1490
+ };
1491
+ }
1492
+ _group.plan = plan;
1493
+ /**
1494
+ * Own-attribute allowlist for the safe clean-structural ungroup subset.
1495
+ * A group carrying ONLY these attributes is a plain structural wrapper:
1496
+ * dissolving it splices its children into the parent without changing
1497
+ * what renders (modulo baking the `transform`, which `ungroup` does).
1498
+ *
1499
+ * - `transform` — baked into each child by prepending its ops.
1500
+ * - `id` — only allowed when no `<use>` references it (checked below);
1501
+ * the group's own `id` simply disappears (no child inherits it).
1502
+ * - `data-grida-id` — the editor's runtime node identity. Internal; the
1503
+ * group node is removed entirely so the id retires with it.
1504
+ *
1505
+ * ANY other own attribute (`class`, `style`, `opacity`, `fill`,
1506
+ * `stroke`, `filter`, `clip-path`, `mask`, `font-*`, …) carries visual
1507
+ * / cascade / inheritance state that is NOT generally equivalent to the
1508
+ * per-child result of removing the group (TODO §10). Those groups are
1509
+ * refused — never silently mishandled.
1510
+ */
1511
+ const UNGROUP_OWN_ATTR_ALLOWLIST = new Set([
1512
+ "transform",
1513
+ "id",
1514
+ "data-grida-id"
1515
+ ]);
1516
+ /** SVG animation elements (SMIL). A direct animation child targets the
1517
+ * group as its `targetElement`; dissolving the group orphans the
1518
+ * animation. Refuse rather than relocate it. */
1519
+ const ANIMATION_TAGS = new Set([
1520
+ "animate",
1521
+ "animateTransform",
1522
+ "animateMotion",
1523
+ "set"
1524
+ ]);
1525
+ function plan_ungroup(doc, id) {
1526
+ if (doc.tag_of(id) !== "g") return null;
1527
+ const parent = doc.parent_of(id);
1528
+ if (parent === null) return null;
1529
+ {
1530
+ let cur = parent;
1531
+ while (cur !== null) {
1532
+ if (doc.tag_of(cur) === "defs") return null;
1533
+ cur = doc.parent_of(cur);
1534
+ }
1535
+ }
1536
+ const children = doc.element_children_of(id);
1537
+ if (children.length < 1) return null;
1538
+ let has_id = false;
1539
+ for (const a of doc.attributes_of(id)) {
1540
+ if (a.ns !== null) return null;
1541
+ if (!UNGROUP_OWN_ATTR_ALLOWLIST.has(a.name)) return null;
1542
+ if (a.name === "id") has_id = true;
1543
+ }
1544
+ if (has_id) {
1545
+ const own_id = doc.get_attr(id, "id");
1546
+ if (own_id !== null) {
1547
+ const fragment = `#${own_id}`;
1548
+ for (const use_id of doc.find_by_tag(doc.root, "use")) if ((doc.get_attr(use_id, "href") ?? doc.get_attr(use_id, "href", XLINK_NS$1)) === fragment) return null;
1549
+ }
1550
+ }
1551
+ for (const child of children) if (ANIMATION_TAGS.has(doc.tag_of(child))) return null;
1552
+ const group_transform = doc.get_attr(id, "transform");
1553
+ if (group_transform !== null) {
1554
+ if (transform.parse(group_transform) === null) return null;
1555
+ for (const child of children) if (transform.parse(doc.get_attr(child, "transform")) === null) return null;
1556
+ }
1557
+ return {
1558
+ group_id: id,
1559
+ parent,
1560
+ children: [...children],
1561
+ group_transform
1562
+ };
1563
+ }
1564
+ _group.plan_ungroup = plan_ungroup;
1565
+ })(group || (group = {}));
1566
+ //#endregion
337
1567
  //#region src/core/translate-pipeline/translate-pipeline.ts
338
1568
  let translate_pipeline;
339
1569
  (function(_translate_pipeline) {
@@ -684,6 +1914,8 @@ let translate_pipeline;
684
1914
  })(translate_pipeline || (translate_pipeline = {}));
685
1915
  //#endregion
686
1916
  //#region src/core/translate-pipeline/orchestrator.ts
1917
+ const movers = (s) => s.clone?.ids ?? s.ids;
1918
+ const mover_baselines = (s) => s.clone?.baselines ?? s.baselines;
687
1919
  const PROVIDER_ID$2 = "svg-editor";
688
1920
  var TranslateOrchestrator = class {
689
1921
  constructor(deps) {
@@ -706,25 +1938,33 @@ var TranslateOrchestrator = class {
706
1938
  drive(input, modifiers, opts) {
707
1939
  if (this.active === null) this.active = this.open(input.ids, opts.snap, opts.label ?? "move");
708
1940
  const session = this.active;
1941
+ this.reconcile_clone(session, modifiers);
709
1942
  const stages = opts.stages ?? translate_pipeline.stages.DEFAULT;
710
1943
  const result = this.run_pass(session, input.movement, modifiers, opts.policy, stages);
711
1944
  session.last_movement = input.movement;
712
1945
  session.last_policy = opts.policy;
713
1946
  session.last_stages = stages;
714
- this.write_preview_delta(session, result.plan);
1947
+ if (opts.phase === "commit" && session.clone !== null) this.write_commit_composite(session, result.plan);
1948
+ else this.write_preview_delta(session, result.plan);
715
1949
  if (opts.phase === "commit") {
1950
+ const cloned = session.clone;
716
1951
  session.preview.commit();
717
1952
  this.dispose_session();
1953
+ if (cloned !== null) this.deps.on_clone_commit?.({
1954
+ origins: cloned.plan.map((p) => p.origin),
1955
+ clones: cloned.ids
1956
+ });
718
1957
  }
719
1958
  return result;
720
1959
  }
721
1960
  /** Re-run the current preview frame with new modifiers, reusing the
722
1961
  * last-known movement / policy / stages. Used when a modifier key
723
- * changes between pointer-move events (Shift down/up mid-drag).
724
- * No-op when no session is active. */
1962
+ * changes between pointer-move events (Shift down/up mid-drag,
1963
+ * Alt down/up → clone toggle). No-op when no session is active. */
725
1964
  redrive_modifiers(modifiers) {
726
1965
  if (!this.active) return null;
727
1966
  const session = this.active;
1967
+ this.reconcile_clone(session, modifiers);
728
1968
  const result = this.run_pass(session, session.last_movement, modifiers, session.last_policy, session.last_stages);
729
1969
  this.write_preview_delta(session, result.plan);
730
1970
  return result;
@@ -732,15 +1972,22 @@ var TranslateOrchestrator = class {
732
1972
  /** Cancel an in-flight gesture (Escape, programmatic abort). */
733
1973
  cancel() {
734
1974
  if (!this.active) return;
735
- this.active.preview.discard();
1975
+ const session = this.active;
1976
+ session.preview.discard();
1977
+ if (session.clone !== null) {
1978
+ subtree.remove_plan(this.deps.get_doc(), session.clone.plan);
1979
+ this.deps.set_selection?.(session.ids);
1980
+ this.deps.emit();
1981
+ }
736
1982
  this.dispose_session();
737
1983
  }
738
1984
  /** Build a plan + context, run the pipeline, stash guides. Pure
739
- * computation — does not touch the preview. */
1985
+ * computation — does not touch the preview. The context's modifiers
1986
+ * are the pipeline's own vocabulary: stages never see `clone`. */
740
1987
  run_pass(session, movement, modifiers, policy, stages) {
741
1988
  const plan0 = {
742
- ids: session.ids,
743
- baselines: session.baselines,
1989
+ ids: movers(session),
1990
+ baselines: mover_baselines(session),
744
1991
  delta: {
745
1992
  x: 0,
746
1993
  y: 0
@@ -748,7 +1995,7 @@ var TranslateOrchestrator = class {
748
1995
  };
749
1996
  const ctx = {
750
1997
  input: {
751
- ids: session.ids,
1998
+ ids: plan0.ids,
752
1999
  movement
753
2000
  },
754
2001
  modifiers,
@@ -760,6 +2007,69 @@ var TranslateOrchestrator = class {
760
2007
  this._last_guides = result.guides;
761
2008
  return result;
762
2009
  }
2010
+ /**
2011
+ * Clone-modifier state machine (Alt-drag translate-with-clone). Runs
2012
+ * before each pipeline pass so a toggle takes effect on the very frame
2013
+ * it happens — lazy clone on the first frame with the modifier held.
2014
+ *
2015
+ * Both edges and the composite commit below depend on one invariant:
2016
+ * `translate_pipeline.*.revert` writes ABSOLUTE baseline values (incl.
2017
+ * tspan's exact-attr restore), so re-reverting an already-reverted set
2018
+ * is a no-op. A future relative-write baseline kind would break this.
2019
+ */
2020
+ reconcile_clone(session, modifiers) {
2021
+ const want = modifiers.clone === true;
2022
+ const cloned = session.clone !== null;
2023
+ if (want && !cloned) this.enter_clone(session);
2024
+ else if (!want && cloned) this.exit_clone(session);
2025
+ }
2026
+ /** Enter the cloned state: origins back to rest, verbatim clones
2027
+ * inserted next to them, gesture + selection + snap retargeted to the
2028
+ * clones. The origin set stays untouched for the rest of the cloned
2029
+ * gesture — the CLONE moves, the origin stays (Figma convention). */
2030
+ enter_clone(session) {
2031
+ const doc = this.deps.get_doc();
2032
+ translate_pipeline.revert(doc, {
2033
+ ids: session.ids,
2034
+ baselines: session.baselines,
2035
+ delta: {
2036
+ x: 0,
2037
+ y: 0
2038
+ }
2039
+ });
2040
+ const plan = subtree.clone_plan(doc, session.ids);
2041
+ if (plan.length === 0) return;
2042
+ subtree.insert_plan(doc, plan);
2043
+ const baselines = /* @__PURE__ */ new Map();
2044
+ for (const p of plan) baselines.set(p.clone, session.baselines.get(p.origin));
2045
+ session.clone = {
2046
+ plan,
2047
+ ids: plan.map((p) => p.clone),
2048
+ baselines
2049
+ };
2050
+ this.retarget(session, session.clone.ids);
2051
+ }
2052
+ /** Exit the cloned state: clones removed, gesture + selection + snap
2053
+ * retargeted back to the origins, which resume following the cursor
2054
+ * on the next pass. Removed clones stay in the document's id map
2055
+ * (standard removed-node policy) and are never serialized; a stale
2056
+ * preview delta auto-reverting onto them later is harmless. Re-press
2057
+ * = fresh enter with new clones. */
2058
+ exit_clone(session) {
2059
+ subtree.remove_plan(this.deps.get_doc(), session.clone.plan);
2060
+ session.clone = null;
2061
+ this.retarget(session, session.ids);
2062
+ }
2063
+ /** Point selection + snap at the new mover set. Selection + emit run
2064
+ * BEFORE the snap reopen: the surface re-renders on notify, so freshly
2065
+ * inserted clones are measurable when the new snap session captures
2066
+ * its neighborhood (where the origins are legitimate snap targets). */
2067
+ retarget(session, ids) {
2068
+ this.deps.set_selection?.(ids);
2069
+ this.deps.emit();
2070
+ session.snap?.dispose();
2071
+ session.snap = session.snap_requested ? this.deps.open_snap(ids) : null;
2072
+ }
763
2073
  open(ids, snap, label) {
764
2074
  const doc = this.deps.get_doc();
765
2075
  const filtered = doc.prune_nested_nodes(ids);
@@ -767,10 +2077,12 @@ var TranslateOrchestrator = class {
767
2077
  ids: filtered,
768
2078
  baselines: translate_pipeline.intent.capture_baselines(doc, filtered),
769
2079
  snap: snap ? this.deps.open_snap(filtered) : null,
2080
+ snap_requested: snap,
770
2081
  preview: this.deps.open_preview(label),
771
2082
  last_movement: [0, 0],
772
2083
  last_policy: "engine",
773
- last_stages: translate_pipeline.stages.DEFAULT
2084
+ last_stages: translate_pipeline.stages.DEFAULT,
2085
+ clone: null
774
2086
  };
775
2087
  }
776
2088
  /** Bind a fresh apply/revert pair (closure over `plan`) into the
@@ -792,6 +2104,43 @@ var TranslateOrchestrator = class {
792
2104
  }
793
2105
  });
794
2106
  }
2107
+ /** Commit-time delta for a CLONED gesture. Per-frame deltas stay
2108
+ * translate-only; the structural insert happened at the toggle edge,
2109
+ * outside the preview. The one delta that gets committed must own the
2110
+ * whole outcome, so a single undo removes clone + move and a redo
2111
+ * restores both:
2112
+ * apply = insert clones (idempotent reposition when live) +
2113
+ * translate + select clones
2114
+ * revert = un-translate + remove clones + select origins
2115
+ * `preview.set` reverts the previous translate-only delta first
2116
+ * (clones back to rest), then runs this apply — the document passes
2117
+ * through exactly the states the closures describe.
2118
+ * Captures locals only (not the session), so the committed delta
2119
+ * retains the minimum undo/redo needs. */
2120
+ write_commit_composite(session, plan) {
2121
+ const doc = this.deps.get_doc();
2122
+ const emit = this.deps.emit;
2123
+ const project = this.deps.project_delta;
2124
+ const set_selection = this.deps.set_selection;
2125
+ const clone_plan = session.clone.plan;
2126
+ const clone_ids = session.clone.ids;
2127
+ const origin_ids = session.ids;
2128
+ session.preview.set({
2129
+ providerId: PROVIDER_ID$2,
2130
+ apply: () => {
2131
+ subtree.insert_plan(doc, clone_plan);
2132
+ translate_pipeline.apply(doc, plan, project);
2133
+ set_selection?.(clone_ids);
2134
+ emit();
2135
+ },
2136
+ revert: () => {
2137
+ translate_pipeline.revert(doc, plan);
2138
+ subtree.remove_plan(doc, clone_plan);
2139
+ set_selection?.(origin_ids);
2140
+ emit();
2141
+ }
2142
+ });
2143
+ }
795
2144
  dispose_session() {
796
2145
  if (!this.active) return;
797
2146
  this.active.snap?.dispose();
@@ -935,6 +2284,27 @@ let rotate_pipeline;
935
2284
  return { kind: "yes" };
936
2285
  }
937
2286
  _intent.is_rotatable = is_rotatable;
2287
+ function is_transformable(doc, id) {
2288
+ const own_transform = doc.get_attr(id, "transform");
2289
+ if (transform.parse(own_transform) === null) return {
2290
+ kind: "refuse",
2291
+ reason: "non-trivial-transform"
2292
+ };
2293
+ if (doc.has_glyph_rotate(id)) return {
2294
+ kind: "refuse",
2295
+ reason: "text-with-glyph-rotate"
2296
+ };
2297
+ if (doc.has_inline_css_transform(id)) return {
2298
+ kind: "refuse",
2299
+ reason: "css-property-transform"
2300
+ };
2301
+ if (doc.has_animate_transform_child(id)) return {
2302
+ kind: "refuse",
2303
+ reason: "animated-transform"
2304
+ };
2305
+ return { kind: "yes" };
2306
+ }
2307
+ _intent.is_transformable = is_transformable;
938
2308
  function find_op(ops, type) {
939
2309
  for (const op of ops) if (op.type === type) return op;
940
2310
  return null;
@@ -3726,4 +5096,4 @@ function emitWithVerbs(network, meta) {
3726
5096
  return encodeSVGPath(commands);
3727
5097
  }
3728
5098
  //#endregion
3729
- export { is_text_input_focused as _, paint as a, hit_shape_svg as c, NudgeDwellWatcher as d, TranslateOrchestrator as f, array_shallow_equal as g, group as h, TOOL_CURSOR as i, RotateOrchestrator as l, transform as m, insertions as n, ResizeOrchestrator as o, translate_pipeline as p, DEFAULT_STYLE as r, resize_pipeline as s, PathModel as t, rotate_pipeline as u };
5099
+ export { is_text_input_focused as S, SVG_NS as _, paint as a, XMLNS_NS as b, hit_shape_svg as c, NudgeDwellWatcher as d, TranslateOrchestrator as f, subtree as g, transform as h, TOOL_CURSOR as i, RotateOrchestrator as l, group as m, insertions as n, ResizeOrchestrator as o, translate_pipeline as p, DEFAULT_STYLE as r, resize_pipeline as s, PathModel as t, rotate_pipeline as u, SvgDocument as v, array_shallow_equal as x, WELL_KNOWN_NS_PREFIXES as y };