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