@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
|
-
import { S as is_text_input_focused, a as paint, c as hit_shape_svg, d as NudgeDwellWatcher, f as TranslateOrchestrator, h as transform, i as TOOL_CURSOR, l as RotateOrchestrator, m as group, n as insertions, o as ResizeOrchestrator, s as resize_pipeline, t as PathModel, x as array_shallow_equal } from "./model-
|
|
1
|
+
import { S as is_text_input_focused, a as paint, c as hit_shape_svg, d as NudgeDwellWatcher, f as TranslateOrchestrator, h as transform, i as TOOL_CURSOR, l as RotateOrchestrator, m as group, n as insertions, o as ResizeOrchestrator, s as resize_pipeline, t as PathModel, w as default_nudge_handler, x as array_shallow_equal } from "./model-DS5MxDrd.mjs";
|
|
2
2
|
import cmath from "@grida/cmath";
|
|
3
3
|
import { svg_parse } from "@grida/svg/parse";
|
|
4
4
|
import { SVGShapes } from "@grida/svg/pathdata";
|
|
@@ -757,6 +757,35 @@ function resolve_text_exit(input) {
|
|
|
757
757
|
return { kind: "noop" };
|
|
758
758
|
}
|
|
759
759
|
//#endregion
|
|
760
|
+
//#region src/selection/marquee.ts
|
|
761
|
+
let marquee_selection;
|
|
762
|
+
(function(_marquee_selection) {
|
|
763
|
+
function hits(boxes, rect) {
|
|
764
|
+
const touched = boxes.filter(([, box]) => cmath.rect.intersects(box, rect));
|
|
765
|
+
const front = touched.length - 1;
|
|
766
|
+
const out = [];
|
|
767
|
+
for (let i = 0; i < touched.length; i++) {
|
|
768
|
+
const [id, box] = touched[i];
|
|
769
|
+
if (i !== front && cmath.rect.contains(box, rect)) continue;
|
|
770
|
+
out.push(id);
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
_marquee_selection.hits = hits;
|
|
775
|
+
function resolve(boxes, rect, baseline, opts = {}) {
|
|
776
|
+
const h = hits(boxes, rect);
|
|
777
|
+
if (!opts.additive) return h;
|
|
778
|
+
const out = [...baseline];
|
|
779
|
+
const seen = new Set(baseline);
|
|
780
|
+
for (const id of h) if (!seen.has(id)) {
|
|
781
|
+
seen.add(id);
|
|
782
|
+
out.push(id);
|
|
783
|
+
}
|
|
784
|
+
return out;
|
|
785
|
+
}
|
|
786
|
+
_marquee_selection.resolve = resolve;
|
|
787
|
+
})(marquee_selection || (marquee_selection = {}));
|
|
788
|
+
//#endregion
|
|
760
789
|
//#region src/gestures/gestures.ts
|
|
761
790
|
/**
|
|
762
791
|
* Sibling to `Keymap`. Owns a list of installed gesture bindings; each
|
|
@@ -993,33 +1022,68 @@ function applyDefaultGestures(gestures) {
|
|
|
993
1022
|
* Install pointer-tracking listeners on `container` and return the
|
|
994
1023
|
* read-side handle. The tracker is owned by the surface and disposed
|
|
995
1024
|
* alongside it; gesture bindings that need to consult it receive the
|
|
996
|
-
* read-only `is_attended` predicate through `GestureContext`.
|
|
1025
|
+
* read-only `is_attended` predicate through `GestureContext`. Hosts
|
|
1026
|
+
* extend the scope through `handle.attention` (`dom.ts`), which fronts
|
|
1027
|
+
* {@link AttentionTracker.add} / {@link AttentionTracker.remove}.
|
|
997
1028
|
*/
|
|
998
1029
|
function create_attention_tracker(container) {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1030
|
+
/** Elements of the scope the pointer is currently over. Per-element
|
|
1031
|
+
* membership (not a single boolean) so crossing from the container
|
|
1032
|
+
* onto overlapping registered chrome — `leave` and `enter` firing in
|
|
1033
|
+
* either order — never reads as a gap in attention. */
|
|
1034
|
+
const hovered = /* @__PURE__ */ new Set();
|
|
1035
|
+
/** Registered extras → their hover-tracking teardown. */
|
|
1036
|
+
const extras = /* @__PURE__ */ new Map();
|
|
1037
|
+
let disposed = false;
|
|
1038
|
+
/** Start hover-tracking `element`; returns the exact undo. */
|
|
1039
|
+
const track = (element) => {
|
|
1040
|
+
const enter = () => {
|
|
1041
|
+
hovered.add(element);
|
|
1042
|
+
};
|
|
1043
|
+
const leave = () => {
|
|
1044
|
+
hovered.delete(element);
|
|
1045
|
+
};
|
|
1046
|
+
element.addEventListener("pointerenter", enter);
|
|
1047
|
+
element.addEventListener("pointerleave", leave);
|
|
1048
|
+
return () => {
|
|
1049
|
+
element.removeEventListener("pointerenter", enter);
|
|
1050
|
+
element.removeEventListener("pointerleave", leave);
|
|
1051
|
+
hovered.delete(element);
|
|
1052
|
+
};
|
|
1005
1053
|
};
|
|
1006
|
-
container
|
|
1007
|
-
container.addEventListener("pointerleave", on_leave);
|
|
1054
|
+
const untrack_container = track(container);
|
|
1008
1055
|
const is_focus_within = () => {
|
|
1009
1056
|
const owner = container.ownerDocument;
|
|
1010
1057
|
if (!owner) return false;
|
|
1011
1058
|
const active = owner.activeElement;
|
|
1012
|
-
|
|
1059
|
+
if (!active || active === owner.body) return false;
|
|
1060
|
+
if (container.contains(active)) return true;
|
|
1061
|
+
for (const element of extras.keys()) if (element.contains(active)) return true;
|
|
1062
|
+
return false;
|
|
1013
1063
|
};
|
|
1014
1064
|
const is_attended = () => {
|
|
1015
|
-
return is_focus_within()
|
|
1065
|
+
return hovered.size > 0 || is_focus_within();
|
|
1016
1066
|
};
|
|
1017
1067
|
return {
|
|
1018
1068
|
is_attended,
|
|
1019
1069
|
is_focus_within,
|
|
1070
|
+
add: (element) => {
|
|
1071
|
+
if (disposed) return;
|
|
1072
|
+
if (element === container || extras.has(element)) return;
|
|
1073
|
+
extras.set(element, track(element));
|
|
1074
|
+
if (typeof element.matches === "function" && element.matches(":hover")) hovered.add(element);
|
|
1075
|
+
},
|
|
1076
|
+
remove: (element) => {
|
|
1077
|
+
const untrack = extras.get(element);
|
|
1078
|
+
if (!untrack) return;
|
|
1079
|
+
extras.delete(element);
|
|
1080
|
+
untrack();
|
|
1081
|
+
},
|
|
1020
1082
|
dispose: () => {
|
|
1021
|
-
|
|
1022
|
-
|
|
1083
|
+
disposed = true;
|
|
1084
|
+
untrack_container();
|
|
1085
|
+
for (const untrack of extras.values()) untrack();
|
|
1086
|
+
extras.clear();
|
|
1023
1087
|
}
|
|
1024
1088
|
};
|
|
1025
1089
|
}
|
|
@@ -1699,7 +1763,8 @@ function attach_dom_surface(editor, options) {
|
|
|
1699
1763
|
inner.detach();
|
|
1700
1764
|
},
|
|
1701
1765
|
camera: surface.camera,
|
|
1702
|
-
gestures: surface.gestures
|
|
1766
|
+
gestures: surface.gestures,
|
|
1767
|
+
attention: surface.attention_scope
|
|
1703
1768
|
};
|
|
1704
1769
|
}
|
|
1705
1770
|
var DomSurface = class DomSurface {
|
|
@@ -1726,7 +1791,10 @@ var DomSurface = class DomSurface {
|
|
|
1726
1791
|
this.text_edit_original = "";
|
|
1727
1792
|
this.pending_text_insert = null;
|
|
1728
1793
|
this.vector_edit = null;
|
|
1794
|
+
this.point_snap_guide = void 0;
|
|
1795
|
+
this.suppress_point_snap = false;
|
|
1729
1796
|
this.vector_edit_region_baseline = null;
|
|
1797
|
+
this.scene_marquee_baseline = null;
|
|
1730
1798
|
this.current_tool = TOOL_CURSOR;
|
|
1731
1799
|
this.pending_insert = null;
|
|
1732
1800
|
this.editor_hover_internal = null;
|
|
@@ -1735,6 +1803,10 @@ var DomSurface = class DomSurface {
|
|
|
1735
1803
|
this.fit_on_attach = options.fit === true;
|
|
1736
1804
|
this.clipboard_enabled = options.clipboard !== false;
|
|
1737
1805
|
this.attention = create_attention_tracker(container);
|
|
1806
|
+
this.attention_scope = {
|
|
1807
|
+
add: (element) => this.attention.add(element),
|
|
1808
|
+
remove: (element) => this.attention.remove(element)
|
|
1809
|
+
};
|
|
1738
1810
|
this.teardown.push(() => this.attention.dispose());
|
|
1739
1811
|
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.");
|
|
1740
1812
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
@@ -1942,6 +2014,8 @@ var DomSurface = class DomSurface {
|
|
|
1942
2014
|
this.editor_hover_internal = internal;
|
|
1943
2015
|
internal.set_content_edit_driver((id) => this.enter_content_edit(id));
|
|
1944
2016
|
this.teardown.push(() => internal.set_content_edit_driver(null));
|
|
2017
|
+
internal.register_command("transform.nudge", (args) => this.handle_nudge_command(args));
|
|
2018
|
+
this.teardown.push(() => internal.register_command("transform.nudge", default_nudge_handler(this.editor)));
|
|
1945
2019
|
internal.set_computed_resolver({
|
|
1946
2020
|
computed_property: (id, name) => {
|
|
1947
2021
|
this.flush_dom();
|
|
@@ -2129,6 +2203,7 @@ var DomSurface = class DomSurface {
|
|
|
2129
2203
|
this.vector_edit_region_baseline = null;
|
|
2130
2204
|
this.hud.setVectorSelection(null);
|
|
2131
2205
|
}
|
|
2206
|
+
this.scene_marquee_baseline = null;
|
|
2132
2207
|
this.gestures._dispose();
|
|
2133
2208
|
this.translate_orchestrator.cancel();
|
|
2134
2209
|
this.resize_orchestrator.cancel();
|
|
@@ -2460,6 +2535,7 @@ var DomSurface = class DomSurface {
|
|
|
2460
2535
|
return rects.length > 0 ? { rects } : void 0;
|
|
2461
2536
|
}
|
|
2462
2537
|
compute_snap_extra() {
|
|
2538
|
+
if (this.point_snap_guide) return snapGuideToHUDDraw(this.point_snap_guide, this.editor.style.measurement_color);
|
|
2463
2539
|
const insert_guide = this.pending_insert?.phase === "drawing" ? this.pending_insert.snap_session?.last_guide : void 0;
|
|
2464
2540
|
if (insert_guide) return snapGuideToHUDDraw(this.project_guide_to_screen(insert_guide), this.editor.style.measurement_color);
|
|
2465
2541
|
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;
|
|
@@ -2547,6 +2623,10 @@ var DomSurface = class DomSurface {
|
|
|
2547
2623
|
* was canceled. */
|
|
2548
2624
|
cancel_in_flight() {
|
|
2549
2625
|
let canceled = false;
|
|
2626
|
+
if (this.scene_marquee_baseline) {
|
|
2627
|
+
this.scene_marquee_baseline = null;
|
|
2628
|
+
canceled = true;
|
|
2629
|
+
}
|
|
2550
2630
|
if (this.translate_orchestrator.has_active_session()) {
|
|
2551
2631
|
this.translate_orchestrator.cancel();
|
|
2552
2632
|
canceled = true;
|
|
@@ -2562,6 +2642,7 @@ var DomSurface = class DomSurface {
|
|
|
2562
2642
|
if (this.active_preview) {
|
|
2563
2643
|
this.active_preview.session.discard();
|
|
2564
2644
|
this.active_preview = null;
|
|
2645
|
+
this.point_snap_guide = void 0;
|
|
2565
2646
|
canceled = true;
|
|
2566
2647
|
}
|
|
2567
2648
|
if (this.pending_insert) {
|
|
@@ -3204,6 +3285,142 @@ var DomSurface = class DomSurface {
|
|
|
3204
3285
|
});
|
|
3205
3286
|
if (intent.phase === "commit") this.request_redraw();
|
|
3206
3287
|
}
|
|
3288
|
+
/**
|
|
3289
|
+
* Shift axis-lock for point-level drags (gridaco/grida#848) — the
|
|
3290
|
+
* vertex / endpoint counterpart of whole-object translate's
|
|
3291
|
+
* `axis_lock: "by_dominance"` (see `current_translate_modifiers`).
|
|
3292
|
+
* Collapses the lesser axis of a local-frame delta when Shift is held,
|
|
3293
|
+
* via the same cmath rule the translate pipeline's `axis_lock` stage
|
|
3294
|
+
* uses; identity when Shift is up. Pull-at-consume from the HUD modifier
|
|
3295
|
+
* store so a mid-drag Shift press/release reflects on the next frame.
|
|
3296
|
+
*/
|
|
3297
|
+
axis_lock_point_delta(dx, dy) {
|
|
3298
|
+
if (!this.hud.modifiers().shift) return [dx, dy];
|
|
3299
|
+
const locked = cmath.ext.movement.axisLockedByDominance([dx, dy]);
|
|
3300
|
+
return cmath.ext.movement.normalize(locked);
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* Point-level snap (gridaco/grida#844) — the vertex / endpoint counterpart
|
|
3304
|
+
* of whole-object translate's edge/center snap. Snaps a dragged point's
|
|
3305
|
+
* delta so it aligns with (or lands on) a sibling point: a path's other
|
|
3306
|
+
* vertices, or a `<line>`'s opposite endpoint.
|
|
3307
|
+
*
|
|
3308
|
+
* Runs entirely in the element's **local frame**: agents (the moving
|
|
3309
|
+
* point[s]) and neighbors (the static sibling points) come straight from
|
|
3310
|
+
* the path's vector network / line attributes — no projection, no separate
|
|
3311
|
+
* neighbor source. The corrected delta is returned in that same local frame
|
|
3312
|
+
* so the caller applies it exactly where it already applies the local drag
|
|
3313
|
+
* delta. Only the snap GUIDE is projected to screen (via the element CTM)
|
|
3314
|
+
* for HUD rendering.
|
|
3315
|
+
*
|
|
3316
|
+
* Honors the global snap toggle (`style.snap_enabled`) and threshold
|
|
3317
|
+
* (`style.snap_threshold_px`, converted to local units by the CTM scale) —
|
|
3318
|
+
* snap off ⇒ free point dragging, per the issue. Bypassed when
|
|
3319
|
+
* `suppress_point_snap` is set (keyboard nudge). Identity when there are no
|
|
3320
|
+
* neighbors / agents.
|
|
3321
|
+
*
|
|
3322
|
+
* The shared `SnapSession` drops 0-area rects (its empty-`<g>` "jerk to
|
|
3323
|
+
* origin" defense), so each point is modeled as a sub-pixel square centered
|
|
3324
|
+
* on it. Symmetric inflation cancels in the corrected delta (the engine's
|
|
3325
|
+
* matched same-edge offset carries the same ±eps on both sides), so eps
|
|
3326
|
+
* magnitude is immaterial as long as it stays far below the threshold.
|
|
3327
|
+
*/
|
|
3328
|
+
snap_local_point_delta(raw_dx, raw_dy, agents_local, neighbors_local, ctm) {
|
|
3329
|
+
this.point_snap_guide = void 0;
|
|
3330
|
+
const style = this.editor.style;
|
|
3331
|
+
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];
|
|
3332
|
+
const det = ctm.a * ctm.d - ctm.c * ctm.b;
|
|
3333
|
+
const scale = Math.sqrt(Math.abs(det)) || 1;
|
|
3334
|
+
const threshold_local = style.snap_threshold_px / scale;
|
|
3335
|
+
const eps = threshold_local * 1e-6 || 1e-9;
|
|
3336
|
+
const to_rect = (p) => ({
|
|
3337
|
+
x: p[0] - eps / 2,
|
|
3338
|
+
y: p[1] - eps / 2,
|
|
3339
|
+
width: eps,
|
|
3340
|
+
height: eps
|
|
3341
|
+
});
|
|
3342
|
+
const session = new SnapSession({
|
|
3343
|
+
agents: agents_local.map(to_rect),
|
|
3344
|
+
neighbors: neighbors_local.map(to_rect)
|
|
3345
|
+
});
|
|
3346
|
+
const { delta, guide } = session.snap({
|
|
3347
|
+
x: raw_dx,
|
|
3348
|
+
y: raw_dy
|
|
3349
|
+
}, {
|
|
3350
|
+
enabled: true,
|
|
3351
|
+
threshold_px: threshold_local
|
|
3352
|
+
});
|
|
3353
|
+
session.dispose();
|
|
3354
|
+
if (guide) this.point_snap_guide = this.project_local_guide_to_screen(guide, ctm, this.container_offset());
|
|
3355
|
+
return [delta.x, delta.y];
|
|
3356
|
+
}
|
|
3357
|
+
/** Split a path's baseline vertices into the snap agents (the moving
|
|
3358
|
+
* sub-selection) and neighbors (everything else) for
|
|
3359
|
+
* {@link snap_local_point_delta}. Both in path-local space. */
|
|
3360
|
+
vertex_snap_points(model, moving) {
|
|
3361
|
+
const verts = model.snapshot().vertices;
|
|
3362
|
+
const moving_set = new Set(moving);
|
|
3363
|
+
const agents = [];
|
|
3364
|
+
const neighbors = [];
|
|
3365
|
+
for (let i = 0; i < verts.length; i++) (moving_set.has(i) ? agents : neighbors).push(verts[i]);
|
|
3366
|
+
return {
|
|
3367
|
+
agents,
|
|
3368
|
+
neighbors
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
/** Project a point-snap guide from an element's local frame to screen
|
|
3372
|
+
* CSS-px (the HUD canvas's identity coordinate system), via the element
|
|
3373
|
+
* CTM + container offset — the same projection `vector_of` uses for
|
|
3374
|
+
* vertex chrome. The local-frame analog of `project_guide_to_screen`
|
|
3375
|
+
* (which projects world-space orchestrator guides via the camera). */
|
|
3376
|
+
project_local_guide_to_screen(g, ctm, container_offset) {
|
|
3377
|
+
return {
|
|
3378
|
+
lines: g.lines.map((l) => {
|
|
3379
|
+
const [x1, y1] = project_point_through_ctm(l.x1, l.y1, ctm, container_offset);
|
|
3380
|
+
const [x2, y2] = project_point_through_ctm(l.x2, l.y2, ctm, container_offset);
|
|
3381
|
+
return {
|
|
3382
|
+
...l,
|
|
3383
|
+
x1,
|
|
3384
|
+
y1,
|
|
3385
|
+
x2,
|
|
3386
|
+
y2
|
|
3387
|
+
};
|
|
3388
|
+
}),
|
|
3389
|
+
points: g.points.map(([x, y]) => project_point_through_ctm(x, y, ctm, container_offset)),
|
|
3390
|
+
rules: g.rules.map(([axis, value]) => {
|
|
3391
|
+
const [px, py] = project_point_through_ctm(axis === "x" ? value : 0, axis === "x" ? 0 : value, ctm, container_offset);
|
|
3392
|
+
return [axis, axis === "x" ? px : py];
|
|
3393
|
+
})
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
/** Translation from screen/page CSS-px to the HUD's container-identity
|
|
3397
|
+
* space: subtract the container's page offset, add its scroll. Used at
|
|
3398
|
+
* every `getScreenCTM` projection boundary (chrome, marquee, point snap)
|
|
3399
|
+
* so they share one definition of "where the container's origin is". */
|
|
3400
|
+
container_offset() {
|
|
3401
|
+
const cr = this.container.getBoundingClientRect();
|
|
3402
|
+
return [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Local-frame drag delta for a vertex sub-selection, from the HUD's
|
|
3406
|
+
* container-space `dx/dy`. Inverse-projects through the element CTM (so a
|
|
3407
|
+
* path under a scaled `<g>` / nested viewport tracks the cursor 1:1) then
|
|
3408
|
+
* point-snaps to the path's other vertices (#844) — before axis-lock, so
|
|
3409
|
+
* the lock keeps final say on a constrained axis. Identity when the element
|
|
3410
|
+
* has no usable CTM. A tangent-only drag (empty `indices`) yields no snap
|
|
3411
|
+
* agents, so snap is a no-op. Shared by the two vertex-translate handlers;
|
|
3412
|
+
* the caller applies axis-lock and feeds the result to `translateVertices`.
|
|
3413
|
+
*/
|
|
3414
|
+
vertex_drag_local_delta(node_id, baseline_model, indices, dx, dy) {
|
|
3415
|
+
const el = this.element_index.get(node_id);
|
|
3416
|
+
const ctm = el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function" ? el.getScreenCTM() : null;
|
|
3417
|
+
if (!ctm) return [dx, dy];
|
|
3418
|
+
let local_dx = dx;
|
|
3419
|
+
let local_dy = dy;
|
|
3420
|
+
if (ctm.a * ctm.d - ctm.c * ctm.b !== 0) [local_dx, local_dy] = project_delta_inverse_ctm(dx, dy, ctm);
|
|
3421
|
+
const { agents, neighbors } = this.vertex_snap_points(baseline_model, indices);
|
|
3422
|
+
return this.snap_local_point_delta(local_dx, local_dy, agents, neighbors, ctm);
|
|
3423
|
+
}
|
|
3207
3424
|
/** Snapshot of HUD modifier state mapped to the orchestrator's `GestureModifiers`.
|
|
3208
3425
|
* Pull-at-consume: HUD is the canonical store (see `sync_modifiers`),
|
|
3209
3426
|
* read live so mid-drag Shift press/release reflects on the next pass. */
|
|
@@ -3310,13 +3527,26 @@ var DomSurface = class DomSurface {
|
|
|
3310
3527
|
if (!pos_own) return;
|
|
3311
3528
|
const target_x = pos_own.x;
|
|
3312
3529
|
const target_y = pos_own.y;
|
|
3530
|
+
const base_x = endpoint === "p1" ? initial.x1 : initial.x2;
|
|
3531
|
+
const base_y = endpoint === "p1" ? initial.y1 : initial.y2;
|
|
3532
|
+
let dx = target_x - base_x;
|
|
3533
|
+
let dy = target_y - base_y;
|
|
3534
|
+
const el = this.element_index.get(id);
|
|
3535
|
+
const ctm = el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function" ? el.getScreenCTM() : null;
|
|
3536
|
+
if (ctm) {
|
|
3537
|
+
const other = endpoint === "p1" ? [initial.x2, initial.y2] : [initial.x1, initial.y1];
|
|
3538
|
+
[dx, dy] = this.snap_local_point_delta(dx, dy, [[base_x, base_y]], [other], ctm);
|
|
3539
|
+
}
|
|
3540
|
+
const [locked_dx, locked_dy] = this.axis_lock_point_delta(dx, dy);
|
|
3541
|
+
const final_x = base_x + locked_dx;
|
|
3542
|
+
const final_y = base_y + locked_dy;
|
|
3313
3543
|
const apply = () => {
|
|
3314
3544
|
if (endpoint === "p1") {
|
|
3315
|
-
doc.set_attr(id, "x1", String(
|
|
3316
|
-
doc.set_attr(id, "y1", String(
|
|
3545
|
+
doc.set_attr(id, "x1", String(final_x));
|
|
3546
|
+
doc.set_attr(id, "y1", String(final_y));
|
|
3317
3547
|
} else {
|
|
3318
|
-
doc.set_attr(id, "x2", String(
|
|
3319
|
-
doc.set_attr(id, "y2", String(
|
|
3548
|
+
doc.set_attr(id, "x2", String(final_x));
|
|
3549
|
+
doc.set_attr(id, "y2", String(final_y));
|
|
3320
3550
|
}
|
|
3321
3551
|
emit();
|
|
3322
3552
|
};
|
|
@@ -3333,6 +3563,7 @@ var DomSurface = class DomSurface {
|
|
|
3333
3563
|
revert
|
|
3334
3564
|
});
|
|
3335
3565
|
if (intent.phase === "commit") {
|
|
3566
|
+
this.point_snap_guide = void 0;
|
|
3336
3567
|
this.active_preview.session.commit();
|
|
3337
3568
|
this.active_preview = null;
|
|
3338
3569
|
}
|
|
@@ -3380,19 +3611,33 @@ var DomSurface = class DomSurface {
|
|
|
3380
3611
|
this.handle_marquee_vectors(intent);
|
|
3381
3612
|
return;
|
|
3382
3613
|
}
|
|
3383
|
-
if (
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
const
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3614
|
+
if (!this.scene_marquee_baseline) {
|
|
3615
|
+
const cr = this.container.getBoundingClientRect();
|
|
3616
|
+
const root = this.editor.tree().root;
|
|
3617
|
+
const boxes = [];
|
|
3618
|
+
for (const id of this._ensure_z_order(false, root)) {
|
|
3619
|
+
const box = this.container_box(id, cr);
|
|
3620
|
+
if (box) boxes.push([id, box]);
|
|
3621
|
+
}
|
|
3622
|
+
this.scene_marquee_baseline = {
|
|
3623
|
+
boxes,
|
|
3624
|
+
selection: this.editor.state.selection
|
|
3625
|
+
};
|
|
3394
3626
|
}
|
|
3395
|
-
this.
|
|
3627
|
+
const next = this.resolve_scene_marquee(intent.rect, intent.additive);
|
|
3628
|
+
this.editor.commands.select(next, { mode: "replace" });
|
|
3629
|
+
if (intent.phase === "commit") this.scene_marquee_baseline = null;
|
|
3630
|
+
}
|
|
3631
|
+
/** Resolve the scene marquee selection for one frame from the frozen,
|
|
3632
|
+
* paint-ordered box snapshot. The rule (shadow + additive) lives in the
|
|
3633
|
+
* headless `marquee_selection` policy (`src/selection/marquee.ts`, spec
|
|
3634
|
+
* `docs/marquee-selection.md`); the surface only supplies the boxes and
|
|
3635
|
+
* the gesture-start baseline. Selection is deterministic in (marquee rect,
|
|
3636
|
+
* gesture-start selection, shift) — meta is a gesture-routing modifier
|
|
3637
|
+
* only (it decides that a drag IS a marquee), not a resolution input. */
|
|
3638
|
+
resolve_scene_marquee(rect, additive) {
|
|
3639
|
+
const { boxes, selection } = this.scene_marquee_baseline;
|
|
3640
|
+
return marquee_selection.resolve(boxes, rect, selection, { additive });
|
|
3396
3641
|
}
|
|
3397
3642
|
/**
|
|
3398
3643
|
* Vector marquee predicate — applies the **vertex-priority precedence
|
|
@@ -3423,8 +3668,7 @@ var DomSurface = class DomSurface {
|
|
|
3423
3668
|
if (typeof el.getScreenCTM !== "function") return;
|
|
3424
3669
|
const ctm = el.getScreenCTM();
|
|
3425
3670
|
if (!ctm) return;
|
|
3426
|
-
const
|
|
3427
|
-
const offset = [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
3671
|
+
const offset = this.container_offset();
|
|
3428
3672
|
const model = this.session_model();
|
|
3429
3673
|
if (!model) return;
|
|
3430
3674
|
const rect = intent.rect;
|
|
@@ -3487,8 +3731,7 @@ var DomSurface = class DomSurface {
|
|
|
3487
3731
|
if (typeof el.getScreenCTM !== "function") return;
|
|
3488
3732
|
const ctm = el.getScreenCTM();
|
|
3489
3733
|
if (!ctm) return;
|
|
3490
|
-
const
|
|
3491
|
-
const offset = [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
3734
|
+
const offset = this.container_offset();
|
|
3492
3735
|
const model = this.session_model();
|
|
3493
3736
|
if (!model) return;
|
|
3494
3737
|
const polygon = intent.polygon;
|
|
@@ -3793,8 +4036,7 @@ var DomSurface = class DomSurface {
|
|
|
3793
4036
|
if (typeof el.getScreenCTM !== "function") return null;
|
|
3794
4037
|
const ctm = el.getScreenCTM();
|
|
3795
4038
|
if (!ctm) return null;
|
|
3796
|
-
const
|
|
3797
|
-
const offset = [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
4039
|
+
const offset = this.container_offset();
|
|
3798
4040
|
return {
|
|
3799
4041
|
vertices: Array.from({ length: snap.vertices.length }, (_, i) => {
|
|
3800
4042
|
const v = snap.vertices[i];
|
|
@@ -4042,19 +4284,13 @@ var DomSurface = class DomSurface {
|
|
|
4042
4284
|
}
|
|
4043
4285
|
const baseline_d = this.active_preview.initial_d;
|
|
4044
4286
|
const indices = this.active_preview.indices;
|
|
4045
|
-
let local_dx = intent.dx;
|
|
4046
|
-
|
|
4047
|
-
const el = this.element_index.get(node_id);
|
|
4048
|
-
if (el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function") {
|
|
4049
|
-
const ctm = el.getScreenCTM();
|
|
4050
|
-
if (ctm) {
|
|
4051
|
-
if (ctm.a * ctm.d - ctm.c * ctm.b !== 0) [local_dx, local_dy] = project_delta_inverse_ctm(intent.dx, intent.dy, ctm);
|
|
4052
|
-
}
|
|
4053
|
-
}
|
|
4287
|
+
let [local_dx, local_dy] = this.vertex_drag_local_delta(node_id, this.active_preview.baseline_model, indices, intent.dx, intent.dy);
|
|
4288
|
+
[local_dx, local_dy] = this.axis_lock_point_delta(local_dx, local_dy);
|
|
4054
4289
|
const preview_model = this.active_preview.baseline_model.translateVertices(indices, [local_dx, local_dy]);
|
|
4055
4290
|
const target_d = preview_model.toSvgPathD();
|
|
4056
4291
|
this.active_preview.preview_model = preview_model;
|
|
4057
4292
|
if (intent.phase === "commit") {
|
|
4293
|
+
this.point_snap_guide = void 0;
|
|
4058
4294
|
const before_selection = this.active_preview.before_selection;
|
|
4059
4295
|
const after_selection = this.vector_edit.snapshot_selection();
|
|
4060
4296
|
this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, after_selection, before_selection));
|
|
@@ -4141,15 +4377,8 @@ var DomSurface = class DomSurface {
|
|
|
4141
4377
|
};
|
|
4142
4378
|
}
|
|
4143
4379
|
const baseline_d = this.active_preview.initial_d;
|
|
4144
|
-
let local_dx = intent.dx;
|
|
4145
|
-
|
|
4146
|
-
const el = this.element_index.get(node_id);
|
|
4147
|
-
if (el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function") {
|
|
4148
|
-
const ctm = el.getScreenCTM();
|
|
4149
|
-
if (ctm) {
|
|
4150
|
-
if (ctm.a * ctm.d - ctm.c * ctm.b !== 0) [local_dx, local_dy] = project_delta_inverse_ctm(intent.dx, intent.dy, ctm);
|
|
4151
|
-
}
|
|
4152
|
-
}
|
|
4380
|
+
let [local_dx, local_dy] = this.vertex_drag_local_delta(node_id, this.active_preview.baseline_model, indices, intent.dx, intent.dy);
|
|
4381
|
+
[local_dx, local_dy] = this.axis_lock_point_delta(local_dx, local_dy);
|
|
4153
4382
|
const baseline_model = this.active_preview.baseline_model;
|
|
4154
4383
|
const baseline_tangent_abs = this.active_preview.baseline_tangent_abs;
|
|
4155
4384
|
let preview_model = indices.length > 0 ? baseline_model.translateVertices(indices, [local_dx, local_dy]) : baseline_model;
|
|
@@ -4161,6 +4390,7 @@ var DomSurface = class DomSurface {
|
|
|
4161
4390
|
const target_d = preview_model.toSvgPathD();
|
|
4162
4391
|
this.active_preview.preview_model = preview_model;
|
|
4163
4392
|
if (intent.phase === "commit") {
|
|
4393
|
+
this.point_snap_guide = void 0;
|
|
4164
4394
|
const before_selection = this.active_preview.before_selection;
|
|
4165
4395
|
const after_selection = this.vector_edit.snapshot_selection();
|
|
4166
4396
|
this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, after_selection, before_selection));
|
|
@@ -4168,6 +4398,63 @@ var DomSurface = class DomSurface {
|
|
|
4168
4398
|
this.active_preview = null;
|
|
4169
4399
|
} else this.active_preview.session.set(this.vector_geometry_step(node_id, target_d, baseline_d, this.active_preview.promo, null, null));
|
|
4170
4400
|
}
|
|
4401
|
+
/**
|
|
4402
|
+
* Surface-aware `transform.nudge` handler — registered over the headless
|
|
4403
|
+
* default on attach (gridaco/grida#849). In vector content-edit, arrow
|
|
4404
|
+
* keys nudge the path sub-selection (the keyboard counterpart of
|
|
4405
|
+
* dragging vertices / segments / tangents) rather than leaking to a
|
|
4406
|
+
* whole-element move (the bug). This mirrors text-edit, where the inline
|
|
4407
|
+
* editor owns the arrow keys. Outside content-edit it delegates to the
|
|
4408
|
+
* headless `default_nudge_handler` (whole-element nudge) — the same
|
|
4409
|
+
* handler the registry restores on detach, so the override and the
|
|
4410
|
+
* default can never drift.
|
|
4411
|
+
*
|
|
4412
|
+
* While a vector session is open the arrows are OWNED by it: an empty
|
|
4413
|
+
* sub-selection is a consumed no-op, never a whole-element nudge — same
|
|
4414
|
+
* "content-edit captures arrows" contract as text-edit.
|
|
4415
|
+
*/
|
|
4416
|
+
handle_nudge_command(args) {
|
|
4417
|
+
const { dx, dy } = args;
|
|
4418
|
+
if (this.editor.state.mode === "edit-content" && this.vector_edit) {
|
|
4419
|
+
this.nudge_vector_selection(dx, dy);
|
|
4420
|
+
return true;
|
|
4421
|
+
}
|
|
4422
|
+
return default_nudge_handler(this.editor)(args) === true;
|
|
4423
|
+
}
|
|
4424
|
+
/**
|
|
4425
|
+
* Discrete keyboard nudge of the vector sub-selection. Reuses the drag
|
|
4426
|
+
* path ({@link handle_translate_vector_selection}) by synthesizing a
|
|
4427
|
+
* single commit-phase intent, so a nudge is byte-identical to a one-step
|
|
4428
|
+
* drag of the same points: same union resolution (selected vertices ∪
|
|
4429
|
+
* selected-segment endpoints ∪ tangents), same parent-vertex tangent
|
|
4430
|
+
* exclusion, same history bracket + sub-selection capture.
|
|
4431
|
+
*
|
|
4432
|
+
* `dx`/`dy` arrive in world units (1px, or 10px with Shift — already
|
|
4433
|
+
* resolved by the keymap binding). `handle_translate_vector_selection`
|
|
4434
|
+
* expects container CSS-px (it projects back through the inverse
|
|
4435
|
+
* screen-CTM), so scale by the camera zoom on the way in; the projection
|
|
4436
|
+
* recovers the world delta in the path's local frame. A no-sub-selection
|
|
4437
|
+
* session resolves to a no-op inside the handler.
|
|
4438
|
+
*/
|
|
4439
|
+
nudge_vector_selection(dx, dy) {
|
|
4440
|
+
const ses = this.vector_edit;
|
|
4441
|
+
if (!ses) return;
|
|
4442
|
+
const zoom = this.camera.zoom || 1;
|
|
4443
|
+
this.suppress_point_snap = true;
|
|
4444
|
+
try {
|
|
4445
|
+
this.handle_translate_vector_selection({
|
|
4446
|
+
kind: "translate_vector_selection",
|
|
4447
|
+
node_id: ses.node_id,
|
|
4448
|
+
additional_vertex_indices: [],
|
|
4449
|
+
dx: dx * zoom,
|
|
4450
|
+
dy: dy * zoom,
|
|
4451
|
+
phase: "commit"
|
|
4452
|
+
});
|
|
4453
|
+
} finally {
|
|
4454
|
+
this.suppress_point_snap = false;
|
|
4455
|
+
}
|
|
4456
|
+
this.request_redraw();
|
|
4457
|
+
}
|
|
4171
4458
|
/** Mirror handler for `select_tangent` (analogous to `select_vertex`). */
|
|
4172
4459
|
handle_select_tangent(intent) {
|
|
4173
4460
|
if (!this.vector_edit || this.vector_edit.node_id !== intent.node_id) return;
|
|
@@ -1,7 +1,56 @@
|
|
|
1
|
-
import { c as SvgEditor, p as Gestures, s as SurfaceHandle, w as Camera } from "./editor-
|
|
1
|
+
import { c as SvgEditor, p as Gestures, s as SurfaceHandle, w as Camera } from "./editor-CcW4BVth.mjs";
|
|
2
2
|
import cmath from "@grida/cmath";
|
|
3
3
|
import { guide } from "@grida/cmath/_snap";
|
|
4
4
|
|
|
5
|
+
//#region src/util/attention.d.ts
|
|
6
|
+
/** The runtime handle returned by `create_attention_tracker`. */
|
|
7
|
+
interface AttentionTracker {
|
|
8
|
+
/**
|
|
9
|
+
* `true` iff focus is inside the attention scope (the container's
|
|
10
|
+
* subtree or a registered element's subtree) OR the pointer is
|
|
11
|
+
* currently over any element of the scope. See module doc for the
|
|
12
|
+
* rationale.
|
|
13
|
+
*
|
|
14
|
+
* Pure read; no DOM mutation. Cheap enough to call once per keydown.
|
|
15
|
+
*/
|
|
16
|
+
is_attended(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* `true` iff focus is inside the attention scope — the focus arm of
|
|
19
|
+
* {@link is_attended} alone, WITHOUT the pointer-over arm.
|
|
20
|
+
*
|
|
21
|
+
* Exists for the native clipboard gate (`dom.ts`): pointer-over is a
|
|
22
|
+
* sufficient signal to claim a keystroke (worst case: a stolen scroll)
|
|
23
|
+
* but NOT a copy/cut/paste gesture (worst case: destroying what the
|
|
24
|
+
* user believed they copied, or routing a paste meant for a host text
|
|
25
|
+
* field into the document). See docs/wg/feat-svg-editor/clipboard.md
|
|
26
|
+
* §Transport "Gating the native events".
|
|
27
|
+
*/
|
|
28
|
+
is_focus_within(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Register a host-chrome element into the attention scope. Editor-
|
|
31
|
+
* adjacent chrome — an inspector, a toolbar, a zoom menu; anything that
|
|
32
|
+
* drives `commands.*` — is a DOM *sibling* of the container (the
|
|
33
|
+
* container is exclusively surface-owned), so without registration the
|
|
34
|
+
* tracker cannot tell it apart from unrelated page surface: clicking
|
|
35
|
+
* its buttons moves focus out of the container, and hovering it fires
|
|
36
|
+
* the container's `pointerleave`, blacking out the whole keymap.
|
|
37
|
+
* Registered elements count for both arms — focus-within and
|
|
38
|
+
* pointer-over. Idempotent; re-adding a registered element is a no-op.
|
|
39
|
+
*/
|
|
40
|
+
add(element: Element): void;
|
|
41
|
+
/**
|
|
42
|
+
* Unregister an element added via {@link add}. Also clears any
|
|
43
|
+
* still-latched pointer-over contribution from it (the element may be
|
|
44
|
+
* unmounted mid-hover — e.g. a popover closing under the cursor).
|
|
45
|
+
* Unknown elements are a no-op. The container itself cannot be
|
|
46
|
+
* removed; it is the scope's fixed root, not a registered extra.
|
|
47
|
+
*/
|
|
48
|
+
remove(element: Element): void;
|
|
49
|
+
/** Detach the internal pointer-tracking listeners (container and every
|
|
50
|
+
* registered element). */
|
|
51
|
+
dispose(): void;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
5
54
|
//#region src/core/snap/options.d.ts
|
|
6
55
|
type SnapOptions = {
|
|
7
56
|
/** When false, snap behavior and snap-guide rendering are both off. */enabled: boolean;
|
|
@@ -88,17 +137,39 @@ type DomSurfaceOptions = {
|
|
|
88
137
|
*/
|
|
89
138
|
font_load_source?: EventTarget;
|
|
90
139
|
};
|
|
140
|
+
/**
|
|
141
|
+
* Host-extendable attention scope — public via `handle.attention`.
|
|
142
|
+
*
|
|
143
|
+
* The document-level keymap (undo / redo / delete / tool keys) is gated on
|
|
144
|
+
* attention: focus inside the scope, or pointer over it. The scope starts
|
|
145
|
+
* as the container alone, which makes editor-adjacent host chrome — an
|
|
146
|
+
* inspector, a toolbar, a zoom menu; anything that drives `commands.*` —
|
|
147
|
+
* indistinguishable from unrelated page surface (chrome must be a DOM
|
|
148
|
+
* *sibling* of the container, never a child). Registering a chrome element
|
|
149
|
+
* keeps the full keymap live while the user works in it, with
|
|
150
|
+
* text-input focus still excluded by the keymap's own guard. The native
|
|
151
|
+
* clipboard gate (deliberately stricter: focus-only, never pointer-over)
|
|
152
|
+
* honors the registered set's focus arm the same way.
|
|
153
|
+
*
|
|
154
|
+
* `add` is idempotent; `remove` of an unregistered element is a no-op.
|
|
155
|
+
* Registrations live for the surface's lifetime — `detach()` drops them
|
|
156
|
+
* with the tracker. Hosts with mounting/unmounting chrome (popovers,
|
|
157
|
+
* panels) pair `add` on mount with `remove` on unmount.
|
|
158
|
+
*/
|
|
159
|
+
type AttentionScope = Pick<AttentionTracker, "add" | "remove">;
|
|
91
160
|
/**
|
|
92
161
|
* Surface handle for the DOM surface. Extends the editor's core
|
|
93
|
-
* `SurfaceHandle` with the viewport-scoped concerns: pan/zoom (`camera`)
|
|
94
|
-
*
|
|
162
|
+
* `SurfaceHandle` with the viewport-scoped concerns: pan/zoom (`camera`),
|
|
163
|
+
* pointer/wheel/keyboard gesture bindings (`gestures`), and the
|
|
164
|
+
* host-extendable attention scope (`attention`).
|
|
95
165
|
*
|
|
96
|
-
* Camera
|
|
97
|
-
*
|
|
166
|
+
* Camera, gestures, and attention are **surface-scoped**: detaching the
|
|
167
|
+
* surface drops all three. They never appear on the headless `SvgEditor`.
|
|
98
168
|
*/
|
|
99
169
|
type DomSurfaceHandle = SurfaceHandle & {
|
|
100
170
|
camera: Camera;
|
|
101
171
|
gestures: Gestures;
|
|
172
|
+
attention: AttentionScope;
|
|
102
173
|
};
|
|
103
174
|
/**
|
|
104
175
|
* Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
|
|
@@ -165,4 +236,4 @@ declare function inverse_project_rect(rect: {
|
|
|
165
236
|
f: number;
|
|
166
237
|
}, offset: readonly [number, number]): cmath.Rectangle | null;
|
|
167
238
|
//#endregion
|
|
168
|
-
export {
|
|
239
|
+
export { install_font_load_geometry_bump as a, project_point_through_ctm as c, attach_dom_surface as i, DEFAULT_SNAP_OPTIONS as l, DomSurfaceHandle as n, inverse_project_rect as o, DomSurfaceOptions as r, project_delta_inverse_ctm as s, AttentionScope as t, SnapOptions as u };
|