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