@grida/svg-editor 1.0.0-alpha.18 → 1.0.0-alpha.20
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 +21 -4
- package/dist/{dom-BMzX1CXZ.d.ts → dom-CQkWJNrK.d.ts} +77 -6
- package/dist/{dom-DKQ4Vt3z.js → dom-CfYDV311.js} +342 -55
- package/dist/{dom-OP-kmK8k.mjs → dom-Dub-TMoN.mjs} +342 -55
- package/dist/{dom-TctdgRnn.d.mts → dom-Dw2SPHgc.d.mts} +77 -6
- package/dist/dom.d.mts +3 -3
- package/dist/dom.d.ts +3 -3
- package/dist/dom.js +1 -1
- package/dist/dom.mjs +1 -1
- package/dist/{editor-Be6UrMeV.js → editor-4U9LAZ6r.js} +70 -182
- package/dist/{editor-BkCbYCz2.mjs → editor-B1GmFnS9.mjs} +63 -175
- package/dist/{editor-KqpIW1qm.d.mts → editor-CcW4BVth.d.mts} +54 -0
- package/dist/{editor-BSxTUsW_.d.ts → editor-CxqRhhzP.d.ts} +54 -0
- 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-BLhMJZKJ.js → model-CzL6_zId.js} +179 -1
- package/dist/{model-DU0GOMwM.mjs → model-DS5MxDrd.mjs} +161 -2
- package/dist/presets.d.mts +2 -2
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +3 -4
- package/dist/presets.mjs +2 -3
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +13 -2
- package/dist/react.mjs +13 -2
- package/package.json +6 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_model = require("./model-
|
|
1
|
+
const require_model = require("./model-CzL6_zId.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");
|
|
@@ -759,6 +759,35 @@ function resolve_text_exit(input) {
|
|
|
759
759
|
return { kind: "noop" };
|
|
760
760
|
}
|
|
761
761
|
//#endregion
|
|
762
|
+
//#region src/selection/marquee.ts
|
|
763
|
+
let marquee_selection;
|
|
764
|
+
(function(_marquee_selection) {
|
|
765
|
+
function hits(boxes, rect) {
|
|
766
|
+
const touched = boxes.filter(([, box]) => _grida_cmath.default.rect.intersects(box, rect));
|
|
767
|
+
const front = touched.length - 1;
|
|
768
|
+
const out = [];
|
|
769
|
+
for (let i = 0; i < touched.length; i++) {
|
|
770
|
+
const [id, box] = touched[i];
|
|
771
|
+
if (i !== front && _grida_cmath.default.rect.contains(box, rect)) continue;
|
|
772
|
+
out.push(id);
|
|
773
|
+
}
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
_marquee_selection.hits = hits;
|
|
777
|
+
function resolve(boxes, rect, baseline, opts = {}) {
|
|
778
|
+
const h = hits(boxes, rect);
|
|
779
|
+
if (!opts.additive) return h;
|
|
780
|
+
const out = [...baseline];
|
|
781
|
+
const seen = new Set(baseline);
|
|
782
|
+
for (const id of h) if (!seen.has(id)) {
|
|
783
|
+
seen.add(id);
|
|
784
|
+
out.push(id);
|
|
785
|
+
}
|
|
786
|
+
return out;
|
|
787
|
+
}
|
|
788
|
+
_marquee_selection.resolve = resolve;
|
|
789
|
+
})(marquee_selection || (marquee_selection = {}));
|
|
790
|
+
//#endregion
|
|
762
791
|
//#region src/gestures/gestures.ts
|
|
763
792
|
/**
|
|
764
793
|
* Sibling to `Keymap`. Owns a list of installed gesture bindings; each
|
|
@@ -995,33 +1024,68 @@ function applyDefaultGestures(gestures) {
|
|
|
995
1024
|
* Install pointer-tracking listeners on `container` and return the
|
|
996
1025
|
* read-side handle. The tracker is owned by the surface and disposed
|
|
997
1026
|
* alongside it; gesture bindings that need to consult it receive the
|
|
998
|
-
* read-only `is_attended` predicate through `GestureContext`.
|
|
1027
|
+
* read-only `is_attended` predicate through `GestureContext`. Hosts
|
|
1028
|
+
* extend the scope through `handle.attention` (`dom.ts`), which fronts
|
|
1029
|
+
* {@link AttentionTracker.add} / {@link AttentionTracker.remove}.
|
|
999
1030
|
*/
|
|
1000
1031
|
function create_attention_tracker(container) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1032
|
+
/** Elements of the scope the pointer is currently over. Per-element
|
|
1033
|
+
* membership (not a single boolean) so crossing from the container
|
|
1034
|
+
* onto overlapping registered chrome — `leave` and `enter` firing in
|
|
1035
|
+
* either order — never reads as a gap in attention. */
|
|
1036
|
+
const hovered = /* @__PURE__ */ new Set();
|
|
1037
|
+
/** Registered extras → their hover-tracking teardown. */
|
|
1038
|
+
const extras = /* @__PURE__ */ new Map();
|
|
1039
|
+
let disposed = false;
|
|
1040
|
+
/** Start hover-tracking `element`; returns the exact undo. */
|
|
1041
|
+
const track = (element) => {
|
|
1042
|
+
const enter = () => {
|
|
1043
|
+
hovered.add(element);
|
|
1044
|
+
};
|
|
1045
|
+
const leave = () => {
|
|
1046
|
+
hovered.delete(element);
|
|
1047
|
+
};
|
|
1048
|
+
element.addEventListener("pointerenter", enter);
|
|
1049
|
+
element.addEventListener("pointerleave", leave);
|
|
1050
|
+
return () => {
|
|
1051
|
+
element.removeEventListener("pointerenter", enter);
|
|
1052
|
+
element.removeEventListener("pointerleave", leave);
|
|
1053
|
+
hovered.delete(element);
|
|
1054
|
+
};
|
|
1007
1055
|
};
|
|
1008
|
-
container
|
|
1009
|
-
container.addEventListener("pointerleave", on_leave);
|
|
1056
|
+
const untrack_container = track(container);
|
|
1010
1057
|
const is_focus_within = () => {
|
|
1011
1058
|
const owner = container.ownerDocument;
|
|
1012
1059
|
if (!owner) return false;
|
|
1013
1060
|
const active = owner.activeElement;
|
|
1014
|
-
|
|
1061
|
+
if (!active || active === owner.body) return false;
|
|
1062
|
+
if (container.contains(active)) return true;
|
|
1063
|
+
for (const element of extras.keys()) if (element.contains(active)) return true;
|
|
1064
|
+
return false;
|
|
1015
1065
|
};
|
|
1016
1066
|
const is_attended = () => {
|
|
1017
|
-
return is_focus_within()
|
|
1067
|
+
return hovered.size > 0 || is_focus_within();
|
|
1018
1068
|
};
|
|
1019
1069
|
return {
|
|
1020
1070
|
is_attended,
|
|
1021
1071
|
is_focus_within,
|
|
1072
|
+
add: (element) => {
|
|
1073
|
+
if (disposed) return;
|
|
1074
|
+
if (element === container || extras.has(element)) return;
|
|
1075
|
+
extras.set(element, track(element));
|
|
1076
|
+
if (typeof element.matches === "function" && element.matches(":hover")) hovered.add(element);
|
|
1077
|
+
},
|
|
1078
|
+
remove: (element) => {
|
|
1079
|
+
const untrack = extras.get(element);
|
|
1080
|
+
if (!untrack) return;
|
|
1081
|
+
extras.delete(element);
|
|
1082
|
+
untrack();
|
|
1083
|
+
},
|
|
1022
1084
|
dispose: () => {
|
|
1023
|
-
|
|
1024
|
-
|
|
1085
|
+
disposed = true;
|
|
1086
|
+
untrack_container();
|
|
1087
|
+
for (const untrack of extras.values()) untrack();
|
|
1088
|
+
extras.clear();
|
|
1025
1089
|
}
|
|
1026
1090
|
};
|
|
1027
1091
|
}
|
|
@@ -1701,7 +1765,8 @@ function attach_dom_surface(editor, options) {
|
|
|
1701
1765
|
inner.detach();
|
|
1702
1766
|
},
|
|
1703
1767
|
camera: surface.camera,
|
|
1704
|
-
gestures: surface.gestures
|
|
1768
|
+
gestures: surface.gestures,
|
|
1769
|
+
attention: surface.attention_scope
|
|
1705
1770
|
};
|
|
1706
1771
|
}
|
|
1707
1772
|
var DomSurface = class DomSurface {
|
|
@@ -1728,7 +1793,10 @@ var DomSurface = class DomSurface {
|
|
|
1728
1793
|
this.text_edit_original = "";
|
|
1729
1794
|
this.pending_text_insert = null;
|
|
1730
1795
|
this.vector_edit = null;
|
|
1796
|
+
this.point_snap_guide = void 0;
|
|
1797
|
+
this.suppress_point_snap = false;
|
|
1731
1798
|
this.vector_edit_region_baseline = null;
|
|
1799
|
+
this.scene_marquee_baseline = null;
|
|
1732
1800
|
this.current_tool = require_model.TOOL_CURSOR;
|
|
1733
1801
|
this.pending_insert = null;
|
|
1734
1802
|
this.editor_hover_internal = null;
|
|
@@ -1737,6 +1805,10 @@ var DomSurface = class DomSurface {
|
|
|
1737
1805
|
this.fit_on_attach = options.fit === true;
|
|
1738
1806
|
this.clipboard_enabled = options.clipboard !== false;
|
|
1739
1807
|
this.attention = create_attention_tracker(container);
|
|
1808
|
+
this.attention_scope = {
|
|
1809
|
+
add: (element) => this.attention.add(element),
|
|
1810
|
+
remove: (element) => this.attention.remove(element)
|
|
1811
|
+
};
|
|
1740
1812
|
this.teardown.push(() => this.attention.dispose());
|
|
1741
1813
|
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.");
|
|
1742
1814
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
@@ -1944,6 +2016,8 @@ var DomSurface = class DomSurface {
|
|
|
1944
2016
|
this.editor_hover_internal = internal;
|
|
1945
2017
|
internal.set_content_edit_driver((id) => this.enter_content_edit(id));
|
|
1946
2018
|
this.teardown.push(() => internal.set_content_edit_driver(null));
|
|
2019
|
+
internal.register_command("transform.nudge", (args) => this.handle_nudge_command(args));
|
|
2020
|
+
this.teardown.push(() => internal.register_command("transform.nudge", require_model.default_nudge_handler(this.editor)));
|
|
1947
2021
|
internal.set_computed_resolver({
|
|
1948
2022
|
computed_property: (id, name) => {
|
|
1949
2023
|
this.flush_dom();
|
|
@@ -2131,6 +2205,7 @@ var DomSurface = class DomSurface {
|
|
|
2131
2205
|
this.vector_edit_region_baseline = null;
|
|
2132
2206
|
this.hud.setVectorSelection(null);
|
|
2133
2207
|
}
|
|
2208
|
+
this.scene_marquee_baseline = null;
|
|
2134
2209
|
this.gestures._dispose();
|
|
2135
2210
|
this.translate_orchestrator.cancel();
|
|
2136
2211
|
this.resize_orchestrator.cancel();
|
|
@@ -2462,6 +2537,7 @@ var DomSurface = class DomSurface {
|
|
|
2462
2537
|
return rects.length > 0 ? { rects } : void 0;
|
|
2463
2538
|
}
|
|
2464
2539
|
compute_snap_extra() {
|
|
2540
|
+
if (this.point_snap_guide) return (0, _grida_hud.snapGuideToHUDDraw)(this.point_snap_guide, this.editor.style.measurement_color);
|
|
2465
2541
|
const insert_guide = this.pending_insert?.phase === "drawing" ? this.pending_insert.snap_session?.last_guide : void 0;
|
|
2466
2542
|
if (insert_guide) return (0, _grida_hud.snapGuideToHUDDraw)(this.project_guide_to_screen(insert_guide), this.editor.style.measurement_color);
|
|
2467
2543
|
const guides = this.translate_orchestrator.last_guides.length > 0 ? this.translate_orchestrator.last_guides : this.resize_orchestrator.last_guides.length > 0 ? this.resize_orchestrator.last_guides : this.nudge_dwell_watcher.guides;
|
|
@@ -2549,6 +2625,10 @@ var DomSurface = class DomSurface {
|
|
|
2549
2625
|
* was canceled. */
|
|
2550
2626
|
cancel_in_flight() {
|
|
2551
2627
|
let canceled = false;
|
|
2628
|
+
if (this.scene_marquee_baseline) {
|
|
2629
|
+
this.scene_marquee_baseline = null;
|
|
2630
|
+
canceled = true;
|
|
2631
|
+
}
|
|
2552
2632
|
if (this.translate_orchestrator.has_active_session()) {
|
|
2553
2633
|
this.translate_orchestrator.cancel();
|
|
2554
2634
|
canceled = true;
|
|
@@ -2564,6 +2644,7 @@ var DomSurface = class DomSurface {
|
|
|
2564
2644
|
if (this.active_preview) {
|
|
2565
2645
|
this.active_preview.session.discard();
|
|
2566
2646
|
this.active_preview = null;
|
|
2647
|
+
this.point_snap_guide = void 0;
|
|
2567
2648
|
canceled = true;
|
|
2568
2649
|
}
|
|
2569
2650
|
if (this.pending_insert) {
|
|
@@ -3206,6 +3287,142 @@ var DomSurface = class DomSurface {
|
|
|
3206
3287
|
});
|
|
3207
3288
|
if (intent.phase === "commit") this.request_redraw();
|
|
3208
3289
|
}
|
|
3290
|
+
/**
|
|
3291
|
+
* Shift axis-lock for point-level drags (gridaco/grida#848) — the
|
|
3292
|
+
* vertex / endpoint counterpart of whole-object translate's
|
|
3293
|
+
* `axis_lock: "by_dominance"` (see `current_translate_modifiers`).
|
|
3294
|
+
* Collapses the lesser axis of a local-frame delta when Shift is held,
|
|
3295
|
+
* via the same cmath rule the translate pipeline's `axis_lock` stage
|
|
3296
|
+
* uses; identity when Shift is up. Pull-at-consume from the HUD modifier
|
|
3297
|
+
* store so a mid-drag Shift press/release reflects on the next frame.
|
|
3298
|
+
*/
|
|
3299
|
+
axis_lock_point_delta(dx, dy) {
|
|
3300
|
+
if (!this.hud.modifiers().shift) return [dx, dy];
|
|
3301
|
+
const locked = _grida_cmath.default.ext.movement.axisLockedByDominance([dx, dy]);
|
|
3302
|
+
return _grida_cmath.default.ext.movement.normalize(locked);
|
|
3303
|
+
}
|
|
3304
|
+
/**
|
|
3305
|
+
* Point-level snap (gridaco/grida#844) — the vertex / endpoint counterpart
|
|
3306
|
+
* of whole-object translate's edge/center snap. Snaps a dragged point's
|
|
3307
|
+
* delta so it aligns with (or lands on) a sibling point: a path's other
|
|
3308
|
+
* vertices, or a `<line>`'s opposite endpoint.
|
|
3309
|
+
*
|
|
3310
|
+
* Runs entirely in the element's **local frame**: agents (the moving
|
|
3311
|
+
* point[s]) and neighbors (the static sibling points) come straight from
|
|
3312
|
+
* the path's vector network / line attributes — no projection, no separate
|
|
3313
|
+
* neighbor source. The corrected delta is returned in that same local frame
|
|
3314
|
+
* so the caller applies it exactly where it already applies the local drag
|
|
3315
|
+
* delta. Only the snap GUIDE is projected to screen (via the element CTM)
|
|
3316
|
+
* for HUD rendering.
|
|
3317
|
+
*
|
|
3318
|
+
* Honors the global snap toggle (`style.snap_enabled`) and threshold
|
|
3319
|
+
* (`style.snap_threshold_px`, converted to local units by the CTM scale) —
|
|
3320
|
+
* snap off ⇒ free point dragging, per the issue. Bypassed when
|
|
3321
|
+
* `suppress_point_snap` is set (keyboard nudge). Identity when there are no
|
|
3322
|
+
* neighbors / agents.
|
|
3323
|
+
*
|
|
3324
|
+
* The shared `SnapSession` drops 0-area rects (its empty-`<g>` "jerk to
|
|
3325
|
+
* origin" defense), so each point is modeled as a sub-pixel square centered
|
|
3326
|
+
* on it. Symmetric inflation cancels in the corrected delta (the engine's
|
|
3327
|
+
* matched same-edge offset carries the same ±eps on both sides), so eps
|
|
3328
|
+
* magnitude is immaterial as long as it stays far below the threshold.
|
|
3329
|
+
*/
|
|
3330
|
+
snap_local_point_delta(raw_dx, raw_dy, agents_local, neighbors_local, ctm) {
|
|
3331
|
+
this.point_snap_guide = void 0;
|
|
3332
|
+
const style = this.editor.style;
|
|
3333
|
+
if (this.suppress_point_snap || !style.snap_enabled || agents_local.length === 0 || neighbors_local.length === 0 || raw_dx === 0 && raw_dy === 0) return [raw_dx, raw_dy];
|
|
3334
|
+
const det = ctm.a * ctm.d - ctm.c * ctm.b;
|
|
3335
|
+
const scale = Math.sqrt(Math.abs(det)) || 1;
|
|
3336
|
+
const threshold_local = style.snap_threshold_px / scale;
|
|
3337
|
+
const eps = threshold_local * 1e-6 || 1e-9;
|
|
3338
|
+
const to_rect = (p) => ({
|
|
3339
|
+
x: p[0] - eps / 2,
|
|
3340
|
+
y: p[1] - eps / 2,
|
|
3341
|
+
width: eps,
|
|
3342
|
+
height: eps
|
|
3343
|
+
});
|
|
3344
|
+
const session = new SnapSession({
|
|
3345
|
+
agents: agents_local.map(to_rect),
|
|
3346
|
+
neighbors: neighbors_local.map(to_rect)
|
|
3347
|
+
});
|
|
3348
|
+
const { delta, guide } = session.snap({
|
|
3349
|
+
x: raw_dx,
|
|
3350
|
+
y: raw_dy
|
|
3351
|
+
}, {
|
|
3352
|
+
enabled: true,
|
|
3353
|
+
threshold_px: threshold_local
|
|
3354
|
+
});
|
|
3355
|
+
session.dispose();
|
|
3356
|
+
if (guide) this.point_snap_guide = this.project_local_guide_to_screen(guide, ctm, this.container_offset());
|
|
3357
|
+
return [delta.x, delta.y];
|
|
3358
|
+
}
|
|
3359
|
+
/** Split a path's baseline vertices into the snap agents (the moving
|
|
3360
|
+
* sub-selection) and neighbors (everything else) for
|
|
3361
|
+
* {@link snap_local_point_delta}. Both in path-local space. */
|
|
3362
|
+
vertex_snap_points(model, moving) {
|
|
3363
|
+
const verts = model.snapshot().vertices;
|
|
3364
|
+
const moving_set = new Set(moving);
|
|
3365
|
+
const agents = [];
|
|
3366
|
+
const neighbors = [];
|
|
3367
|
+
for (let i = 0; i < verts.length; i++) (moving_set.has(i) ? agents : neighbors).push(verts[i]);
|
|
3368
|
+
return {
|
|
3369
|
+
agents,
|
|
3370
|
+
neighbors
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
/** Project a point-snap guide from an element's local frame to screen
|
|
3374
|
+
* CSS-px (the HUD canvas's identity coordinate system), via the element
|
|
3375
|
+
* CTM + container offset — the same projection `vector_of` uses for
|
|
3376
|
+
* vertex chrome. The local-frame analog of `project_guide_to_screen`
|
|
3377
|
+
* (which projects world-space orchestrator guides via the camera). */
|
|
3378
|
+
project_local_guide_to_screen(g, ctm, container_offset) {
|
|
3379
|
+
return {
|
|
3380
|
+
lines: g.lines.map((l) => {
|
|
3381
|
+
const [x1, y1] = project_point_through_ctm(l.x1, l.y1, ctm, container_offset);
|
|
3382
|
+
const [x2, y2] = project_point_through_ctm(l.x2, l.y2, ctm, container_offset);
|
|
3383
|
+
return {
|
|
3384
|
+
...l,
|
|
3385
|
+
x1,
|
|
3386
|
+
y1,
|
|
3387
|
+
x2,
|
|
3388
|
+
y2
|
|
3389
|
+
};
|
|
3390
|
+
}),
|
|
3391
|
+
points: g.points.map(([x, y]) => project_point_through_ctm(x, y, ctm, container_offset)),
|
|
3392
|
+
rules: g.rules.map(([axis, value]) => {
|
|
3393
|
+
const [px, py] = project_point_through_ctm(axis === "x" ? value : 0, axis === "x" ? 0 : value, ctm, container_offset);
|
|
3394
|
+
return [axis, axis === "x" ? px : py];
|
|
3395
|
+
})
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
/** Translation from screen/page CSS-px to the HUD's container-identity
|
|
3399
|
+
* space: subtract the container's page offset, add its scroll. Used at
|
|
3400
|
+
* every `getScreenCTM` projection boundary (chrome, marquee, point snap)
|
|
3401
|
+
* so they share one definition of "where the container's origin is". */
|
|
3402
|
+
container_offset() {
|
|
3403
|
+
const cr = this.container.getBoundingClientRect();
|
|
3404
|
+
return [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* Local-frame drag delta for a vertex sub-selection, from the HUD's
|
|
3408
|
+
* container-space `dx/dy`. Inverse-projects through the element CTM (so a
|
|
3409
|
+
* path under a scaled `<g>` / nested viewport tracks the cursor 1:1) then
|
|
3410
|
+
* point-snaps to the path's other vertices (#844) — before axis-lock, so
|
|
3411
|
+
* the lock keeps final say on a constrained axis. Identity when the element
|
|
3412
|
+
* has no usable CTM. A tangent-only drag (empty `indices`) yields no snap
|
|
3413
|
+
* agents, so snap is a no-op. Shared by the two vertex-translate handlers;
|
|
3414
|
+
* the caller applies axis-lock and feeds the result to `translateVertices`.
|
|
3415
|
+
*/
|
|
3416
|
+
vertex_drag_local_delta(node_id, baseline_model, indices, dx, dy) {
|
|
3417
|
+
const el = this.element_index.get(node_id);
|
|
3418
|
+
const ctm = el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function" ? el.getScreenCTM() : null;
|
|
3419
|
+
if (!ctm) return [dx, dy];
|
|
3420
|
+
let local_dx = dx;
|
|
3421
|
+
let local_dy = dy;
|
|
3422
|
+
if (ctm.a * ctm.d - ctm.c * ctm.b !== 0) [local_dx, local_dy] = project_delta_inverse_ctm(dx, dy, ctm);
|
|
3423
|
+
const { agents, neighbors } = this.vertex_snap_points(baseline_model, indices);
|
|
3424
|
+
return this.snap_local_point_delta(local_dx, local_dy, agents, neighbors, ctm);
|
|
3425
|
+
}
|
|
3209
3426
|
/** Snapshot of HUD modifier state mapped to the orchestrator's `GestureModifiers`.
|
|
3210
3427
|
* Pull-at-consume: HUD is the canonical store (see `sync_modifiers`),
|
|
3211
3428
|
* read live so mid-drag Shift press/release reflects on the next pass. */
|
|
@@ -3312,13 +3529,26 @@ var DomSurface = class DomSurface {
|
|
|
3312
3529
|
if (!pos_own) return;
|
|
3313
3530
|
const target_x = pos_own.x;
|
|
3314
3531
|
const target_y = pos_own.y;
|
|
3532
|
+
const base_x = endpoint === "p1" ? initial.x1 : initial.x2;
|
|
3533
|
+
const base_y = endpoint === "p1" ? initial.y1 : initial.y2;
|
|
3534
|
+
let dx = target_x - base_x;
|
|
3535
|
+
let dy = target_y - base_y;
|
|
3536
|
+
const el = this.element_index.get(id);
|
|
3537
|
+
const ctm = el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function" ? el.getScreenCTM() : null;
|
|
3538
|
+
if (ctm) {
|
|
3539
|
+
const other = endpoint === "p1" ? [initial.x2, initial.y2] : [initial.x1, initial.y1];
|
|
3540
|
+
[dx, dy] = this.snap_local_point_delta(dx, dy, [[base_x, base_y]], [other], ctm);
|
|
3541
|
+
}
|
|
3542
|
+
const [locked_dx, locked_dy] = this.axis_lock_point_delta(dx, dy);
|
|
3543
|
+
const final_x = base_x + locked_dx;
|
|
3544
|
+
const final_y = base_y + locked_dy;
|
|
3315
3545
|
const apply = () => {
|
|
3316
3546
|
if (endpoint === "p1") {
|
|
3317
|
-
doc.set_attr(id, "x1", String(
|
|
3318
|
-
doc.set_attr(id, "y1", String(
|
|
3547
|
+
doc.set_attr(id, "x1", String(final_x));
|
|
3548
|
+
doc.set_attr(id, "y1", String(final_y));
|
|
3319
3549
|
} else {
|
|
3320
|
-
doc.set_attr(id, "x2", String(
|
|
3321
|
-
doc.set_attr(id, "y2", String(
|
|
3550
|
+
doc.set_attr(id, "x2", String(final_x));
|
|
3551
|
+
doc.set_attr(id, "y2", String(final_y));
|
|
3322
3552
|
}
|
|
3323
3553
|
emit();
|
|
3324
3554
|
};
|
|
@@ -3335,6 +3565,7 @@ var DomSurface = class DomSurface {
|
|
|
3335
3565
|
revert
|
|
3336
3566
|
});
|
|
3337
3567
|
if (intent.phase === "commit") {
|
|
3568
|
+
this.point_snap_guide = void 0;
|
|
3338
3569
|
this.active_preview.session.commit();
|
|
3339
3570
|
this.active_preview = null;
|
|
3340
3571
|
}
|
|
@@ -3382,19 +3613,33 @@ var DomSurface = class DomSurface {
|
|
|
3382
3613
|
this.handle_marquee_vectors(intent);
|
|
3383
3614
|
return;
|
|
3384
3615
|
}
|
|
3385
|
-
if (
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
const
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3616
|
+
if (!this.scene_marquee_baseline) {
|
|
3617
|
+
const cr = this.container.getBoundingClientRect();
|
|
3618
|
+
const root = this.editor.tree().root;
|
|
3619
|
+
const boxes = [];
|
|
3620
|
+
for (const id of this._ensure_z_order(false, root)) {
|
|
3621
|
+
const box = this.container_box(id, cr);
|
|
3622
|
+
if (box) boxes.push([id, box]);
|
|
3623
|
+
}
|
|
3624
|
+
this.scene_marquee_baseline = {
|
|
3625
|
+
boxes,
|
|
3626
|
+
selection: this.editor.state.selection
|
|
3627
|
+
};
|
|
3396
3628
|
}
|
|
3397
|
-
this.
|
|
3629
|
+
const next = this.resolve_scene_marquee(intent.rect, intent.additive);
|
|
3630
|
+
this.editor.commands.select(next, { mode: "replace" });
|
|
3631
|
+
if (intent.phase === "commit") this.scene_marquee_baseline = null;
|
|
3632
|
+
}
|
|
3633
|
+
/** Resolve the scene marquee selection for one frame from the frozen,
|
|
3634
|
+
* paint-ordered box snapshot. The rule (shadow + additive) lives in the
|
|
3635
|
+
* headless `marquee_selection` policy (`src/selection/marquee.ts`, spec
|
|
3636
|
+
* `docs/marquee-selection.md`); the surface only supplies the boxes and
|
|
3637
|
+
* the gesture-start baseline. Selection is deterministic in (marquee rect,
|
|
3638
|
+
* gesture-start selection, shift) — meta is a gesture-routing modifier
|
|
3639
|
+
* only (it decides that a drag IS a marquee), not a resolution input. */
|
|
3640
|
+
resolve_scene_marquee(rect, additive) {
|
|
3641
|
+
const { boxes, selection } = this.scene_marquee_baseline;
|
|
3642
|
+
return marquee_selection.resolve(boxes, rect, selection, { additive });
|
|
3398
3643
|
}
|
|
3399
3644
|
/**
|
|
3400
3645
|
* Vector marquee predicate — applies the **vertex-priority precedence
|
|
@@ -3425,8 +3670,7 @@ var DomSurface = class DomSurface {
|
|
|
3425
3670
|
if (typeof el.getScreenCTM !== "function") return;
|
|
3426
3671
|
const ctm = el.getScreenCTM();
|
|
3427
3672
|
if (!ctm) return;
|
|
3428
|
-
const
|
|
3429
|
-
const offset = [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
3673
|
+
const offset = this.container_offset();
|
|
3430
3674
|
const model = this.session_model();
|
|
3431
3675
|
if (!model) return;
|
|
3432
3676
|
const rect = intent.rect;
|
|
@@ -3489,8 +3733,7 @@ var DomSurface = class DomSurface {
|
|
|
3489
3733
|
if (typeof el.getScreenCTM !== "function") return;
|
|
3490
3734
|
const ctm = el.getScreenCTM();
|
|
3491
3735
|
if (!ctm) return;
|
|
3492
|
-
const
|
|
3493
|
-
const offset = [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
3736
|
+
const offset = this.container_offset();
|
|
3494
3737
|
const model = this.session_model();
|
|
3495
3738
|
if (!model) return;
|
|
3496
3739
|
const polygon = intent.polygon;
|
|
@@ -3795,8 +4038,7 @@ var DomSurface = class DomSurface {
|
|
|
3795
4038
|
if (typeof el.getScreenCTM !== "function") return null;
|
|
3796
4039
|
const ctm = el.getScreenCTM();
|
|
3797
4040
|
if (!ctm) return null;
|
|
3798
|
-
const
|
|
3799
|
-
const offset = [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
4041
|
+
const offset = this.container_offset();
|
|
3800
4042
|
return {
|
|
3801
4043
|
vertices: Array.from({ length: snap.vertices.length }, (_, i) => {
|
|
3802
4044
|
const v = snap.vertices[i];
|
|
@@ -4044,19 +4286,13 @@ var DomSurface = class DomSurface {
|
|
|
4044
4286
|
}
|
|
4045
4287
|
const baseline_d = this.active_preview.initial_d;
|
|
4046
4288
|
const indices = this.active_preview.indices;
|
|
4047
|
-
let local_dx = intent.dx;
|
|
4048
|
-
|
|
4049
|
-
const el = this.element_index.get(node_id);
|
|
4050
|
-
if (el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function") {
|
|
4051
|
-
const ctm = el.getScreenCTM();
|
|
4052
|
-
if (ctm) {
|
|
4053
|
-
if (ctm.a * ctm.d - ctm.c * ctm.b !== 0) [local_dx, local_dy] = project_delta_inverse_ctm(intent.dx, intent.dy, ctm);
|
|
4054
|
-
}
|
|
4055
|
-
}
|
|
4289
|
+
let [local_dx, local_dy] = this.vertex_drag_local_delta(node_id, this.active_preview.baseline_model, indices, intent.dx, intent.dy);
|
|
4290
|
+
[local_dx, local_dy] = this.axis_lock_point_delta(local_dx, local_dy);
|
|
4056
4291
|
const preview_model = this.active_preview.baseline_model.translateVertices(indices, [local_dx, local_dy]);
|
|
4057
4292
|
const target_d = preview_model.toSvgPathD();
|
|
4058
4293
|
this.active_preview.preview_model = preview_model;
|
|
4059
4294
|
if (intent.phase === "commit") {
|
|
4295
|
+
this.point_snap_guide = void 0;
|
|
4060
4296
|
const before_selection = this.active_preview.before_selection;
|
|
4061
4297
|
const after_selection = this.vector_edit.snapshot_selection();
|
|
4062
4298
|
this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, after_selection, before_selection));
|
|
@@ -4143,15 +4379,8 @@ var DomSurface = class DomSurface {
|
|
|
4143
4379
|
};
|
|
4144
4380
|
}
|
|
4145
4381
|
const baseline_d = this.active_preview.initial_d;
|
|
4146
|
-
let local_dx = intent.dx;
|
|
4147
|
-
|
|
4148
|
-
const el = this.element_index.get(node_id);
|
|
4149
|
-
if (el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function") {
|
|
4150
|
-
const ctm = el.getScreenCTM();
|
|
4151
|
-
if (ctm) {
|
|
4152
|
-
if (ctm.a * ctm.d - ctm.c * ctm.b !== 0) [local_dx, local_dy] = project_delta_inverse_ctm(intent.dx, intent.dy, ctm);
|
|
4153
|
-
}
|
|
4154
|
-
}
|
|
4382
|
+
let [local_dx, local_dy] = this.vertex_drag_local_delta(node_id, this.active_preview.baseline_model, indices, intent.dx, intent.dy);
|
|
4383
|
+
[local_dx, local_dy] = this.axis_lock_point_delta(local_dx, local_dy);
|
|
4155
4384
|
const baseline_model = this.active_preview.baseline_model;
|
|
4156
4385
|
const baseline_tangent_abs = this.active_preview.baseline_tangent_abs;
|
|
4157
4386
|
let preview_model = indices.length > 0 ? baseline_model.translateVertices(indices, [local_dx, local_dy]) : baseline_model;
|
|
@@ -4163,6 +4392,7 @@ var DomSurface = class DomSurface {
|
|
|
4163
4392
|
const target_d = preview_model.toSvgPathD();
|
|
4164
4393
|
this.active_preview.preview_model = preview_model;
|
|
4165
4394
|
if (intent.phase === "commit") {
|
|
4395
|
+
this.point_snap_guide = void 0;
|
|
4166
4396
|
const before_selection = this.active_preview.before_selection;
|
|
4167
4397
|
const after_selection = this.vector_edit.snapshot_selection();
|
|
4168
4398
|
this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, after_selection, before_selection));
|
|
@@ -4170,6 +4400,63 @@ var DomSurface = class DomSurface {
|
|
|
4170
4400
|
this.active_preview = null;
|
|
4171
4401
|
} else this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, null, null));
|
|
4172
4402
|
}
|
|
4403
|
+
/**
|
|
4404
|
+
* Surface-aware `transform.nudge` handler — registered over the headless
|
|
4405
|
+
* default on attach (gridaco/grida#849). In vector content-edit, arrow
|
|
4406
|
+
* keys nudge the path sub-selection (the keyboard counterpart of
|
|
4407
|
+
* dragging vertices / segments / tangents) rather than leaking to a
|
|
4408
|
+
* whole-element move (the bug). This mirrors text-edit, where the inline
|
|
4409
|
+
* editor owns the arrow keys. Outside content-edit it delegates to the
|
|
4410
|
+
* headless `default_nudge_handler` (whole-element nudge) — the same
|
|
4411
|
+
* handler the registry restores on detach, so the override and the
|
|
4412
|
+
* default can never drift.
|
|
4413
|
+
*
|
|
4414
|
+
* While a vector session is open the arrows are OWNED by it: an empty
|
|
4415
|
+
* sub-selection is a consumed no-op, never a whole-element nudge — same
|
|
4416
|
+
* "content-edit captures arrows" contract as text-edit.
|
|
4417
|
+
*/
|
|
4418
|
+
handle_nudge_command(args) {
|
|
4419
|
+
const { dx, dy } = args;
|
|
4420
|
+
if (this.editor.state.mode === "edit-content" && this.vector_edit) {
|
|
4421
|
+
this.nudge_vector_selection(dx, dy);
|
|
4422
|
+
return true;
|
|
4423
|
+
}
|
|
4424
|
+
return require_model.default_nudge_handler(this.editor)(args) === true;
|
|
4425
|
+
}
|
|
4426
|
+
/**
|
|
4427
|
+
* Discrete keyboard nudge of the vector sub-selection. Reuses the drag
|
|
4428
|
+
* path ({@link handle_translate_vector_selection}) by synthesizing a
|
|
4429
|
+
* single commit-phase intent, so a nudge is byte-identical to a one-step
|
|
4430
|
+
* drag of the same points: same union resolution (selected vertices ∪
|
|
4431
|
+
* selected-segment endpoints ∪ tangents), same parent-vertex tangent
|
|
4432
|
+
* exclusion, same history bracket + sub-selection capture.
|
|
4433
|
+
*
|
|
4434
|
+
* `dx`/`dy` arrive in world units (1px, or 10px with Shift — already
|
|
4435
|
+
* resolved by the keymap binding). `handle_translate_vector_selection`
|
|
4436
|
+
* expects container CSS-px (it projects back through the inverse
|
|
4437
|
+
* screen-CTM), so scale by the camera zoom on the way in; the projection
|
|
4438
|
+
* recovers the world delta in the path's local frame. A no-sub-selection
|
|
4439
|
+
* session resolves to a no-op inside the handler.
|
|
4440
|
+
*/
|
|
4441
|
+
nudge_vector_selection(dx, dy) {
|
|
4442
|
+
const ses = this.vector_edit;
|
|
4443
|
+
if (!ses) return;
|
|
4444
|
+
const zoom = this.camera.zoom || 1;
|
|
4445
|
+
this.suppress_point_snap = true;
|
|
4446
|
+
try {
|
|
4447
|
+
this.handle_translate_vector_selection({
|
|
4448
|
+
kind: "translate_vector_selection",
|
|
4449
|
+
node_id: ses.node_id,
|
|
4450
|
+
additional_vertex_indices: [],
|
|
4451
|
+
dx: dx * zoom,
|
|
4452
|
+
dy: dy * zoom,
|
|
4453
|
+
phase: "commit"
|
|
4454
|
+
});
|
|
4455
|
+
} finally {
|
|
4456
|
+
this.suppress_point_snap = false;
|
|
4457
|
+
}
|
|
4458
|
+
this.request_redraw();
|
|
4459
|
+
}
|
|
4173
4460
|
/** Mirror handler for `select_tangent` (analogous to `select_vertex`). */
|
|
4174
4461
|
handle_select_tangent(intent) {
|
|
4175
4462
|
if (!this.vector_edit || this.vector_edit.node_id !== intent.node_id) return;
|