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