@grida/svg-editor 1.0.0-alpha.12 → 1.0.0-alpha.14

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,8 +1,9 @@
1
- import { A as is_text_input_focused, D as parse_transform_list, E as emit_transform_list, _ as apply_translate, a as DEFAULT_STYLE, c as serialize_paint, g as apply_rotate, h as apply_resize, k as plan_group, m as STAGES_NUDGE, o as TOOL_CURSOR, p as prepare_translate_rpc, r as default_paint_attrs, s as parse_paint, u as prepare_rotate_rpc, v as capture_resize_baseline, x as is_resizable, y as capture_translate_baseline } from "./insertions-Okcuo-Ck.mjs";
1
+ import { _ as is_text_input_focused, a as paint, g as array_shallow_equal, h as group, i as TOOL_CURSOR, m as transform, n as insertions, p as translate_pipeline, r as DEFAULT_STYLE, s as resize_pipeline, u as rotate_pipeline } from "./model-DIzZmeyf.mjs";
2
2
  import { HistoryImpl } from "@grida/history";
3
3
  import { KeyCode, M, chunkKey, eventToChunk, getKeyboardOS, kb, keybindingsToKeyCodes } from "@grida/keybinding";
4
4
  import cmath from "@grida/cmath";
5
5
  import { XLINK_NS, encode_attr_value, encode_text, parse_svg } from "@grida/svg/parser";
6
+ import { svg_parse } from "@grida/svg/parse";
6
7
  //#region src/commands/registry.ts
7
8
  var CommandRegistry = class {
8
9
  constructor() {
@@ -118,6 +119,7 @@ function registerDefaultCommands(reg, editor) {
118
119
  if (editor.state.mode !== "select") return false;
119
120
  return editor.commands.align(args);
120
121
  });
122
+ reg.register("content.enter", () => editor.enter_content_edit());
121
123
  reg.register("hierarchy.enter", () => {
122
124
  if (editor.state.selection.length !== 1) return false;
123
125
  const id = editor.state.selection[0];
@@ -142,8 +144,10 @@ function registerDefaultCommands(reg, editor) {
142
144
  return true;
143
145
  });
144
146
  reg.register(TOOL_SET, (args) => {
145
- if (editor.state.mode !== "select") return false;
146
- editor.set_tool(args);
147
+ const next = args;
148
+ const required_mode = next.type === "lasso" || next.type === "bend" ? "edit-content" : next.type === "insert" || next.type === "insert-text" ? "select" : null;
149
+ if (required_mode !== null && editor.state.mode !== required_mode) return false;
150
+ editor.set_tool(next);
147
151
  return true;
148
152
  });
149
153
  }
@@ -163,12 +167,6 @@ function registerDefaultCommands(reg, editor) {
163
167
  * measurement). That stays on the HUD modifiers channel. The keymap
164
168
  * only sees Mod+D-shape chords.
165
169
  */
166
- /** Modifiers that, when held, allow a binding to fire even inside a text input. */
167
- const TEXT_INPUT_SAFE_MODS = new Set([
168
- KeyCode.Meta,
169
- KeyCode.Ctrl,
170
- KeyCode.Alt
171
- ]);
172
170
  var Keymap = class {
173
171
  constructor(commands, platformGetter = getKeyboardOS) {
174
172
  this.commands = commands;
@@ -234,15 +232,16 @@ var Keymap = class {
234
232
  * bar even when the binding's handler rejects.
235
233
  *
236
234
  * Pure read; runs no handlers, no side effects. Honors the same
237
- * text-input-focused guard `dispatch` uses, so a typing user's
238
- * keystroke isn't "claimed" by an unrelated unmodified key.
235
+ * form-element focus guard `dispatch` uses, so a typing user's
236
+ * keystroke isn't "claimed" and the browser's native text-editing
237
+ * default (Cmd+A select all, Cmd+Z undo, etc.) wins.
239
238
  */
240
239
  claims(event) {
241
240
  const chunk = eventToChunk(event);
242
241
  if (chunk.keys.length === 0) return false;
243
242
  const list = this.buckets.get(chunkKey(chunk));
244
243
  if (!list || list.length === 0) return false;
245
- if (is_text_input_focused() && !this.has_safe_mod(chunk.mods)) return false;
244
+ if (is_text_input_focused()) return list.some(({ binding }) => binding.allowInFormElement === true);
246
245
  return true;
247
246
  }
248
247
  /**
@@ -250,6 +249,12 @@ var Keymap = class {
250
249
  * order. Returns `true` on the first handler that consumes; returns
251
250
  * `false` if nothing matched or all matches fell through.
252
251
  *
252
+ * **Form-element focus guard.** When a text input is focused
253
+ * (`<input>`, `<textarea>`, contentEditable), bindings are suppressed
254
+ * by default so the platform's native shortcuts (Cmd+A, Cmd+Z, Cmd+C,
255
+ * arrow nav, …) are preserved. A binding can opt out of this guard
256
+ * with `allowInFormElement: true` — see `KeymapBinding`.
257
+ *
253
258
  * `dispatch` is browser-agnostic: it does NOT call `preventDefault()`
254
259
  * or touch the event in any way. The host decides what to do with the
255
260
  * platform default — typically `if (keymap.claims(e)) e.preventDefault()`,
@@ -264,7 +269,7 @@ var Keymap = class {
264
269
  if (!list || list.length === 0) return false;
265
270
  const text_focused = is_text_input_focused();
266
271
  for (const { binding } of list) {
267
- if (text_focused && !this.has_safe_mod(chunk.mods)) continue;
272
+ if (text_focused && binding.allowInFormElement !== true) continue;
268
273
  if (this.commands.invoke(binding.command, binding.args)) return true;
269
274
  }
270
275
  return false;
@@ -284,10 +289,6 @@ var Keymap = class {
284
289
  }
285
290
  return out;
286
291
  }
287
- has_safe_mod(mods) {
288
- for (const m of mods) if (TEXT_INPUT_SAFE_MODS.has(m)) return true;
289
- return false;
290
- }
291
292
  };
292
293
  function compareEntries(a, b) {
293
294
  const pa = a.binding.priority ?? 0;
@@ -382,6 +383,10 @@ const DEFAULT_BINDINGS = [
382
383
  command: "selection.align",
383
384
  args: "vertical_centers"
384
385
  },
386
+ {
387
+ keybinding: kb(KeyCode.Enter),
388
+ command: "content.enter"
389
+ },
385
390
  {
386
391
  keybinding: kb(KeyCode.Enter),
387
392
  command: "hierarchy.enter"
@@ -483,6 +488,16 @@ const DEFAULT_BINDINGS = [
483
488
  tag: "line"
484
489
  }
485
490
  },
491
+ {
492
+ keybinding: kb(KeyCode.KeyT),
493
+ command: TOOL_SET,
494
+ args: { type: "insert-text" }
495
+ },
496
+ {
497
+ keybinding: kb(KeyCode.KeyQ),
498
+ command: TOOL_SET,
499
+ args: { type: "lasso" }
500
+ },
486
501
  {
487
502
  keybinding: kb(KeyCode.BracketRight),
488
503
  command: "reorder",
@@ -509,14 +524,6 @@ function applyDefaultBindings(keymap) {
509
524
  for (const b of DEFAULT_BINDINGS) keymap.bind(b);
510
525
  }
511
526
  //#endregion
512
- //#region src/util/equal.ts
513
- function array_shallow_equal(a, b) {
514
- if (a === b) return true;
515
- if (a.length !== b.length) return false;
516
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
517
- return true;
518
- }
519
- //#endregion
520
527
  //#region src/core/defs.ts
521
528
  var GradientsRegistry = class {
522
529
  constructor(doc) {
@@ -791,11 +798,14 @@ const GEOMETRY_ATTRS = new Set([
791
798
  "marker-mid",
792
799
  "marker-end"
793
800
  ]);
801
+ /** `transform:` CSS property at the start of a declaration list or after `;`. */
802
+ const CSS_TRANSFORM_PROPERTY = /(?:^|;)\s*transform\s*:/i;
794
803
  var SvgDocument = class SvgDocument {
795
804
  constructor(svg) {
796
805
  this.listeners = /* @__PURE__ */ new Set();
797
806
  this._structure_version = 0;
798
807
  this._geometry_version = 0;
808
+ if (typeof svg !== "string") throw new TypeError(`new SvgDocument(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
799
809
  this.source = svg;
800
810
  const parsed = parse_svg(svg);
801
811
  this.original = parsed;
@@ -821,6 +831,7 @@ var SvgDocument = class SvgDocument {
821
831
  }
822
832
  /** Replace document with new svg source (clears edits + history-owned state). */
823
833
  load(svg) {
834
+ if (typeof svg !== "string") throw new TypeError(`SvgDocument.load(svg) requires a string source, got ${svg === null ? "null" : typeof svg}`);
824
835
  this.source = svg;
825
836
  const parsed = parse_svg(svg);
826
837
  this.original = parsed;
@@ -1058,6 +1069,88 @@ var SvgDocument = class SvgDocument {
1058
1069
  for (const c of n.children) if (this.nodes.get(c)?.kind !== "text") return false;
1059
1070
  return true;
1060
1071
  }
1072
+ /**
1073
+ * Returns a tag-discriminated snapshot of the authored geometry attrs
1074
+ * if this node is eligible for vector (vertex) editing — else `null`.
1075
+ *
1076
+ * v1 eligibility:
1077
+ * - `<path>` — requires non-empty `d`.
1078
+ * - `<polyline>` — requires `points` parseable to ≥ 2 vertices.
1079
+ * - `<polygon>` — same as polyline.
1080
+ *
1081
+ * Deliberately rejects `<line>` in v1: the only useful vertex-edit
1082
+ * gestures on a `<line>` are (a) introducing a new vertex (which would
1083
+ * have to promote it to `<polyline>`) and (b) bending it with a tangent
1084
+ * (which would have to promote it to `<path>`). Both promotions are
1085
+ * out of scope for v1, so opening a `<line>` in vector-edit mode would
1086
+ * advertise capabilities that don't work.
1087
+ *
1088
+ * Also rejects `<rect>`, `<circle>`, `<ellipse>`, `<image>`, `<use>` —
1089
+ * those would force the same promotion-to-`<path>` machinery (trivia
1090
+ * transfer, cross-cutting attr carry, DOM-element swap, history-bracket
1091
+ * changes) that v1 keeps out of scope.
1092
+ */
1093
+ is_vector_edit_target(id) {
1094
+ const n = this.nodes.get(id);
1095
+ if (!n || n.kind !== "element") return null;
1096
+ switch (n.local) {
1097
+ case "path": {
1098
+ const d = this.get_attr(id, "d");
1099
+ if (d === null || d.trim().length === 0) return null;
1100
+ return {
1101
+ kind: "path",
1102
+ d
1103
+ };
1104
+ }
1105
+ case "polyline":
1106
+ case "polygon": {
1107
+ const raw = this.get_attr(id, "points") ?? "";
1108
+ const parsed = svg_parse.parse_points(raw);
1109
+ if (parsed.length < 2) return null;
1110
+ const points = parsed.map((p) => [p.x, p.y]);
1111
+ return n.local === "polyline" ? {
1112
+ kind: "polyline",
1113
+ points
1114
+ } : {
1115
+ kind: "polygon",
1116
+ points
1117
+ };
1118
+ }
1119
+ default: return null;
1120
+ }
1121
+ }
1122
+ /**
1123
+ * True iff this `<text>` / `<tspan>` carries a non-empty `rotate=""`
1124
+ * per-glyph attribute (which conflicts with element-level rotation).
1125
+ */
1126
+ has_glyph_rotate(id) {
1127
+ const tag = this.tag_of(id);
1128
+ if (tag !== "text" && tag !== "tspan") return false;
1129
+ const value = this.get_attr(id, "rotate");
1130
+ if (value === null) return false;
1131
+ return value.trim() !== "";
1132
+ }
1133
+ /**
1134
+ * True iff this element's inline `style=""` declares a `transform:`
1135
+ * CSS property (which would shadow the editor's `transform=` writes).
1136
+ */
1137
+ has_inline_css_transform(id) {
1138
+ const style = this.get_attr(id, "style");
1139
+ if (!style) return false;
1140
+ return CSS_TRANSFORM_PROPERTY.test(style);
1141
+ }
1142
+ /**
1143
+ * True iff this element has a direct `<animateTransform>` child
1144
+ * (which produces a time-varying transform invisible to attribute writes).
1145
+ * Only direct children are checked — nested cases attach to the nearer ancestor.
1146
+ */
1147
+ has_animate_transform_child(id) {
1148
+ for (const c of this.children_of(id)) {
1149
+ const n = this.nodes.get(c);
1150
+ if (n?.kind === "element" && n.local === "animateTransform") return true;
1151
+ }
1152
+ return false;
1153
+ }
1061
1154
  text_of(id) {
1062
1155
  const n = this.nodes.get(id);
1063
1156
  if (!n || n.kind !== "element") return "";
@@ -1248,153 +1341,162 @@ function delta_for(bbox, target, direction) {
1248
1341
  }
1249
1342
  //#endregion
1250
1343
  //#region src/core/properties.ts
1251
- /** SVG properties that inherit per SVG 2 §6 (subset; the common ones). */
1252
- const INHERITED = new Set([
1253
- "color",
1254
- "cursor",
1255
- "direction",
1256
- "fill",
1257
- "fill-opacity",
1258
- "fill-rule",
1259
- "font",
1260
- "font-family",
1261
- "font-size",
1262
- "font-style",
1263
- "font-variant",
1264
- "font-weight",
1265
- "letter-spacing",
1266
- "marker",
1267
- "marker-end",
1268
- "marker-mid",
1269
- "marker-start",
1270
- "paint-order",
1271
- "pointer-events",
1272
- "shape-rendering",
1273
- "stroke",
1274
- "stroke-dasharray",
1275
- "stroke-dashoffset",
1276
- "stroke-linecap",
1277
- "stroke-linejoin",
1278
- "stroke-miterlimit",
1279
- "stroke-opacity",
1280
- "stroke-width",
1281
- "text-anchor",
1282
- "text-rendering",
1283
- "visibility",
1284
- "word-spacing",
1285
- "writing-mode"
1286
- ]);
1287
- /** Initial values for known properties (subset). */
1288
- const INITIAL = {
1289
- fill: "black",
1290
- stroke: "none",
1291
- "fill-opacity": "1",
1292
- "stroke-opacity": "1",
1293
- "stroke-width": "1",
1294
- opacity: "1",
1295
- visibility: "visible",
1296
- display: "inline"
1297
- };
1298
- /**
1299
- * Resolve a property's declared value and its provenance for a single node.
1300
- *
1301
- * The cascade engine here covers what the README says is in scope:
1302
- * presentation attributes + inline style + parent inheritance + initial.
1303
- * `<style>` block matching is deferred.
1304
- */
1305
- function resolve_declared(doc, id, property) {
1306
- const inline = doc.get_style(id, property);
1307
- if (inline !== null && inline !== "") return {
1308
- declared: inline,
1309
- provenance: {
1310
- origin: "author",
1311
- carrier: "inline_style"
1312
- }
1344
+ let properties;
1345
+ (function(_properties) {
1346
+ /** SVG properties that inherit per SVG 2 §6 (subset; the common ones). */
1347
+ const INHERITED = new Set([
1348
+ "color",
1349
+ "cursor",
1350
+ "direction",
1351
+ "fill",
1352
+ "fill-opacity",
1353
+ "fill-rule",
1354
+ "font",
1355
+ "font-family",
1356
+ "font-size",
1357
+ "font-style",
1358
+ "font-variant",
1359
+ "font-weight",
1360
+ "letter-spacing",
1361
+ "marker",
1362
+ "marker-end",
1363
+ "marker-mid",
1364
+ "marker-start",
1365
+ "paint-order",
1366
+ "pointer-events",
1367
+ "shape-rendering",
1368
+ "stroke",
1369
+ "stroke-dasharray",
1370
+ "stroke-dashoffset",
1371
+ "stroke-linecap",
1372
+ "stroke-linejoin",
1373
+ "stroke-miterlimit",
1374
+ "stroke-opacity",
1375
+ "stroke-width",
1376
+ "text-anchor",
1377
+ "text-rendering",
1378
+ "visibility",
1379
+ "word-spacing",
1380
+ "writing-mode"
1381
+ ]);
1382
+ /** Initial values for known properties (subset). */
1383
+ const INITIAL = {
1384
+ fill: "black",
1385
+ stroke: "none",
1386
+ "fill-opacity": "1",
1387
+ "stroke-opacity": "1",
1388
+ "stroke-width": "1",
1389
+ opacity: "1",
1390
+ visibility: "visible",
1391
+ display: "inline"
1313
1392
  };
1314
- const attr = doc.get_attr(id, property);
1315
- if (attr !== null && attr !== "") return {
1316
- declared: attr,
1317
- provenance: {
1318
- origin: "author",
1319
- carrier: "presentation_attribute"
1320
- }
1321
- };
1322
- if (INHERITED.has(property)) {
1323
- const parent = doc.parent_of(id);
1324
- if (parent !== null && doc.is_element(parent)) {
1325
- const r = resolve_declared(doc, parent, property);
1326
- if (r.declared !== null) return {
1327
- declared: r.declared,
1328
- provenance: {
1329
- origin: "author",
1330
- carrier: "inherited"
1331
- }
1332
- };
1393
+ function resolve_declared(doc, id, property) {
1394
+ const inline = doc.get_style(id, property);
1395
+ if (inline !== null && inline !== "") return {
1396
+ declared: inline,
1397
+ provenance: {
1398
+ origin: "author",
1399
+ carrier: "inline_style"
1400
+ }
1401
+ };
1402
+ const attr = doc.get_attr(id, property);
1403
+ if (attr !== null && attr !== "") return {
1404
+ declared: attr,
1405
+ provenance: {
1406
+ origin: "author",
1407
+ carrier: "presentation_attribute"
1408
+ }
1409
+ };
1410
+ if (INHERITED.has(property)) {
1411
+ const parent = doc.parent_of(id);
1412
+ if (parent !== null && doc.is_element(parent)) {
1413
+ const r = resolve_declared(doc, parent, property);
1414
+ if (r.declared !== null) return {
1415
+ declared: r.declared,
1416
+ provenance: {
1417
+ origin: "author",
1418
+ carrier: "inherited"
1419
+ }
1420
+ };
1421
+ }
1333
1422
  }
1423
+ return {
1424
+ declared: INITIAL[property] ?? null,
1425
+ provenance: {
1426
+ origin: "user_agent",
1427
+ carrier: "defaulted"
1428
+ }
1429
+ };
1334
1430
  }
1335
- return {
1336
- declared: INITIAL[property] ?? null,
1337
- provenance: {
1338
- origin: "user_agent",
1339
- carrier: "defaulted"
1340
- }
1341
- };
1342
- }
1343
- /**
1344
- * Type-parsed computed value for known properties. Unknown property names
1345
- * return the declared string as-is.
1346
- */
1347
- function compute_known(property, declared) {
1348
- if (declared === null) return null;
1349
- const trimmed = declared.trim();
1350
- if (trimmed === "inherit" || trimmed === "initial" || trimmed === "unset" || trimmed === "revert" || trimmed === "revert-layer") return null;
1351
- if (/^var\s*\(/i.test(trimmed)) return {
1352
- error: "invalid_at_computed_value_time",
1353
- reason: `var() substitution requires a cascade engine (not implemented)`
1354
- };
1355
- switch (property) {
1356
- case "opacity":
1357
- case "fill-opacity":
1358
- case "stroke-opacity":
1359
- case "stroke-width":
1360
- case "x":
1361
- case "y":
1362
- case "width":
1363
- case "height":
1364
- case "cx":
1365
- case "cy":
1366
- case "r":
1367
- case "rx":
1368
- case "ry":
1369
- case "font-size": {
1370
- const n = parseFloat(trimmed);
1371
- return Number.isFinite(n) ? n : trimmed;
1431
+ _properties.resolve_declared = resolve_declared;
1432
+ function compute_known(property, declared) {
1433
+ if (declared === null) return null;
1434
+ const trimmed = declared.trim();
1435
+ if (trimmed === "inherit" || trimmed === "initial" || trimmed === "unset" || trimmed === "revert" || trimmed === "revert-layer") return null;
1436
+ if (/^var\s*\(/i.test(trimmed)) return {
1437
+ error: "invalid_at_computed_value_time",
1438
+ reason: `var() substitution requires a cascade engine (not implemented)`
1439
+ };
1440
+ switch (property) {
1441
+ case "opacity":
1442
+ case "fill-opacity":
1443
+ case "stroke-opacity":
1444
+ case "stroke-width":
1445
+ case "x":
1446
+ case "y":
1447
+ case "width":
1448
+ case "height":
1449
+ case "cx":
1450
+ case "cy":
1451
+ case "r":
1452
+ case "rx":
1453
+ case "ry":
1454
+ case "font-size": {
1455
+ const n = parseFloat(trimmed);
1456
+ return Number.isFinite(n) ? n : trimmed;
1457
+ }
1458
+ default: return trimmed;
1372
1459
  }
1373
- default: return trimmed;
1374
1460
  }
1375
- }
1376
- function read_property(doc, id, property) {
1377
- const { declared, provenance } = resolve_declared(doc, id, property);
1378
- return {
1379
- declared,
1380
- computed: compute_known(property, declared),
1381
- provenance
1382
- };
1383
- }
1384
- /** Which carrier should a `set_property` write to? Per the README (P1):
1385
- * whichever carrier currently wins the cascade. If nothing wins (defaulted /
1386
- * inherited), write a presentation attribute by default. */
1387
- function choose_write_carrier(doc, id, property) {
1388
- const inline = doc.get_style(id, property);
1389
- if (inline !== null && inline !== "") return "inline_style";
1390
- return "presentation_attribute";
1391
- }
1461
+ _properties.compute_known = compute_known;
1462
+ function read(doc, id, property) {
1463
+ const { declared, provenance } = resolve_declared(doc, id, property);
1464
+ return {
1465
+ declared,
1466
+ computed: compute_known(property, declared),
1467
+ provenance
1468
+ };
1469
+ }
1470
+ _properties.read = read;
1471
+ function choose_write_carrier(doc, id, property) {
1472
+ const inline = doc.get_style(id, property);
1473
+ if (inline !== null && inline !== "") return "inline_style";
1474
+ return "presentation_attribute";
1475
+ }
1476
+ _properties.choose_write_carrier = choose_write_carrier;
1477
+ function value_equals(a, b) {
1478
+ if (a === b) return true;
1479
+ if (a.declared !== b.declared) return false;
1480
+ if (a.provenance.carrier !== b.provenance.carrier) return false;
1481
+ if (a.provenance.origin !== b.provenance.origin) return false;
1482
+ if (a.computed === b.computed) return true;
1483
+ if (a.computed && b.computed && typeof a.computed === "object" && typeof b.computed === "object" && "error" in a.computed && "error" in b.computed) return a.computed.error === b.computed.error && a.computed.reason === b.computed.reason;
1484
+ return false;
1485
+ }
1486
+ _properties.value_equals = value_equals;
1487
+ })(properties || (properties = {}));
1392
1488
  //#endregion
1393
1489
  //#region src/core/editor.ts
1394
1490
  const PROVIDER_ID = "svg-editor";
1395
1491
  /** Max characters in a synthesized display label before truncation. */
1396
1492
  const DISPLAY_LABEL_MAX_LEN = 40;
1397
- function createSvgEditor(opts) {
1493
+ /**
1494
+ * Wide internal factory — returns the full object including the
1495
+ * `_internal` / `keymap` surfaces in its inferred type. Stays private.
1496
+ * The public `createSvgEditor` below wraps this and narrows the return
1497
+ * to `SvgEditor` so the published `.d.ts` doesn't advertise internals.
1498
+ */
1499
+ function _create_svg_editor_internal(opts) {
1398
1500
  const doc = new SvgDocument(opts.svg);
1399
1501
  const history = new HistoryImpl();
1400
1502
  const defs = create_defs(doc);
@@ -1438,6 +1540,7 @@ function createSvgEditor(opts) {
1438
1540
  can_undo: history.stack.canUndo,
1439
1541
  can_redo: history.stack.canRedo,
1440
1542
  version,
1543
+ content_version: doc_version,
1441
1544
  structure_version: doc.structure_version,
1442
1545
  geometry_version: doc.geometry_version,
1443
1546
  load_version
@@ -1526,7 +1629,7 @@ function createSvgEditor(opts) {
1526
1629
  }
1527
1630
  function tools_equal(a, b) {
1528
1631
  if (a.type !== b.type) return false;
1529
- if (a.type === "cursor") return true;
1632
+ if (a.type === "cursor" || a.type === "lasso" || a.type === "bend" || a.type === "insert-text") return true;
1530
1633
  return b.type === "insert" && a.tag === b.tag;
1531
1634
  }
1532
1635
  function set_tool(next) {
@@ -1587,8 +1690,8 @@ function createSvgEditor(opts) {
1587
1690
  const key = `${id}${name}`;
1588
1691
  const cached = property_cache.get(key);
1589
1692
  if (cached && cached.doc_version === doc_version) return cached.value;
1590
- const next = read_property(doc, id, name);
1591
- if (cached && property_value_equals(cached.value, next)) {
1693
+ const next = properties.read(doc, id, name);
1694
+ if (cached && properties.value_equals(cached.value, next)) {
1592
1695
  cached.doc_version = doc_version;
1593
1696
  return cached.value;
1594
1697
  }
@@ -1624,13 +1727,13 @@ function createSvgEditor(opts) {
1624
1727
  const key = `${id}${channel}`;
1625
1728
  const cached = paint_cache.get(key);
1626
1729
  if (cached && cached.doc_version === doc_version) return cached.value;
1627
- const { declared, provenance } = resolve_declared(doc, id, channel);
1730
+ const { declared, provenance } = properties.resolve_declared(doc, id, channel);
1628
1731
  const next = {
1629
1732
  declared,
1630
- computed: parse_paint(declared),
1733
+ computed: paint.parse(declared),
1631
1734
  provenance
1632
1735
  };
1633
- if (cached && paint_value_equals(cached.value, next)) {
1736
+ if (cached && paint.value_equals(cached.value, next)) {
1634
1737
  cached.doc_version = doc_version;
1635
1738
  return cached.value;
1636
1739
  }
@@ -1641,7 +1744,7 @@ function createSvgEditor(opts) {
1641
1744
  return next;
1642
1745
  }
1643
1746
  function write_property(id, name, value) {
1644
- if (choose_write_carrier(doc, id, name) === "inline_style") doc.set_style(id, name, value);
1747
+ if (properties.choose_write_carrier(doc, id, name) === "inline_style") doc.set_style(id, name, value);
1645
1748
  else doc.set_attr(id, name, value);
1646
1749
  }
1647
1750
  function set_property(name, value) {
@@ -1704,14 +1807,14 @@ function createSvgEditor(opts) {
1704
1807
  discard: () => preview.discard()
1705
1808
  };
1706
1809
  }
1707
- function set_paint(channel, paint) {
1810
+ function set_paint(channel, p) {
1708
1811
  if (selection.length === 0) return;
1709
- set_property(channel, serialize_paint(paint));
1812
+ set_property(channel, paint.serialize(p));
1710
1813
  }
1711
1814
  function preview_paint(channel) {
1712
1815
  const session = preview_property(channel);
1713
1816
  return {
1714
- update: (paint) => session.update(serialize_paint(paint)),
1817
+ update: (p) => session.update(paint.serialize(p)),
1715
1818
  commit: () => session.commit(),
1716
1819
  discard: () => session.discard()
1717
1820
  };
@@ -1729,7 +1832,7 @@ function createSvgEditor(opts) {
1729
1832
  function do_translate_oneshot(delta, stages, label) {
1730
1833
  if (selection.length === 0) return false;
1731
1834
  if (delta.dx === 0 && delta.dy === 0) return false;
1732
- const { apply, revert } = prepare_translate_rpc({
1835
+ const { apply, revert } = translate_pipeline.prepare_rpc({
1733
1836
  doc,
1734
1837
  ids: selection,
1735
1838
  delta: {
@@ -1758,7 +1861,7 @@ function createSvgEditor(opts) {
1758
1861
  if (do_translate_oneshot(delta, void 0, "translate")) notify_translate_commit();
1759
1862
  }
1760
1863
  function nudge(delta) {
1761
- if (do_translate_oneshot(delta, STAGES_NUDGE, "nudge")) notify_translate_commit();
1864
+ if (do_translate_oneshot(delta, translate_pipeline.stages.NUDGE, "nudge")) notify_translate_commit();
1762
1865
  }
1763
1866
  /**
1764
1867
  * One-shot multi-member resize to an explicit target rect. Mirrors a
@@ -1783,13 +1886,13 @@ function createSvgEditor(opts) {
1783
1886
  if (!geometry_provider) return false;
1784
1887
  const members = [];
1785
1888
  for (const id of ids) {
1786
- if (!is_resizable(doc.tag_of(id))) continue;
1889
+ if (!resize_pipeline.intent.is_resizable(doc.tag_of(id))) continue;
1787
1890
  const bbox = geometry_provider.bounds_of(id);
1788
1891
  if (!bbox) continue;
1789
1892
  members.push({
1790
1893
  id,
1791
- rz: capture_resize_baseline(doc, id, bbox),
1792
- tx_pre: capture_translate_baseline(doc, id),
1894
+ rz: resize_pipeline.intent.capture_baseline(doc, id, bbox),
1895
+ tx_pre: translate_pipeline.intent.capture_baseline(doc, id),
1793
1896
  transform_pre: doc.get_attr(id, "transform"),
1794
1897
  bbox
1795
1898
  });
@@ -1805,16 +1908,16 @@ function createSvgEditor(opts) {
1805
1908
  const dx = target.x - union.x;
1806
1909
  const dy = target.y - union.y;
1807
1910
  const apply = () => {
1808
- for (const m of members) apply_resize(doc, m.id, m.rz, sx, sy, origin);
1911
+ for (const m of members) resize_pipeline.intent.apply(doc, m.id, m.rz, sx, sy, origin);
1809
1912
  if (dx !== 0 || dy !== 0) for (const m of members) {
1810
- const tx_after = capture_translate_baseline(doc, m.id);
1811
- apply_translate(doc, m.id, tx_after, dx, dy);
1913
+ const tx_after = translate_pipeline.intent.capture_baseline(doc, m.id);
1914
+ translate_pipeline.intent.apply(doc, m.id, tx_after, dx, dy);
1812
1915
  }
1813
1916
  emit();
1814
1917
  };
1815
1918
  const revert = () => {
1816
1919
  for (const m of members) {
1817
- apply_resize(doc, m.id, m.rz, 1, 1, origin);
1920
+ resize_pipeline.intent.apply(doc, m.id, m.rz, 1, 1, origin);
1818
1921
  doc.set_attr(m.id, "transform", m.transform_pre);
1819
1922
  }
1820
1923
  emit();
@@ -1855,10 +1958,11 @@ function createSvgEditor(opts) {
1855
1958
  function rotate(angle, opts) {
1856
1959
  const ids = opts?.ids ?? selection;
1857
1960
  if (ids.length === 0) return false;
1858
- const prepared = prepare_rotate_rpc({
1961
+ const pivot = opts?.pivot ?? default_rotate_pivot(ids);
1962
+ const prepared = rotate_pipeline.prepare_rpc({
1859
1963
  doc,
1860
1964
  ids,
1861
- pivot: opts?.pivot ?? default_rotate_pivot(ids),
1965
+ pivot,
1862
1966
  angle_radians: angle,
1863
1967
  options: { angle_snap_step_radians: style.angle_snap_step_radians },
1864
1968
  emit
@@ -1877,10 +1981,11 @@ function createSvgEditor(opts) {
1877
1981
  function rotate_to(angle, opts) {
1878
1982
  const ids = opts?.ids ?? selection;
1879
1983
  if (ids.length === 0) return false;
1880
- const probe = prepare_rotate_rpc({
1984
+ const pivot = opts?.pivot ?? default_rotate_pivot(ids);
1985
+ const probe = rotate_pipeline.prepare_rpc({
1881
1986
  doc,
1882
1987
  ids,
1883
- pivot: opts?.pivot ?? default_rotate_pivot(ids),
1988
+ pivot,
1884
1989
  angle_radians: 0,
1885
1990
  options: { angle_snap_step_radians: style.angle_snap_step_radians },
1886
1991
  emit: () => {}
@@ -1890,12 +1995,12 @@ function createSvgEditor(opts) {
1890
1995
  const apply = () => {
1891
1996
  for (const m of probe.plan.members) {
1892
1997
  const delta = angle - m.baseline.current_rotation_deg * DEG_TO_RAD;
1893
- apply_rotate(doc, m.id, m.baseline, delta);
1998
+ rotate_pipeline.intent.apply(doc, m.id, m.baseline, delta);
1894
1999
  }
1895
2000
  emit();
1896
2001
  };
1897
2002
  const revert = () => {
1898
- for (const m of probe.plan.members) apply_rotate(doc, m.id, m.baseline, 0);
2003
+ for (const m of probe.plan.members) rotate_pipeline.intent.apply(doc, m.id, m.baseline, 0);
1899
2004
  emit();
1900
2005
  };
1901
2006
  apply();
@@ -1915,7 +2020,7 @@ function createSvgEditor(opts) {
1915
2020
  for (const id of ids) {
1916
2021
  const pre = doc.get_attr(id, "transform");
1917
2022
  if (pre === null) continue;
1918
- const ops = parse_transform_list(pre);
2023
+ const ops = transform.parse(pre);
1919
2024
  if (ops === null) continue;
1920
2025
  if (ops.length === 1 && ops[0].type === "matrix") continue;
1921
2026
  members.push({
@@ -1925,104 +2030,11 @@ function createSvgEditor(opts) {
1925
2030
  });
1926
2031
  }
1927
2032
  if (members.length === 0) return false;
1928
- const IDENT = [
1929
- 1,
1930
- 0,
1931
- 0,
1932
- 1,
1933
- 0,
1934
- 0
1935
- ];
1936
- function mul(m1, m2) {
1937
- const [a1, b1, c1, d1, e1, f1] = m1;
1938
- const [a2, b2, c2, d2, e2, f2] = m2;
1939
- return [
1940
- a1 * a2 + c1 * b2,
1941
- b1 * a2 + d1 * b2,
1942
- a1 * c2 + c1 * d2,
1943
- b1 * c2 + d1 * d2,
1944
- a1 * e2 + c1 * f2 + e1,
1945
- b1 * e2 + d1 * f2 + f1
1946
- ];
1947
- }
1948
- function op_to_mat(op) {
1949
- switch (op.type) {
1950
- case "matrix": return [
1951
- op.a,
1952
- op.b,
1953
- op.c,
1954
- op.d,
1955
- op.e,
1956
- op.f
1957
- ];
1958
- case "translate": return [
1959
- 1,
1960
- 0,
1961
- 0,
1962
- 1,
1963
- op.tx,
1964
- op.ty
1965
- ];
1966
- case "rotate": {
1967
- const rad = op.angle * Math.PI / 180;
1968
- const c = Math.cos(rad);
1969
- const s = Math.sin(rad);
1970
- if (op.cx === 0 && op.cy === 0) return [
1971
- c,
1972
- s,
1973
- -s,
1974
- c,
1975
- 0,
1976
- 0
1977
- ];
1978
- const e = op.cx - c * op.cx + s * op.cy;
1979
- const f = op.cy - s * op.cx - c * op.cy;
1980
- return [
1981
- c,
1982
- s,
1983
- -s,
1984
- c,
1985
- e,
1986
- f
1987
- ];
1988
- }
1989
- case "scale": return [
1990
- op.sx,
1991
- 0,
1992
- 0,
1993
- op.sy,
1994
- 0,
1995
- 0
1996
- ];
1997
- case "skewX": {
1998
- const rad = op.angle * Math.PI / 180;
1999
- return [
2000
- 1,
2001
- 0,
2002
- Math.tan(rad),
2003
- 1,
2004
- 0,
2005
- 0
2006
- ];
2007
- }
2008
- case "skewY": {
2009
- const rad = op.angle * Math.PI / 180;
2010
- return [
2011
- 1,
2012
- Math.tan(rad),
2013
- 0,
2014
- 1,
2015
- 0,
2016
- 0
2017
- ];
2018
- }
2019
- }
2020
- }
2021
2033
  const apply = () => {
2022
2034
  for (const m of members) {
2023
- let mat = IDENT;
2024
- for (const op of m.ops) mat = mul(mat, op_to_mat(op));
2025
- doc.set_attr(m.id, "transform", emit_transform_list([{
2035
+ let mat = FLATTEN_IDENT;
2036
+ for (const op of m.ops) mat = flatten_mul(mat, flatten_op_to_mat(op));
2037
+ doc.set_attr(m.id, "transform", transform.emit([{
2026
2038
  type: "matrix",
2027
2039
  a: mat[0],
2028
2040
  b: mat[1],
@@ -2072,7 +2084,7 @@ function createSvgEditor(opts) {
2072
2084
  for (const id of ids) {
2073
2085
  const bbox = geometry_provider.bounds_of(id);
2074
2086
  if (!bbox) continue;
2075
- const baseline = capture_translate_baseline(doc, id);
2087
+ const baseline = translate_pipeline.intent.capture_baseline(doc, id);
2076
2088
  if (baseline.type === "unsupported") continue;
2077
2089
  members.push({
2078
2090
  id,
@@ -2094,12 +2106,12 @@ function createSvgEditor(opts) {
2094
2106
  const apply = () => {
2095
2107
  for (const m of members) {
2096
2108
  const d = deltas.get(m.id);
2097
- if (d) apply_translate(doc, m.id, m.baseline, d.x, d.y);
2109
+ if (d) translate_pipeline.intent.apply(doc, m.id, m.baseline, d.x, d.y);
2098
2110
  }
2099
2111
  emit();
2100
2112
  };
2101
2113
  const revert = () => {
2102
- for (const m of members) if (deltas.has(m.id)) apply_translate(doc, m.id, m.baseline, 0, 0);
2114
+ for (const m of members) if (deltas.has(m.id)) translate_pipeline.intent.apply(doc, m.id, m.baseline, 0, 0);
2103
2115
  emit();
2104
2116
  };
2105
2117
  apply();
@@ -2224,8 +2236,8 @@ function createSvgEditor(opts) {
2224
2236
  });
2225
2237
  });
2226
2238
  }
2227
- function group() {
2228
- const plan = plan_group(doc, selection);
2239
+ function group$1() {
2240
+ const plan = group.plan(doc, selection);
2229
2241
  if (!plan) return false;
2230
2242
  const group_id = doc.create_element("g");
2231
2243
  const original_selection = selection;
@@ -2353,12 +2365,70 @@ function createSvgEditor(opts) {
2353
2365
  }
2354
2366
  };
2355
2367
  }
2368
+ /**
2369
+ * Text-creation bracket for the click-to-place text tool. Creates an
2370
+ * empty `<text>` with `initial` attrs, opens a single history preview,
2371
+ * and selects it — the DOM surface then mounts inline content-edit on
2372
+ * it. The surface finalizes the returned session when content-edit
2373
+ * exits:
2374
+ *
2375
+ * - `commit()` — snapshots the live text content into the delta and
2376
+ * commits ONE undo step (create + text together). Redo replays both,
2377
+ * so a redone text insert keeps its content (a plain `insert_preview`
2378
+ * would lose it — text is not an attribute).
2379
+ * - `discard()` — rolls the creation back entirely: no node, no
2380
+ * committed history entry. This is the empty-equals-delete rule for a
2381
+ * freshly-placed node (design:
2382
+ * `docs/wg/feat-svg-editor/text-tool.md`).
2383
+ *
2384
+ * The node is inserted empty on open (so the caret has somewhere to
2385
+ * live); live edits mutate its text in place, and `commit()` reads the
2386
+ * final text back off the document.
2387
+ */
2388
+ function insert_text_preview(initial, opts) {
2389
+ const parent = opts?.parent ?? doc.root;
2390
+ const id = doc.create_element("text");
2391
+ const previous_selection = selection;
2392
+ const attrs = { ...initial };
2393
+ let committed_text = "";
2394
+ const apply = () => {
2395
+ for (const name in attrs) doc.set_attr(id, name, attrs[name]);
2396
+ if (doc.parent_of(id) === null) doc.insert(id, parent, null);
2397
+ doc.set_text(id, committed_text);
2398
+ set_selection([id]);
2399
+ };
2400
+ const revert = () => {
2401
+ doc.remove(id);
2402
+ set_selection(previous_selection);
2403
+ };
2404
+ const preview = history.preview("insert text");
2405
+ let active = true;
2406
+ preview.set({
2407
+ providerId: PROVIDER_ID,
2408
+ apply,
2409
+ revert
2410
+ });
2411
+ return {
2412
+ id,
2413
+ commit() {
2414
+ if (!active) return;
2415
+ active = false;
2416
+ committed_text = doc.text_of(id);
2417
+ preview.commit();
2418
+ },
2419
+ discard() {
2420
+ if (!active) return;
2421
+ active = false;
2422
+ preview.discard();
2423
+ }
2424
+ };
2425
+ }
2356
2426
  /** Per-tag default paint attrs. Wrapped so callers don't need to depend
2357
2427
  * on the InsertableTag type — `insert()` accepts arbitrary string tags
2358
2428
  * (so `commands.insert("path", ...)` works for paste / RPC) but only
2359
2429
  * the closed insertable set gets default paint. */
2360
2430
  function default_paint_attrs_for(tag) {
2361
- if (tag === "rect" || tag === "ellipse" || tag === "line") return default_paint_attrs(tag);
2431
+ if (tag === "rect" || tag === "ellipse" || tag === "line") return insertions.default_paint_attrs(tag);
2362
2432
  return {};
2363
2433
  }
2364
2434
  function set_text(value) {
@@ -2407,7 +2477,7 @@ function createSvgEditor(opts) {
2407
2477
  function enter_content_edit(target) {
2408
2478
  const id = target ?? (selection.length === 1 ? selection[0] : null);
2409
2479
  if (!id) return false;
2410
- if (!doc.is_text_edit_target(id)) return false;
2480
+ if (!doc.is_text_edit_target(id) && doc.is_vector_edit_target(id) === null) return false;
2411
2481
  if (!content_edit_driver) return false;
2412
2482
  return content_edit_driver(id);
2413
2483
  }
@@ -2455,7 +2525,7 @@ function createSvgEditor(opts) {
2455
2525
  align,
2456
2526
  reorder,
2457
2527
  remove,
2458
- group,
2528
+ group: group$1,
2459
2529
  insert,
2460
2530
  insert_preview,
2461
2531
  set_text,
@@ -2511,6 +2581,10 @@ function createSvgEditor(opts) {
2511
2581
  emit();
2512
2582
  }
2513
2583
  const public_editor = {
2584
+ /**
2585
+ * Low-level IR handle. Mutating directly bypasses history; prefer
2586
+ * `editor.commands` for app code.
2587
+ */
2514
2588
  document: doc,
2515
2589
  get state() {
2516
2590
  return snapshot();
@@ -2521,9 +2595,28 @@ function createSvgEditor(opts) {
2521
2595
  node_paint,
2522
2596
  dom_computed_property,
2523
2597
  dom_computed_paint,
2598
+ /**
2599
+ * Enter content-edit mode on a `<text>` node. Returns `false` (no-op)
2600
+ * when no DOM surface is attached.
2601
+ */
2524
2602
  enter_content_edit,
2525
2603
  defs,
2526
2604
  commands,
2605
+ /**
2606
+ * Human-readable label for hierarchy panels. SVG has no native "name";
2607
+ * this is the package's single source of truth so panels don't reinvent
2608
+ * the rule.
2609
+ *
2610
+ * Rule:
2611
+ * - `<text>` → text content, whitespace-collapsed and truncated at
2612
+ * ~40 chars (falls back to `"text"` for empty content).
2613
+ * - Otherwise → tag name, suffixed with `#id` when the `id` attribute
2614
+ * is present (e.g. `"rect #sun"`).
2615
+ *
2616
+ * `opts.tagLabel` lets callers substitute a friendlier or localized
2617
+ * term for the raw tag (e.g. `"rect"` → `"Rectangle"`). Only invoked
2618
+ * on the non-text branch.
2619
+ */
2527
2620
  display_label(id, opts) {
2528
2621
  const tag = doc.tag_of(id);
2529
2622
  if (tag === "text") {
@@ -2538,30 +2631,59 @@ function createSvgEditor(opts) {
2538
2631
  tree() {
2539
2632
  return tree_snapshot();
2540
2633
  },
2634
+ /**
2635
+ * The effective hover from the attached HUD surface — what's under the
2636
+ * pointer, OR whatever `set_surface_hover_override` last pushed. Used
2637
+ * by out-of-canvas UI (layers panel, breadcrumbs) to mirror the canvas
2638
+ * highlight. Returns `null` when nothing is hovered.
2639
+ */
2541
2640
  surface_hover() {
2542
2641
  return current_surface_hover;
2543
2642
  },
2643
+ /**
2644
+ * Push a hover override into the HUD surface — e.g. when the user
2645
+ * hovers a row in a layers panel. The HUD will render the override's
2646
+ * outline and (when applicable) drive measurement to that node.
2647
+ * Pass `null` to clear and let the pointer pick take over again.
2648
+ */
2544
2649
  set_surface_hover_override(id) {
2545
2650
  if (surface_hover_override === id) return;
2546
2651
  surface_hover_override = id;
2547
2652
  if (surface_hover_override_driver) surface_hover_override_driver(id);
2548
2653
  },
2654
+ /**
2655
+ * Subscribe to changes in the effective surface hover. Fires when the
2656
+ * HUD reports a new pointer pick AND when an override is set/cleared.
2657
+ * Cheap channel — does NOT bump `state.version`.
2658
+ */
2549
2659
  subscribe_surface_hover(cb) {
2550
2660
  surface_hover_listeners.add(cb);
2551
2661
  return () => {
2552
2662
  surface_hover_listeners.delete(cb);
2553
2663
  };
2554
2664
  },
2665
+ /**
2666
+ * Subscribe to bounds-affecting changes. Fires when any document
2667
+ * mutation advances `state.geometry_version` — drag, resize, text
2668
+ * edit, structural insert/remove. Skips presentation-only writes
2669
+ * (fill, opacity, stroke-color).
2670
+ */
2555
2671
  subscribe_geometry(cb) {
2556
2672
  geometry_listeners.add(cb);
2557
2673
  return () => {
2558
2674
  geometry_listeners.delete(cb);
2559
2675
  };
2560
2676
  },
2677
+ /**
2678
+ * World-space geometry queries. Non-null when a DOM surface is
2679
+ * attached; null otherwise (queries need a renderer to read bbox
2680
+ * from). Read-only — never mutates document state.
2681
+ */
2561
2682
  get geometry() {
2562
2683
  return geometry_provider;
2563
2684
  },
2564
2685
  modes,
2686
+ /** Switch the active tool. No history entry; bumps `state.version`. */
2565
2687
  set_tool,
2566
2688
  get style() {
2567
2689
  return style;
@@ -2577,6 +2699,7 @@ function createSvgEditor(opts) {
2577
2699
  _internal: {
2578
2700
  doc,
2579
2701
  history: { preview: (label) => history.preview(label) },
2702
+ insert_text_preview,
2580
2703
  emit,
2581
2704
  subscribe_translate_commit(cb) {
2582
2705
  translate_commit_listeners.add(cb);
@@ -2608,37 +2731,112 @@ function createSvgEditor(opts) {
2608
2731
  applyDefaultBindings(keymap);
2609
2732
  return public_editor;
2610
2733
  }
2611
- function paint_value_equals(a, b) {
2612
- if (a === b) return true;
2613
- if (a.declared !== b.declared) return false;
2614
- if (a.provenance.carrier !== b.provenance.carrier) return false;
2615
- if (a.provenance.origin !== b.provenance.origin) return false;
2616
- return paint_equals(a.computed, b.computed);
2617
- }
2618
- function paint_equals(a, b) {
2619
- if (a === b) return true;
2620
- if (a == null || b == null) return false;
2621
- if ("error" in a || "error" in b) return "error" in a && "error" in b && a.error === b.error && a.reason === b.reason;
2622
- if (a.kind !== b.kind) return false;
2623
- if (a.kind === "color" && b.kind === "color") {
2624
- if (a.value.kind !== b.value.kind) return false;
2625
- if (a.value.kind === "rgb" && b.value.kind === "rgb") return a.value.value === b.value.value;
2626
- return true;
2734
+ /**
2735
+ * Construct a headless SVG editor. The returned object is the public
2736
+ * editor surface — observation (`state`, `subscribe`), commands
2737
+ * (`commands.*`), lifecycle (`attach` / `dispose`), and the typed-read
2738
+ * caches (`node_paint`, `node_properties`). Surfaces (DOM, headless)
2739
+ * attach later via `editor.attach(surface)`.
2740
+ */
2741
+ function createSvgEditor(opts) {
2742
+ if (opts == null || typeof opts.svg !== "string") {
2743
+ const got = opts == null ? String(opts) : opts.svg === null ? "null" : typeof opts.svg;
2744
+ throw new TypeError(`createSvgEditor({ svg }) requires { svg: string }, got svg=${got}`);
2627
2745
  }
2628
- if (a.kind === "ref" && b.kind === "ref") return a.id === b.id;
2629
- if (a.kind === "none" && b.kind === "none") return true;
2630
- if (a.kind === "context_fill" && b.kind === "context_fill") return true;
2631
- if (a.kind === "context_stroke" && b.kind === "context_stroke") return true;
2632
- return false;
2746
+ return _create_svg_editor_internal(opts);
2633
2747
  }
2634
- function property_value_equals(a, b) {
2635
- if (a === b) return true;
2636
- if (a.declared !== b.declared) return false;
2637
- if (a.provenance.carrier !== b.provenance.carrier) return false;
2638
- if (a.provenance.origin !== b.provenance.origin) return false;
2639
- if (a.computed === b.computed) return true;
2640
- if (a.computed && b.computed && typeof a.computed === "object" && typeof b.computed === "object" && "error" in a.computed && "error" in b.computed) return a.computed.error === b.computed.error && a.computed.reason === b.computed.reason;
2641
- return false;
2748
+ const FLATTEN_IDENT = [
2749
+ 1,
2750
+ 0,
2751
+ 0,
2752
+ 1,
2753
+ 0,
2754
+ 0
2755
+ ];
2756
+ function flatten_mul(m1, m2) {
2757
+ const [a1, b1, c1, d1, e1, f1] = m1;
2758
+ const [a2, b2, c2, d2, e2, f2] = m2;
2759
+ return [
2760
+ a1 * a2 + c1 * b2,
2761
+ b1 * a2 + d1 * b2,
2762
+ a1 * c2 + c1 * d2,
2763
+ b1 * c2 + d1 * d2,
2764
+ a1 * e2 + c1 * f2 + e1,
2765
+ b1 * e2 + d1 * f2 + f1
2766
+ ];
2767
+ }
2768
+ function flatten_op_to_mat(op) {
2769
+ switch (op.type) {
2770
+ case "matrix": return [
2771
+ op.a,
2772
+ op.b,
2773
+ op.c,
2774
+ op.d,
2775
+ op.e,
2776
+ op.f
2777
+ ];
2778
+ case "translate": return [
2779
+ 1,
2780
+ 0,
2781
+ 0,
2782
+ 1,
2783
+ op.tx,
2784
+ op.ty
2785
+ ];
2786
+ case "rotate": {
2787
+ const rad = op.angle * Math.PI / 180;
2788
+ const c = Math.cos(rad);
2789
+ const s = Math.sin(rad);
2790
+ if (op.cx === 0 && op.cy === 0) return [
2791
+ c,
2792
+ s,
2793
+ -s,
2794
+ c,
2795
+ 0,
2796
+ 0
2797
+ ];
2798
+ const e = op.cx - c * op.cx + s * op.cy;
2799
+ const f = op.cy - s * op.cx - c * op.cy;
2800
+ return [
2801
+ c,
2802
+ s,
2803
+ -s,
2804
+ c,
2805
+ e,
2806
+ f
2807
+ ];
2808
+ }
2809
+ case "scale": return [
2810
+ op.sx,
2811
+ 0,
2812
+ 0,
2813
+ op.sy,
2814
+ 0,
2815
+ 0
2816
+ ];
2817
+ case "skewX": {
2818
+ const rad = op.angle * Math.PI / 180;
2819
+ return [
2820
+ 1,
2821
+ 0,
2822
+ Math.tan(rad),
2823
+ 1,
2824
+ 0,
2825
+ 0
2826
+ ];
2827
+ }
2828
+ case "skewY": {
2829
+ const rad = op.angle * Math.PI / 180;
2830
+ return [
2831
+ 1,
2832
+ Math.tan(rad),
2833
+ 0,
2834
+ 1,
2835
+ 0,
2836
+ 0
2837
+ ];
2838
+ }
2839
+ }
2642
2840
  }
2643
2841
  //#endregion
2644
2842
  export { createSvgEditor as t };