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

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,7 +1,8 @@
1
- const require_model = require("./model-DqGqV1H4.js");
1
+ const require_model = require("./model-CJ1Ctq14.js");
2
2
  let _grida_cmath = require("@grida/cmath");
3
3
  _grida_cmath = require_model.__toESM(_grida_cmath);
4
4
  let _grida_svg_parse = require("@grida/svg/parse");
5
+ let _grida_svg_pathdata = require("@grida/svg/pathdata");
5
6
  let _grida_vn = require("@grida/vn");
6
7
  _grida_vn = require_model.__toESM(_grida_vn);
7
8
  let _grida_text_editor_dom = require("@grida/text-editor/dom");
@@ -344,6 +345,12 @@ var MemoizedGeometryProvider = class {
344
345
  node_at_point(p) {
345
346
  return this.driver.node_at_point(p);
346
347
  }
348
+ /** Pass-through. Frame projection depends on live layout, not on the
349
+ * bounds cache, so there is nothing to memoize. Falls back to the raw
350
+ * delta when the driver can't resolve a frame. */
351
+ world_delta_to_local(id, delta) {
352
+ return this.driver.world_delta_to_local?.(id, delta) ?? delta;
353
+ }
347
354
  /** Unsubscribe from both signals. Call on surface detach. */
348
355
  dispose() {
349
356
  for (const unsub of this.unsubscribers) unsub();
@@ -1206,7 +1213,8 @@ function sub_selection_equal(a, b) {
1206
1213
  var VectorEditSession = class {
1207
1214
  constructor(node_id, source, session_d) {
1208
1215
  this.node_id = node_id;
1209
- this.source = source;
1216
+ this._source = source;
1217
+ this._source_before_promotion = null;
1210
1218
  this._session_d = session_d;
1211
1219
  this._last_seen_d = session_d;
1212
1220
  this._selected_vertices = [];
@@ -1214,6 +1222,51 @@ var VectorEditSession = class {
1214
1222
  this._selected_tangents = [];
1215
1223
  this._hovered_control = null;
1216
1224
  }
1225
+ /** Source tag the session currently projects through. See `_source`. */
1226
+ get source() {
1227
+ return this._source;
1228
+ }
1229
+ /**
1230
+ * Flip the source to `path` after the underlying element was promoted
1231
+ * (rect / circle / ellipse → `<path>`). Idempotent: a second call while
1232
+ * already promoted does nothing, so the pre-promotion source captured by
1233
+ * the first flip is never clobbered.
1234
+ */
1235
+ promote_source_to_path() {
1236
+ if (this._source_before_promotion !== null) return;
1237
+ if (this._source.kind === "path") return;
1238
+ this._source_before_promotion = this._source;
1239
+ this._source = {
1240
+ kind: "path",
1241
+ d: this._session_d
1242
+ };
1243
+ }
1244
+ /** Reverse a {@link promote_source_to_path} (gesture undo). No-op if the
1245
+ * source was never promoted. */
1246
+ restore_source() {
1247
+ if (this._source_before_promotion === null) return;
1248
+ this._source = this._source_before_promotion;
1249
+ this._source_before_promotion = null;
1250
+ }
1251
+ /**
1252
+ * Re-sync the source to the document's current tag, outright. Unlike
1253
+ * {@link promote_source_to_path} / {@link restore_source} (which manage a
1254
+ * single primitive→path flip within one gesture), this sets the source to
1255
+ * an authoritative value derived from the live document and clears the
1256
+ * promotion bookkeeping.
1257
+ *
1258
+ * The host calls this when an undo/redo re-types the node out from under a
1259
+ * *different* live session object than the one that performed the original
1260
+ * flip (exit + undo-exit creates a fresh session; the captured session's
1261
+ * `restore_source` then no-ops). Without it the live session could keep
1262
+ * `source.kind === "path"` while the node is back to a primitive, and the
1263
+ * next gesture would write a stray `d` onto the native tag. Re-deriving
1264
+ * from the document keeps the live session authoritative.
1265
+ */
1266
+ sync_source(source) {
1267
+ this._source = source;
1268
+ this._source_before_promotion = null;
1269
+ }
1217
1270
  /** The session's current PathModel-form `d`. Gesture handlers read
1218
1271
  * this instead of `doc.get_attr(node_id, "d")` so they stay tag-
1219
1272
  * oblivious (non-path sources have no `d` on the document). */
@@ -1378,26 +1431,39 @@ var VectorEditSession = class {
1378
1431
  function source_to_session_d(source) {
1379
1432
  switch (source.kind) {
1380
1433
  case "path": return source.d;
1434
+ case "line": return _grida_vn.default.toSVGPathData(_grida_vn.default.fromPolyline([[source.x1, source.y1], [source.x2, source.y2]]));
1381
1435
  case "polyline": return _grida_vn.default.toSVGPathData(_grida_vn.default.fromPolyline(source.points.map((p) => [p[0], p[1]])));
1382
1436
  case "polygon": return _grida_vn.default.toSVGPathData(_grida_vn.default.fromPolygon(source.points.map((p) => [p[0], p[1]])));
1437
+ case "circle": return _grida_vn.default.toSVGPathData(_grida_vn.default.fromEllipse({
1438
+ x: source.cx - source.r,
1439
+ y: source.cy - source.r,
1440
+ width: source.r * 2,
1441
+ height: source.r * 2
1442
+ }));
1443
+ case "ellipse": return _grida_vn.default.toSVGPathData(_grida_vn.default.fromEllipse({
1444
+ x: source.cx - source.rx,
1445
+ y: source.cy - source.ry,
1446
+ width: source.rx * 2,
1447
+ height: source.ry * 2
1448
+ }));
1449
+ case "rect": return _grida_svg_pathdata.SVGShapes.createRect(source.x, source.y, source.width, source.height, source.rx, source.ry).encode();
1383
1450
  }
1384
1451
  }
1385
1452
  /**
1386
- * Tag-aware document write. Given a new path-data `d` from a gesture,
1453
+ * Native-attribute writeback. Given a new path-data `d` from a gesture,
1387
1454
  * project it back into the source tag's native attrs and write those —
1388
- * unless the source is `<path>`, in which case `d` is written
1389
- * directly.
1455
+ * `<path>` takes `d` directly; `<line>` takes `x1/y1/x2/y2`;
1456
+ * `<polyline>` / `<polygon>` take `points`.
1390
1457
  *
1391
- * Returns `true` on success. Returns `false` for non-path sources when
1392
- * the model can no longer be expressed in the source tag's native attrs
1393
- * (tangent introduced, topology change). v1 treats `false` as gesture
1394
- * refusal the caller should NOT fall through and write `d` on a non-
1395
- * path element. Promotion to `<path>` lives in v1.1+.
1458
+ * Returns `true` if the geometry was written natively. Returns `false`
1459
+ * when the source tag cannot express the geometry a curve was introduced
1460
+ * or the topology left the tag's canonical form, OR the source is a
1461
+ * geometry primitive (rect / circle / ellipse) which has no native vector
1462
+ * form at all. A `false` return is the re-type-to-`<path>` signal; the
1463
+ * caller ({@link vector_apply}) handles it. This function never re-types.
1396
1464
  *
1397
- * Symmetric across apply / revert: gesture handlers call this for both
1398
- * the in-flight write and the undo-revert (since both are just "set the
1399
- * geometry to this d"), so apply/revert stay consistent even when one
1400
- * writes native attrs and the other would.
1465
+ * Symmetric across apply / revert: callers use it for both the in-flight
1466
+ * write and the undo-revert (both are just "set the geometry to this d").
1401
1467
  */
1402
1468
  function apply_session_d(doc, node_id, source, d) {
1403
1469
  if (source.kind === "path") {
@@ -1406,10 +1472,62 @@ function apply_session_d(doc, node_id, source, d) {
1406
1472
  }
1407
1473
  const native = require_model.PathModel.fromSvgPathD(d).toNativeAttrs(source.kind);
1408
1474
  if (native === null) return false;
1475
+ if (native.kind === "line") {
1476
+ doc.set_attr(node_id, "x1", String(native.x1));
1477
+ doc.set_attr(node_id, "y1", String(native.y1));
1478
+ doc.set_attr(node_id, "x2", String(native.x2));
1479
+ doc.set_attr(node_id, "y2", String(native.y2));
1480
+ return true;
1481
+ }
1409
1482
  const points = native.points.map((p) => `${p[0]},${p[1]}`).join(" ");
1410
1483
  doc.set_attr(node_id, "points", points);
1411
1484
  return true;
1412
1485
  }
1486
+ /**
1487
+ * Session-aware geometry write — the single commit chokepoint the DOM
1488
+ * gesture handlers call so re-typing stays in one place rather than being
1489
+ * reimplemented per gesture. One uniform rule across every source:
1490
+ *
1491
+ * 1. Try native writeback ({@link apply_session_d}). For `<path>` and for
1492
+ * a vertex tag (`line` / `polyline` / `polygon`) whose edit still fits
1493
+ * its native form, this writes and we're done — the element keeps its
1494
+ * tag.
1495
+ * 2. If native writeback refused (a curve was introduced, the topology
1496
+ * escaped the canonical chain, or the source is a geometry primitive
1497
+ * with no native form), re-type the element to `<path>` via
1498
+ * {@link SvgDocument.retype_to_path} and flip the session source to
1499
+ * `path` (so every downstream reader — overlay, gates, the
1500
+ * external-mutation reconciler — behaves correctly).
1501
+ *
1502
+ * Returns the {@link RetypeRecord} token iff this call performed a re-type
1503
+ * (so the caller can pair it with the edit in one history bracket and hand
1504
+ * it to {@link vector_revert} on undo); otherwise `null`.
1505
+ */
1506
+ function vector_apply(doc, session, d) {
1507
+ if (apply_session_d(doc, session.node_id, session.source, d)) return null;
1508
+ const token = doc.retype_to_path(session.node_id, d);
1509
+ if (token) {
1510
+ session.promote_source_to_path();
1511
+ return token;
1512
+ }
1513
+ doc.set_attr(session.node_id, "d", d);
1514
+ return null;
1515
+ }
1516
+ /**
1517
+ * Counterpart to {@link vector_apply}. If this gesture re-typed the element
1518
+ * (a non-null `promotion` token), restore the original tag/attrs and the
1519
+ * session source — the re-type and the edit undo as one step. Otherwise
1520
+ * re-write the baseline geometry natively; for a geometry primitive that
1521
+ * never re-typed, {@link apply_session_d} writes nothing (a correct no-op).
1522
+ */
1523
+ function vector_revert(doc, session, baseline_d, promotion) {
1524
+ if (promotion) {
1525
+ doc.revert_retype(session.node_id, promotion);
1526
+ session.restore_source();
1527
+ return;
1528
+ }
1529
+ apply_session_d(doc, session.node_id, session.source, baseline_d);
1530
+ }
1413
1531
  //#endregion
1414
1532
  //#region src/core/vector-edit/marquee.ts
1415
1533
  let marquee;
@@ -1521,11 +1639,6 @@ const IS_MODIFIER_KEY = {
1521
1639
  * surface skips render() during the in-flight mount and doesn't yank the
1522
1640
  * live `<text>` element out from under the about-to-mount text surface. */
1523
1641
  const TEXT_EDIT_PENDING = { __pending: true };
1524
- /** Per-frame `neighbours: []` for the `vector_of` HUD projection. Polyline
1525
- * and polygon sources never render tangent handles in v1 (curve edits would
1526
- * promote to `<path>`); using a frozen module-level array avoids allocating
1527
- * a fresh empty array per redraw frame. */
1528
- const EMPTY_NEIGHBOURS = Object.freeze([]);
1529
1642
  /**
1530
1643
  * Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
1531
1644
  * whose `detach()` is the inverse — DOM cleared, listeners removed,
@@ -1580,6 +1693,7 @@ var DomSurface = class DomSurface {
1580
1693
  this.teardown.push(() => this.attention.dispose());
1581
1694
  if (process.env.NODE_ENV !== "production" && container.children.length > 0) console.warn("@grida/svg-editor: surface container is not empty at attach time. Render chrome (toolbars, layer lists, inspectors) as siblings of the container, not children — otherwise clicks on those children will silently break. See README §Surface.");
1582
1695
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
1696
+ container.style.overflow = "hidden";
1583
1697
  container.style.userSelect = "none";
1584
1698
  container.style.webkitUserSelect = "none";
1585
1699
  const translate_options = () => {
@@ -1596,7 +1710,8 @@ var DomSurface = class DomSurface {
1596
1710
  emit: () => this.editor_internal().emit(),
1597
1711
  open_preview: (label) => this.editor_internal().history.preview(label),
1598
1712
  open_snap: (ids) => this.open_snap_session_for(ids),
1599
- options: translate_options
1713
+ options: translate_options,
1714
+ project_delta: (id, d) => this._geometry_provider?.world_delta_to_local(id, d) ?? d
1600
1715
  });
1601
1716
  const resize_options = () => {
1602
1717
  const style = this.editor.style;
@@ -2336,14 +2451,15 @@ var DomSurface = class DomSurface {
2336
2451
  const neighbor_ids = compute_neighborhood(doc, ids);
2337
2452
  const agent_id_set = /* @__PURE__ */ new Set();
2338
2453
  for (const id of ids) for (const inner of snap_descent(doc, id)) agent_id_set.add(inner);
2454
+ const bounds_of = (id) => this._geometry_provider?.bounds_of(id) ?? null;
2339
2455
  const agents = [];
2340
2456
  for (const id of agent_id_set) {
2341
- const r = this.bbox_world_for_snap(id);
2457
+ const r = bounds_of(id);
2342
2458
  if (r) agents.push(r);
2343
2459
  }
2344
2460
  const neighbors = [];
2345
2461
  for (const id of neighbor_ids) {
2346
- const r = this.bbox_world_for_snap(id);
2462
+ const r = bounds_of(id);
2347
2463
  if (r) neighbors.push(r);
2348
2464
  }
2349
2465
  return new SnapSession({
@@ -3261,7 +3377,7 @@ var DomSurface = class DomSurface {
3261
3377
  if (this.text_edit || this.vector_edit) return false;
3262
3378
  const tag = this.tag_of(id);
3263
3379
  if (tag === "text" || tag === "tspan") return this.enter_text_edit(id);
3264
- if (tag === "path" || tag === "polyline" || tag === "polygon") return this.enter_vector_edit(id);
3380
+ if (this.editor_internal().doc.is_vector_edit_target(id) !== null) return this.enter_vector_edit(id);
3265
3381
  return false;
3266
3382
  }
3267
3383
  /**
@@ -3544,11 +3660,11 @@ var DomSurface = class DomSurface {
3544
3660
  b_control: project_point_through_ctm(b_ctrl_local[0], b_ctrl_local[1], ctm, offset)
3545
3661
  };
3546
3662
  }),
3547
- neighbours: this.vector_edit.source.kind === "path" ? model.neighbouringVertices({
3663
+ neighbours: model.neighbouringVertices({
3548
3664
  vertices: this.vector_edit.selected_vertices,
3549
3665
  segments: this.vector_edit.selected_segments,
3550
3666
  tangents: this.vector_edit.selected_tangents
3551
- }) : EMPTY_NEIGHBOURS,
3667
+ }),
3552
3668
  origin: [0, 0]
3553
3669
  };
3554
3670
  }
@@ -3613,11 +3729,54 @@ var DomSurface = class DomSurface {
3613
3729
  replay_vector_session_state(target_node_id, d, selection) {
3614
3730
  const cur = this.vector_edit;
3615
3731
  if (!cur || cur.node_id !== target_node_id) return;
3616
- if (d !== null) cur.mark_seen(d);
3732
+ if (d !== null) {
3733
+ cur.mark_seen(d);
3734
+ const live_source = this.editor_internal().doc.is_vector_edit_target(cur.node_id);
3735
+ if (live_source) cur.sync_source(live_source);
3736
+ }
3617
3737
  cur.restore_selection(selection);
3618
3738
  this.sync_selection_mirror();
3619
3739
  }
3620
3740
  /**
3741
+ * Build the `{ apply, revert }` history step for a vector-edit geometry
3742
+ * delta — the single chokepoint that routes the write through
3743
+ * {@link vector_apply} / {@link vector_revert} so promote-to-path of a
3744
+ * primitive source (rect / circle / ellipse) is handled in one place
3745
+ * rather than per gesture.
3746
+ *
3747
+ * `promo` is the per-gesture token holder (shared by reference across an
3748
+ * `active_preview`'s preview frames and its committed step) so the
3749
+ * promotion that fires on the first frame is the one undo reverses —
3750
+ * promotion + first edit collapse into a single undo step. On redo after
3751
+ * an undo-demote, `apply` re-promotes and refreshes the token.
3752
+ *
3753
+ * `after_selection` / `before_selection` drive sub-selection replay;
3754
+ * pass `null` (preview frames) to skip it.
3755
+ */
3756
+ vector_geometry_step(node_id, target_d, baseline_d, promo, after_selection, before_selection) {
3757
+ const internal = this.editor_internal();
3758
+ const doc = internal.doc;
3759
+ const emit = internal.emit;
3760
+ const session = this.vector_edit;
3761
+ return {
3762
+ providerId: "svg-editor",
3763
+ apply: () => {
3764
+ if (session) {
3765
+ const tok = vector_apply(doc, session, target_d);
3766
+ if (tok) promo.token = tok;
3767
+ }
3768
+ if (after_selection) this.replay_vector_session_state(node_id, target_d, after_selection);
3769
+ emit();
3770
+ },
3771
+ revert: () => {
3772
+ if (session) vector_revert(doc, session, baseline_d, promo.token);
3773
+ promo.token = null;
3774
+ if (before_selection) this.replay_vector_session_state(node_id, baseline_d, before_selection);
3775
+ emit();
3776
+ }
3777
+ };
3778
+ }
3779
+ /**
3621
3780
  * Push a standalone vector sub-selection change as one history entry.
3622
3781
  *
3623
3782
  * Called by selection-only handlers (vertex / segment / tangent click,
@@ -3709,11 +3868,7 @@ var DomSurface = class DomSurface {
3709
3868
  handle_translate_vertices(intent) {
3710
3869
  if (!this.vector_edit || this.vector_edit.node_id !== intent.node_id) return;
3711
3870
  const internal = this.editor_internal();
3712
- const doc = internal.doc;
3713
- const emit = internal.emit;
3714
3871
  const node_id = intent.node_id;
3715
- const source = this.vector_edit.source;
3716
- const commit = (d) => apply_session_d(doc, node_id, source, d);
3717
3872
  if (!this.active_preview || this.active_preview.kind !== "vector_vertex_translate" || this.active_preview.node_id !== node_id || !require_model.array_shallow_equal(this.active_preview.indices, intent.indices)) {
3718
3873
  if (this.active_preview) {
3719
3874
  if ("session" in this.active_preview) this.active_preview.session.discard();
@@ -3724,6 +3879,7 @@ var DomSurface = class DomSurface {
3724
3879
  this.active_preview = {
3725
3880
  kind: "vector_vertex_translate",
3726
3881
  node_id,
3882
+ promo: { token: null },
3727
3883
  indices: [...intent.indices],
3728
3884
  initial_d,
3729
3885
  baseline_model,
@@ -3749,32 +3905,10 @@ var DomSurface = class DomSurface {
3749
3905
  if (intent.phase === "commit") {
3750
3906
  const before_selection = this.active_preview.before_selection;
3751
3907
  const after_selection = this.vector_edit.snapshot_selection();
3752
- this.active_preview.session.set({
3753
- providerId: "svg-editor",
3754
- apply: () => {
3755
- commit(target_d);
3756
- this.replay_vector_session_state(node_id, target_d, after_selection);
3757
- emit();
3758
- },
3759
- revert: () => {
3760
- commit(baseline_d);
3761
- this.replay_vector_session_state(node_id, baseline_d, before_selection);
3762
- emit();
3763
- }
3764
- });
3908
+ this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, after_selection, before_selection));
3765
3909
  this.active_preview.session.commit();
3766
3910
  this.active_preview = null;
3767
- } else this.active_preview.session.set({
3768
- providerId: "svg-editor",
3769
- apply: () => {
3770
- commit(target_d);
3771
- emit();
3772
- },
3773
- revert: () => {
3774
- commit(baseline_d);
3775
- emit();
3776
- }
3777
- });
3911
+ } else this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, null, null));
3778
3912
  }
3779
3913
  /**
3780
3914
  * `translate_vector_selection` — the sub-selection-aware delta-translate.
@@ -3806,11 +3940,7 @@ var DomSurface = class DomSurface {
3806
3940
  handle_translate_vector_selection(intent) {
3807
3941
  if (!this.vector_edit || this.vector_edit.node_id !== intent.node_id) return;
3808
3942
  const internal = this.editor_internal();
3809
- const doc = internal.doc;
3810
- const emit = internal.emit;
3811
3943
  const node_id = intent.node_id;
3812
- const source = this.vector_edit.source;
3813
- const commit = (d) => apply_session_d(doc, node_id, source, d);
3814
3944
  const ses = this.vector_edit;
3815
3945
  const current_d = this.read_session_d();
3816
3946
  if (current_d === null) return;
@@ -3847,6 +3977,7 @@ var DomSurface = class DomSurface {
3847
3977
  this.active_preview = {
3848
3978
  kind: "vector_translate_selection",
3849
3979
  node_id,
3980
+ promo: { token: null },
3850
3981
  indices: [...indices],
3851
3982
  tangent_refs: [...tangent_refs],
3852
3983
  initial_d,
@@ -3880,32 +4011,10 @@ var DomSurface = class DomSurface {
3880
4011
  if (intent.phase === "commit") {
3881
4012
  const before_selection = this.active_preview.before_selection;
3882
4013
  const after_selection = this.vector_edit.snapshot_selection();
3883
- this.active_preview.session.set({
3884
- providerId: "svg-editor",
3885
- apply: () => {
3886
- commit(target_d);
3887
- this.replay_vector_session_state(node_id, target_d, after_selection);
3888
- emit();
3889
- },
3890
- revert: () => {
3891
- commit(baseline_d);
3892
- this.replay_vector_session_state(node_id, baseline_d, before_selection);
3893
- emit();
3894
- }
3895
- });
4014
+ this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, after_selection, before_selection));
3896
4015
  this.active_preview.session.commit();
3897
4016
  this.active_preview = null;
3898
- } else this.active_preview.session.set({
3899
- providerId: "svg-editor",
3900
- apply: () => {
3901
- commit(target_d);
3902
- emit();
3903
- },
3904
- revert: () => {
3905
- commit(baseline_d);
3906
- emit();
3907
- }
3908
- });
4017
+ } else this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, null, null));
3909
4018
  }
3910
4019
  /** Mirror handler for `select_tangent` (analogous to `select_vertex`). */
3911
4020
  handle_select_tangent(intent) {
@@ -3930,13 +4039,8 @@ var DomSurface = class DomSurface {
3930
4039
  */
3931
4040
  handle_set_tangent_intent(intent) {
3932
4041
  if (!this.vector_edit || this.vector_edit.node_id !== intent.node_id) return;
3933
- if (this.vector_edit.source.kind !== "path") return;
3934
4042
  const internal = this.editor_internal();
3935
- const doc = internal.doc;
3936
- const emit = internal.emit;
3937
4043
  const node_id = intent.node_id;
3938
- const source = this.vector_edit.source;
3939
- const commit = (d) => apply_session_d(doc, node_id, source, d);
3940
4044
  if (!this.active_preview || this.active_preview.kind !== "vector_set_tangent" || this.active_preview.node_id !== node_id || this.active_preview.tangent[0] !== intent.tangent[0] || this.active_preview.tangent[1] !== intent.tangent[1]) {
3941
4045
  if (this.active_preview && "session" in this.active_preview) this.active_preview.session.discard();
3942
4046
  const initial_d = this.read_session_d();
@@ -3945,6 +4049,7 @@ var DomSurface = class DomSurface {
3945
4049
  this.active_preview = {
3946
4050
  kind: "vector_set_tangent",
3947
4051
  node_id,
4052
+ promo: { token: null },
3948
4053
  tangent: [intent.tangent[0], intent.tangent[1]],
3949
4054
  initial_d,
3950
4055
  baseline_model,
@@ -3962,32 +4067,10 @@ var DomSurface = class DomSurface {
3962
4067
  if (intent.phase === "commit") {
3963
4068
  const before_selection = this.active_preview.before_selection;
3964
4069
  const after_selection = this.vector_edit.snapshot_selection();
3965
- this.active_preview.session.set({
3966
- providerId: "svg-editor",
3967
- apply: () => {
3968
- commit(target_d);
3969
- this.replay_vector_session_state(node_id, target_d, after_selection);
3970
- emit();
3971
- },
3972
- revert: () => {
3973
- commit(baseline_d);
3974
- this.replay_vector_session_state(node_id, baseline_d, before_selection);
3975
- emit();
3976
- }
3977
- });
4070
+ this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, after_selection, before_selection));
3978
4071
  this.active_preview.session.commit();
3979
4072
  this.active_preview = null;
3980
- } else this.active_preview.session.set({
3981
- providerId: "svg-editor",
3982
- apply: () => {
3983
- commit(target_d);
3984
- emit();
3985
- },
3986
- revert: () => {
3987
- commit(baseline_d);
3988
- emit();
3989
- }
3990
- });
4073
+ } else this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, null, null));
3991
4074
  }
3992
4075
  /**
3993
4076
  * Split a segment at parametric position `t`. One-shot atomic edit; no
@@ -3998,35 +4081,20 @@ var DomSurface = class DomSurface {
3998
4081
  handle_split_segment(intent) {
3999
4082
  if (!this.vector_edit || this.vector_edit.node_id !== intent.node_id) return;
4000
4083
  const node_id = intent.node_id;
4001
- const source = this.vector_edit.source;
4002
- const commit = (d) => apply_session_d(doc, node_id, source, d);
4003
4084
  const baseline_d = this.read_session_d();
4004
4085
  if (baseline_d === null) return;
4005
4086
  const { model: next_model, new_vertex } = require_model.PathModel.fromSvgPathD(baseline_d).splitSegment(intent.segment, intent.t);
4006
4087
  const target_d = next_model.toSvgPathD();
4007
4088
  const internal = this.editor_internal();
4008
- const doc = internal.doc;
4009
- const emit = internal.emit;
4010
4089
  const before_selection = this.vector_edit.snapshot_selection();
4011
4090
  const after_selection = Object.freeze({
4012
4091
  vertices: Object.freeze([new_vertex]),
4013
4092
  segments: Object.freeze([]),
4014
4093
  tangents: Object.freeze([])
4015
4094
  });
4095
+ const promo = { token: null };
4016
4096
  const split_session = internal.history.preview("vector/split-segment");
4017
- split_session.set({
4018
- providerId: "svg-editor",
4019
- apply: () => {
4020
- commit(target_d);
4021
- this.replay_vector_session_state(node_id, target_d, after_selection);
4022
- emit();
4023
- },
4024
- revert: () => {
4025
- commit(baseline_d);
4026
- this.replay_vector_session_state(node_id, baseline_d, before_selection);
4027
- emit();
4028
- }
4029
- });
4097
+ split_session.set(this.vector_geometry_step(node_id, target_d, baseline_d, promo, after_selection, before_selection));
4030
4098
  split_session.commit();
4031
4099
  this.redraw();
4032
4100
  }
@@ -4041,13 +4109,8 @@ var DomSurface = class DomSurface {
4041
4109
  */
4042
4110
  handle_bend_segment(intent) {
4043
4111
  if (!this.vector_edit || this.vector_edit.node_id !== intent.node_id) return;
4044
- if (this.vector_edit.source.kind !== "path") return;
4045
4112
  const internal = this.editor_internal();
4046
- const doc = internal.doc;
4047
- const emit = internal.emit;
4048
4113
  const node_id = intent.node_id;
4049
- const source = this.vector_edit.source;
4050
- const commit = (d) => apply_session_d(doc, node_id, source, d);
4051
4114
  if (!this.active_preview || this.active_preview.kind !== "vector_bend_segment" || this.active_preview.node_id !== node_id || this.active_preview.segment !== intent.segment || this.active_preview.ca !== intent.ca) {
4052
4115
  if (this.active_preview && "session" in this.active_preview) this.active_preview.session.discard();
4053
4116
  const initial_d = this.read_session_d();
@@ -4061,6 +4124,7 @@ var DomSurface = class DomSurface {
4061
4124
  this.active_preview = {
4062
4125
  kind: "vector_bend_segment",
4063
4126
  node_id,
4127
+ promo: { token: null },
4064
4128
  segment: intent.segment,
4065
4129
  ca: intent.ca,
4066
4130
  frozen: {
@@ -4086,32 +4150,10 @@ var DomSurface = class DomSurface {
4086
4150
  if (intent.phase === "commit") {
4087
4151
  const before_selection = this.active_preview.before_selection;
4088
4152
  const after_selection = this.vector_edit.snapshot_selection();
4089
- this.active_preview.session.set({
4090
- providerId: "svg-editor",
4091
- apply: () => {
4092
- commit(target_d);
4093
- this.replay_vector_session_state(node_id, target_d, after_selection);
4094
- emit();
4095
- },
4096
- revert: () => {
4097
- commit(baseline_d);
4098
- this.replay_vector_session_state(node_id, baseline_d, before_selection);
4099
- emit();
4100
- }
4101
- });
4153
+ this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, after_selection, before_selection));
4102
4154
  this.active_preview.session.commit();
4103
4155
  this.active_preview = null;
4104
- } else this.active_preview.session.set({
4105
- providerId: "svg-editor",
4106
- apply: () => {
4107
- commit(target_d);
4108
- emit();
4109
- },
4110
- revert: () => {
4111
- commit(baseline_d);
4112
- emit();
4113
- }
4114
- });
4156
+ } else this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, null, null));
4115
4157
  }
4116
4158
  /**
4117
4159
  * Project a doc-space point (HUD's container CSS-px frame) back to the
@@ -4168,24 +4210,6 @@ var DomSurface = class DomSurface {
4168
4210
  const transform_str = this.editor.document.get_attr(id, "transform");
4169
4211
  return require_model.transform.project(local, transform_str);
4170
4212
  }
4171
- /** World-space rect for snap purposes. Differs from `bbox_world` for
4172
- * `<svg>` viewport-establishing elements: `getBBox()` on an `<svg>`
4173
- * reports the union of descendant geometry (SVG 2 §4.6.4), which —
4174
- * when the dragged element is a descendant — silently turns the
4175
- * dragged element's own pre-gesture position into a snap target via
4176
- * the parent's edges. Use the viewport extent instead so the root
4177
- * SVG's snap edges represent the canvas boundary, not "wherever the
4178
- * children happen to be right now". */
4179
- bbox_world_for_snap(id) {
4180
- if (this.tag_of(id) === "svg") {
4181
- const el = this.element_index.get(id);
4182
- if (el instanceof SVGSVGElement) {
4183
- const vp = svg_viewport_bounds(el);
4184
- if (vp) return vp;
4185
- }
4186
- }
4187
- return this.bbox_world(id);
4188
- }
4189
4213
  editor_internal() {
4190
4214
  return this.editor._internal;
4191
4215
  }
@@ -4440,6 +4464,36 @@ var SvgGeometryDriver = class {
4440
4464
  node_at_point(p) {
4441
4465
  return this.accessors.pick_at_world(p, true);
4442
4466
  }
4467
+ /** World→local delta projection. The frame an element's position is
4468
+ * written in is its PARENT user-space: a `<rect>`'s `x`/`y` and the
4469
+ * leading `translate(...)` composed onto a `<g>`/transformed node are
4470
+ * both interpreted there. We take the parent element's frame (not the
4471
+ * element's own) so that translating a node whose OWN transform has a
4472
+ * scale/rotation is not double-counted.
4473
+ *
4474
+ * Camera-free: `inv(root.getScreenCTM) ∘ parent.getScreenCTM` maps
4475
+ * parent user-space → root world-space, cancelling the shared CSS /
4476
+ * camera transform. Inverting its linear part turns a world delta into
4477
+ * the local delta. Identity (→ delta unchanged) for flat frames,
4478
+ * top-level nodes, and any degenerate / unavailable matrix. */
4479
+ world_delta_to_local(id, delta) {
4480
+ const parent = this.accessors.element_for(id)?.parentNode;
4481
+ const root = this.accessors.root();
4482
+ if (!(parent instanceof SVGGraphicsElement) || !root) return delta;
4483
+ if (parent === root) return delta;
4484
+ if (typeof parent.getScreenCTM !== "function" || typeof root.getScreenCTM !== "function") return delta;
4485
+ const parent_ctm = parent.getScreenCTM();
4486
+ const root_ctm = root.getScreenCTM();
4487
+ if (!parent_ctm || !root_ctm) return delta;
4488
+ const m = root_ctm.inverse().multiply(parent_ctm);
4489
+ const det = m.a * m.d - m.c * m.b;
4490
+ if (!Number.isFinite(det) || det === 0) return delta;
4491
+ const [x, y] = project_delta_inverse_ctm(delta.x, delta.y, m);
4492
+ return {
4493
+ x,
4494
+ y
4495
+ };
4496
+ }
4443
4497
  };
4444
4498
  var SvgHitShapeDriver = class {
4445
4499
  constructor(accessors) {
package/dist/dom.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { C as BoundsResolver, E as CameraOptions, T as CameraConstraints, d as GestureContext, f as GestureId, g as MemoizedGeometryProvider, h as GeometrySignals, i as DomComputedResolver, m as GeometryProvider, p as Gestures, r as DomComputedPaint, u as GestureBinding, w as Camera } from "./editor-YQwdWHBb.mjs";
2
- import { a as project_delta_inverse_ctm, c as SnapOptions, i as inverse_project_rect, n as DomSurfaceOptions, o as project_point_through_ctm, r as attach_dom_surface, s as DEFAULT_SNAP_OPTIONS, t as DomSurfaceHandle } from "./dom-Dz_V6q0Y.mjs";
1
+ import { C as BoundsResolver, E as CameraOptions, T as CameraConstraints, d as GestureContext, f as GestureId, g as MemoizedGeometryProvider, h as GeometrySignals, i as DomComputedResolver, m as GeometryProvider, p as Gestures, r as DomComputedPaint, u as GestureBinding, w as Camera } from "./editor-Dl7c0q5A.mjs";
2
+ import { a as project_delta_inverse_ctm, c as SnapOptions, i as inverse_project_rect, n as DomSurfaceOptions, o as project_point_through_ctm, r as attach_dom_surface, s as DEFAULT_SNAP_OPTIONS, t as DomSurfaceHandle } from "./dom-CK6GlgFF.mjs";
3
3
  export { type BoundsResolver, Camera, type CameraConstraints, type CameraOptions, DEFAULT_SNAP_OPTIONS, type DomComputedPaint, type DomComputedResolver, DomSurfaceHandle, DomSurfaceOptions, type GeometryProvider, type GeometrySignals, type GestureBinding, type GestureContext, type GestureId, Gestures, MemoizedGeometryProvider, type SnapOptions, attach_dom_surface, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
package/dist/dom.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { C as BoundsResolver, E as CameraOptions, T as CameraConstraints, d as GestureContext, f as GestureId, g as MemoizedGeometryProvider, h as GeometrySignals, i as DomComputedResolver, m as GeometryProvider, p as Gestures, r as DomComputedPaint, u as GestureBinding, w as Camera } from "./editor-CJ2KuRh5.js";
2
- import { a as project_delta_inverse_ctm, c as SnapOptions, i as inverse_project_rect, n as DomSurfaceOptions, o as project_point_through_ctm, r as attach_dom_surface, s as DEFAULT_SNAP_OPTIONS, t as DomSurfaceHandle } from "./dom-D4dy6kq5.js";
1
+ import { C as BoundsResolver, E as CameraOptions, T as CameraConstraints, d as GestureContext, f as GestureId, g as MemoizedGeometryProvider, h as GeometrySignals, i as DomComputedResolver, m as GeometryProvider, p as Gestures, r as DomComputedPaint, u as GestureBinding, w as Camera } from "./editor-BKoo9SPL.js";
2
+ import { a as project_delta_inverse_ctm, c as SnapOptions, i as inverse_project_rect, n as DomSurfaceOptions, o as project_point_through_ctm, r as attach_dom_surface, s as DEFAULT_SNAP_OPTIONS, t as DomSurfaceHandle } from "./dom-CsKXTaNw.js";
3
3
  export { type BoundsResolver, Camera, type CameraConstraints, type CameraOptions, DEFAULT_SNAP_OPTIONS, type DomComputedPaint, type DomComputedResolver, DomSurfaceHandle, DomSurfaceOptions, type GeometryProvider, type GeometrySignals, type GestureBinding, type GestureContext, type GestureId, Gestures, MemoizedGeometryProvider, type SnapOptions, attach_dom_surface, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
package/dist/dom.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dom = require("./dom-BuD8TKmL.js");
2
+ const require_dom = require("./dom-Dee6FtgZ.js");
3
3
  exports.Camera = require_dom.Camera;
4
4
  exports.DEFAULT_SNAP_OPTIONS = require_dom.DEFAULT_SNAP_OPTIONS;
5
5
  exports.Gestures = require_dom.Gestures;
package/dist/dom.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as Gestures, c as Camera, i as project_point_through_ctm, n as inverse_project_rect, o as DEFAULT_SNAP_OPTIONS, r as project_delta_inverse_ctm, s as MemoizedGeometryProvider, t as attach_dom_surface } from "./dom-DSjfCllZ.mjs";
1
+ import { a as Gestures, c as Camera, i as project_point_through_ctm, n as inverse_project_rect, o as DEFAULT_SNAP_OPTIONS, r as project_delta_inverse_ctm, s as MemoizedGeometryProvider, t as attach_dom_surface } from "./dom-DILY80j7.mjs";
2
2
  export { Camera, DEFAULT_SNAP_OPTIONS, Gestures, MemoizedGeometryProvider, attach_dom_surface, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };