@grida/svg-editor 1.0.0-alpha.20 → 1.0.0-alpha.22
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 +1 -1
- package/dist/{dom-Dub-TMoN.mjs → dom-BIjCxCgx.mjs} +346 -86
- package/dist/{dom-CfYDV311.js → dom-H4PvmPe3.js} +346 -86
- package/dist/dom.js +1 -1
- package/dist/dom.mjs +1 -1
- package/dist/{editor-B1GmFnS9.mjs → editor-DCDQl18y.mjs} +5 -4
- package/dist/{editor-4U9LAZ6r.js → editor-DFvojUwn.js} +5 -4
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{model-CzL6_zId.js → model-HEKGO-56.js} +21 -14
- package/dist/{model-DS5MxDrd.mjs → model-zMPCOVAr.mjs} +21 -14
- package/dist/presets.js +2 -2
- package/dist/presets.mjs +1 -1
- package/dist/react.js +2 -2
- package/dist/react.mjs +2 -2
- package/package.json +6 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_model = require("./model-
|
|
1
|
+
const require_model = require("./model-HEKGO-56.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");
|
|
@@ -788,6 +788,139 @@ let marquee_selection;
|
|
|
788
788
|
_marquee_selection.resolve = resolve;
|
|
789
789
|
})(marquee_selection || (marquee_selection = {}));
|
|
790
790
|
//#endregion
|
|
791
|
+
//#region src/selection/targeting.ts
|
|
792
|
+
let targeting;
|
|
793
|
+
(function(_targeting) {
|
|
794
|
+
/** Siblings are treated as closer than parent/child (distance 1) so a tap
|
|
795
|
+
* into a sibling subtree resolves to the sibling at the focus depth without
|
|
796
|
+
* a tie-breaker. Mirrors Grida Canvas `weights: { sibling: 0.9 }`. */
|
|
797
|
+
const SIBLING_WEIGHT = .9;
|
|
798
|
+
function resolve_target(hits, tree, opts) {
|
|
799
|
+
if (hits.length === 0) return null;
|
|
800
|
+
const depth_cache = /* @__PURE__ */ new Map();
|
|
801
|
+
const depth = (id) => {
|
|
802
|
+
const cached = depth_cache.get(id);
|
|
803
|
+
if (cached !== void 0) return cached;
|
|
804
|
+
let d = 0;
|
|
805
|
+
const seen = new Set([id]);
|
|
806
|
+
let cur = tree.parent_of(id);
|
|
807
|
+
while (cur !== null && !seen.has(cur)) {
|
|
808
|
+
d++;
|
|
809
|
+
seen.add(cur);
|
|
810
|
+
cur = tree.parent_of(cur);
|
|
811
|
+
}
|
|
812
|
+
depth_cache.set(id, d);
|
|
813
|
+
return d;
|
|
814
|
+
};
|
|
815
|
+
const ancestors_inclusive = (id) => {
|
|
816
|
+
const set = /* @__PURE__ */ new Set();
|
|
817
|
+
let cur = id;
|
|
818
|
+
while (cur !== null && !set.has(cur)) {
|
|
819
|
+
set.add(cur);
|
|
820
|
+
cur = tree.parent_of(cur);
|
|
821
|
+
}
|
|
822
|
+
return set;
|
|
823
|
+
};
|
|
824
|
+
/** Lowest common ancestor, or `null` if the two ids are not connected
|
|
825
|
+
* (e.g. a stale selection id from a different/old tree). */
|
|
826
|
+
const lca = (a, b) => {
|
|
827
|
+
const a_anc = ancestors_inclusive(a);
|
|
828
|
+
const seen = /* @__PURE__ */ new Set();
|
|
829
|
+
let cur = b;
|
|
830
|
+
while (cur !== null && !seen.has(cur)) {
|
|
831
|
+
if (a_anc.has(cur)) return cur;
|
|
832
|
+
seen.add(cur);
|
|
833
|
+
cur = tree.parent_of(cur);
|
|
834
|
+
}
|
|
835
|
+
return null;
|
|
836
|
+
};
|
|
837
|
+
/** Is `a` a (strict) descendant of `b`? */
|
|
838
|
+
const is_descendant_of = (a, b) => {
|
|
839
|
+
const seen = /* @__PURE__ */ new Set();
|
|
840
|
+
let cur = tree.parent_of(a);
|
|
841
|
+
while (cur !== null && !seen.has(cur)) {
|
|
842
|
+
if (cur === b) return true;
|
|
843
|
+
seen.add(cur);
|
|
844
|
+
cur = tree.parent_of(cur);
|
|
845
|
+
}
|
|
846
|
+
return false;
|
|
847
|
+
};
|
|
848
|
+
const graph_distance = (a, b) => {
|
|
849
|
+
if (a === b) return 0;
|
|
850
|
+
const m = lca(a, b);
|
|
851
|
+
if (m === null) return Infinity;
|
|
852
|
+
return depth(a) + depth(b) - 2 * depth(m);
|
|
853
|
+
};
|
|
854
|
+
const weighted_distance = (a, b) => {
|
|
855
|
+
if (a === b) return 0;
|
|
856
|
+
const pa = tree.parent_of(a);
|
|
857
|
+
if (pa !== null && pa === tree.parent_of(b)) return SIBLING_WEIGHT;
|
|
858
|
+
return graph_distance(a, b);
|
|
859
|
+
};
|
|
860
|
+
if (opts.deepest) {
|
|
861
|
+
let best = null;
|
|
862
|
+
let best_depth = -Infinity;
|
|
863
|
+
for (const id of hits) {
|
|
864
|
+
const d = depth(id);
|
|
865
|
+
if (d > best_depth) {
|
|
866
|
+
best_depth = d;
|
|
867
|
+
best = id;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return best;
|
|
871
|
+
}
|
|
872
|
+
const by_depth = [...hits].sort((a, b) => {
|
|
873
|
+
const dd = depth(a) - depth(b);
|
|
874
|
+
return dd !== 0 ? dd : hits.indexOf(a) - hits.indexOf(b);
|
|
875
|
+
});
|
|
876
|
+
const leaf = hits[0];
|
|
877
|
+
const selection = opts.selection.filter((s) => lca(leaf, s) !== null);
|
|
878
|
+
const find_nearest = (candidates, sel, use_sibling_weight, prefer_children) => {
|
|
879
|
+
if (candidates.length === 0 || sel.length === 0) return null;
|
|
880
|
+
const dist = use_sibling_weight ? weighted_distance : graph_distance;
|
|
881
|
+
const scored = candidates.map((c) => {
|
|
882
|
+
let min = Infinity;
|
|
883
|
+
for (const s of sel) {
|
|
884
|
+
const d = dist(c, s);
|
|
885
|
+
if (d < min) min = d;
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
id: c,
|
|
889
|
+
distance: min,
|
|
890
|
+
is_child: prefer_children ? sel.some((s) => is_descendant_of(c, s)) : false,
|
|
891
|
+
depth: depth(c)
|
|
892
|
+
};
|
|
893
|
+
});
|
|
894
|
+
scored.sort((a, b) => {
|
|
895
|
+
if (a.distance !== b.distance) return a.distance - b.distance;
|
|
896
|
+
if (prefer_children) {
|
|
897
|
+
if (a.is_child && !b.is_child) return -1;
|
|
898
|
+
if (!a.is_child && b.is_child) return 1;
|
|
899
|
+
}
|
|
900
|
+
return a.depth - b.depth;
|
|
901
|
+
});
|
|
902
|
+
return scored[0]?.id ?? null;
|
|
903
|
+
};
|
|
904
|
+
if (selection.length > 0) {
|
|
905
|
+
if (opts.nested_first) {
|
|
906
|
+
const descendants = by_depth.filter((c) => !selection.includes(c) && selection.some((s) => is_descendant_of(c, s)));
|
|
907
|
+
if (descendants.length > 0) {
|
|
908
|
+
const r = find_nearest(descendants, selection, false, false);
|
|
909
|
+
if (r !== null) return r;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const focus_ancestors = /* @__PURE__ */ new Set();
|
|
913
|
+
for (const s of selection) for (const a of ancestors_inclusive(s)) focus_ancestors.add(a);
|
|
914
|
+
for (const s of selection) focus_ancestors.delete(s);
|
|
915
|
+
const lateral = by_depth.filter((c) => !focus_ancestors.has(c));
|
|
916
|
+
const nearest = find_nearest(lateral.length > 0 ? lateral : by_depth, selection, true, opts.nested_first);
|
|
917
|
+
if (nearest !== null) return nearest;
|
|
918
|
+
}
|
|
919
|
+
return by_depth[0] ?? null;
|
|
920
|
+
}
|
|
921
|
+
_targeting.resolve_target = resolve_target;
|
|
922
|
+
})(targeting || (targeting = {}));
|
|
923
|
+
//#endregion
|
|
791
924
|
//#region src/gestures/gestures.ts
|
|
792
925
|
/**
|
|
793
926
|
* Sibling to `Keymap`. Owns a list of installed gesture bindings; each
|
|
@@ -1783,6 +1916,7 @@ var DomSurface = class DomSurface {
|
|
|
1783
1916
|
this.last_pointer_valid = false;
|
|
1784
1917
|
this.resize_observer = null;
|
|
1785
1918
|
this.redraw_raf_id = null;
|
|
1919
|
+
this.hover_repick_raf_id = null;
|
|
1786
1920
|
this._geometry_provider = null;
|
|
1787
1921
|
this._hit_shapes = null;
|
|
1788
1922
|
this._z_order_cache = [];
|
|
@@ -1999,6 +2133,7 @@ var DomSurface = class DomSurface {
|
|
|
1999
2133
|
});
|
|
2000
2134
|
this.teardown.push(unsub);
|
|
2001
2135
|
this.teardown.push(editor.subscribe_geometry(() => this.request_redraw()));
|
|
2136
|
+
this.teardown.push(editor.subscribe_with_selector((s) => s.selection, () => this.request_hover_repick(), require_model.array_shallow_equal));
|
|
2002
2137
|
if (typeof ResizeObserver !== "undefined") {
|
|
2003
2138
|
this.resize_observer = new ResizeObserver(() => this.sync_canvas_size());
|
|
2004
2139
|
this.resize_observer.observe(container);
|
|
@@ -2089,7 +2224,62 @@ var DomSurface = class DomSurface {
|
|
|
2089
2224
|
}
|
|
2090
2225
|
hit_test(x, y) {
|
|
2091
2226
|
if (this.vector_edit) return null;
|
|
2092
|
-
|
|
2227
|
+
const leaf = this.pick_at(x, y, false);
|
|
2228
|
+
if (leaf === null) return null;
|
|
2229
|
+
return this.resolve_group_first(leaf, { nested_first: false });
|
|
2230
|
+
}
|
|
2231
|
+
/** Group-first targeting over the raw leaf's ancestor chain — the host's
|
|
2232
|
+
* selection POLICY (pure rules in `src/selection/targeting.ts`). The
|
|
2233
|
+
* current selection defines the focus depth: a tap moves laterally /
|
|
2234
|
+
* sibling-aware at that depth, meta/ctrl jumps to the leaf, and
|
|
2235
|
+
* `nested_first` (double-click) descends one level. Returns the raw `leaf`
|
|
2236
|
+
* unchanged if the document is momentarily unavailable. */
|
|
2237
|
+
resolve_group_first(leaf, opts) {
|
|
2238
|
+
const doc = (() => {
|
|
2239
|
+
try {
|
|
2240
|
+
return this.editor_internal().doc;
|
|
2241
|
+
} catch {
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
})();
|
|
2245
|
+
if (!doc) return leaf;
|
|
2246
|
+
const root = doc.root;
|
|
2247
|
+
const hits = [];
|
|
2248
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2249
|
+
let cur = leaf;
|
|
2250
|
+
while (cur !== null && cur !== root && !seen.has(cur)) {
|
|
2251
|
+
hits.push(cur);
|
|
2252
|
+
seen.add(cur);
|
|
2253
|
+
cur = doc.parent_of(cur);
|
|
2254
|
+
}
|
|
2255
|
+
const mods = this.hud.modifiers();
|
|
2256
|
+
return targeting.resolve_target(hits, { parent_of: (id) => doc.parent_of(id) }, {
|
|
2257
|
+
selection: this.editor.state.selection,
|
|
2258
|
+
deepest: mods.meta || mods.ctrl,
|
|
2259
|
+
nested_first: opts.nested_first
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
/** Double-click behaviour, as two distinct phases over the hierarchy:
|
|
2263
|
+
*
|
|
2264
|
+
* 1. **Descend** — resolve one level deeper toward `leaf` (relative to the
|
|
2265
|
+
* current focus, via `resolve_group_first` with `nested_first`). A
|
|
2266
|
+
* double-click that lands on a node deeper than the selection just
|
|
2267
|
+
* SELECTS it.
|
|
2268
|
+
* 2. **Enter content-edit** — only when there is nowhere deeper to go: the
|
|
2269
|
+
* resolved target is already the sole selection. Then (and only then) a
|
|
2270
|
+
* double-click opens content-edit, when the node is editable.
|
|
2271
|
+
*
|
|
2272
|
+
* Descending and editing never collide: progressive double-clicks peel
|
|
2273
|
+
* `G1 → G2 → … → leaf`, and one more double-click on the now-focused leaf
|
|
2274
|
+
* edits it. A top-level editable node (no container to peel) is selected by
|
|
2275
|
+
* the double-click's own first press, so its second press lands in the
|
|
2276
|
+
* edit phase — it still edits on a single double-click. */
|
|
2277
|
+
descend_or_enter_content_edit(leaf) {
|
|
2278
|
+
const target = this.resolve_group_first(leaf, { nested_first: true });
|
|
2279
|
+
if (target === null) return;
|
|
2280
|
+
const selection = this.editor.state.selection;
|
|
2281
|
+
if (selection.length === 1 && selection[0] === target) this.editor.enter_content_edit(target);
|
|
2282
|
+
else this.editor.commands.select(target);
|
|
2093
2283
|
}
|
|
2094
2284
|
/** Element-walk under (x, y) → first ancestor with `ID_ATTR`. When
|
|
2095
2285
|
* `allow_root` is `false`, root hits are rejected (returns `null`) so
|
|
@@ -2129,9 +2319,12 @@ var DomSurface = class DomSurface {
|
|
|
2129
2319
|
* `allow_root` controls whether the root `<svg>` may be returned:
|
|
2130
2320
|
* selection HUD passes `false`, measurement HUD passes `true`.
|
|
2131
2321
|
*
|
|
2132
|
-
*
|
|
2133
|
-
*
|
|
2134
|
-
*
|
|
2322
|
+
* This is the RAW (leaf-first) picker — one source of truth for "what
|
|
2323
|
+
* element is under this point." Used by `pick_at` (HUD hover / measurement)
|
|
2324
|
+
* and `SvgGeometryDriver.node_at_point` (core geometry queries). Selection
|
|
2325
|
+
* targeting policy (group-first / meta-deepest / focus-depth) is applied on
|
|
2326
|
+
* top of the raw leaf by `hit_test` → `resolve_group_first`, NOT here, so
|
|
2327
|
+
* measurement and geometry queries stay leaf-exact. */
|
|
2135
2328
|
_pick_node_at_world(p, allow_root) {
|
|
2136
2329
|
const root_id = this.editor.tree().root;
|
|
2137
2330
|
const tol_px = this.editor.style.hit_tolerance_px;
|
|
@@ -2215,6 +2408,10 @@ var DomSurface = class DomSurface {
|
|
|
2215
2408
|
(this.container.ownerDocument.defaultView ?? window).cancelAnimationFrame(this.redraw_raf_id);
|
|
2216
2409
|
this.redraw_raf_id = null;
|
|
2217
2410
|
}
|
|
2411
|
+
if (this.hover_repick_raf_id !== null) {
|
|
2412
|
+
(this.container.ownerDocument.defaultView ?? window).cancelAnimationFrame(this.hover_repick_raf_id);
|
|
2413
|
+
this.hover_repick_raf_id = null;
|
|
2414
|
+
}
|
|
2218
2415
|
for (const fn of this.teardown) fn();
|
|
2219
2416
|
this.teardown = [];
|
|
2220
2417
|
this.hud.dispose();
|
|
@@ -2392,6 +2589,53 @@ var DomSurface = class DomSurface {
|
|
|
2392
2589
|
});
|
|
2393
2590
|
}
|
|
2394
2591
|
/**
|
|
2592
|
+
* Re-pick the hover / target id at the RESTING pointer and return what
|
|
2593
|
+
* changed. The hover target is a pure function of (pointer, modifiers,
|
|
2594
|
+
* selection, tree) resolved through `hit_test` → group-first targeting;
|
|
2595
|
+
* every input OTHER than the pointer changes off the pointer path, so the
|
|
2596
|
+
* stored hover goes stale until the next real move. Replaying a synthetic
|
|
2597
|
+
* idle pointer-move re-runs `pick` under the current state, keeping the
|
|
2598
|
+
* highlight a faithful preview of what the next click would select.
|
|
2599
|
+
*
|
|
2600
|
+
* Idle-only: a synthetic move opens / cancels no gesture and leaves any tap
|
|
2601
|
+
* candidate / pending press untouched. No-op (all-false) when the pointer
|
|
2602
|
+
* has never been observed, has left the canvas, or a text / vector edit owns
|
|
2603
|
+
* it. Pushes nothing and redraws nothing — the caller reflects the flags.
|
|
2604
|
+
*/
|
|
2605
|
+
repick_hover_at_rest() {
|
|
2606
|
+
if (!this.last_pointer_valid || this.hud.gesture().kind !== "idle" || this.text_edit || this.vector_edit) return {
|
|
2607
|
+
needsRedraw: false,
|
|
2608
|
+
cursorChanged: false,
|
|
2609
|
+
hoverChanged: false
|
|
2610
|
+
};
|
|
2611
|
+
return this.hud.dispatch({
|
|
2612
|
+
kind: "pointer_move",
|
|
2613
|
+
x: this.last_pointer.x,
|
|
2614
|
+
y: this.last_pointer.y,
|
|
2615
|
+
mods: this.hud.modifiers()
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Schedule an idle hover re-pick on the next frame (RAF-coalesced, like
|
|
2620
|
+
* `request_redraw`). DEFERRED on purpose: the trigger — a selection change —
|
|
2621
|
+
* frequently fires from inside `hud.dispatch` (a double-click drill resolves
|
|
2622
|
+
* its new selection via `onIntent`, mid-dispatch), and re-picking there would
|
|
2623
|
+
* re-enter the HUD. By next frame the dispatch has unwound and the gesture
|
|
2624
|
+
* has settled, so `repick_hover_at_rest` runs against stable state. Multiple
|
|
2625
|
+
* changes within a frame fold into one re-pick.
|
|
2626
|
+
*/
|
|
2627
|
+
request_hover_repick() {
|
|
2628
|
+
if (this.hover_repick_raf_id !== null) return;
|
|
2629
|
+
const win = this.container.ownerDocument.defaultView ?? window;
|
|
2630
|
+
this.hover_repick_raf_id = win.requestAnimationFrame(() => {
|
|
2631
|
+
this.hover_repick_raf_id = null;
|
|
2632
|
+
const moved = this.repick_hover_at_rest();
|
|
2633
|
+
if (moved.needsRedraw || moved.hoverChanged) this.redraw();
|
|
2634
|
+
if (moved.cursorChanged) this.sync_cursor();
|
|
2635
|
+
if (moved.hoverChanged) this.editor_hover_internal?.push_surface_hover(this.hud.hover());
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2395
2639
|
* Build the host-fed measurement guide for the current frame, or
|
|
2396
2640
|
* `undefined` if no guide should be drawn.
|
|
2397
2641
|
*
|
|
@@ -2950,11 +3194,18 @@ var DomSurface = class DomSurface {
|
|
|
2950
3194
|
mods: next
|
|
2951
3195
|
});
|
|
2952
3196
|
if ((prev.shift !== next.shift || prev.alt !== next.alt) && this.translate_orchestrator.has_active_session()) this.translate_orchestrator.redrive_modifiers(this.current_translate_modifiers());
|
|
2953
|
-
if (prev.shift !== next.shift && this.resize_orchestrator.has_active_session()) this.resize_orchestrator.redrive_modifiers(this.current_resize_modifiers());
|
|
3197
|
+
if ((prev.shift !== next.shift || prev.alt !== next.alt) && this.resize_orchestrator.has_active_session()) this.resize_orchestrator.redrive_modifiers(this.current_resize_modifiers());
|
|
2954
3198
|
if (prev.shift !== next.shift && this.rotate_orchestrator.has_active_session()) this.rotate_orchestrator.redrive_modifiers(this.current_rotate_modifiers());
|
|
3199
|
+
let cursor_changed = response.cursorChanged;
|
|
3200
|
+
let hover_changed = response.hoverChanged;
|
|
3201
|
+
if (prev.meta !== next.meta || prev.ctrl !== next.ctrl) {
|
|
3202
|
+
const moved = this.repick_hover_at_rest();
|
|
3203
|
+
if (moved.cursorChanged) cursor_changed = true;
|
|
3204
|
+
if (moved.hoverChanged) hover_changed = true;
|
|
3205
|
+
}
|
|
2955
3206
|
this.redraw();
|
|
2956
|
-
if (
|
|
2957
|
-
if (
|
|
3207
|
+
if (cursor_changed) this.sync_cursor();
|
|
3208
|
+
if (hover_changed) this.editor_hover_internal?.push_surface_hover(this.hud.hover());
|
|
2958
3209
|
}
|
|
2959
3210
|
dispatch_pointer(e, kind) {
|
|
2960
3211
|
if (this.text_edit) {
|
|
@@ -3236,10 +3487,16 @@ var DomSurface = class DomSurface {
|
|
|
3236
3487
|
case "set_endpoint":
|
|
3237
3488
|
this.handle_set_endpoint(intent);
|
|
3238
3489
|
return;
|
|
3239
|
-
case "enter_content_edit":
|
|
3240
|
-
this.
|
|
3241
|
-
|
|
3490
|
+
case "enter_content_edit": {
|
|
3491
|
+
const leaf = this.last_pointer_valid ? this.pick_at(this.last_pointer.x, this.last_pointer.y, false) : null;
|
|
3492
|
+
if (leaf === null) {
|
|
3493
|
+
this.editor.commands.select(intent.id);
|
|
3494
|
+
this.editor.enter_content_edit(intent.id);
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
this.descend_or_enter_content_edit(leaf);
|
|
3242
3498
|
return;
|
|
3499
|
+
}
|
|
3243
3500
|
case "exit_content_edit":
|
|
3244
3501
|
this.exit_vector_edit();
|
|
3245
3502
|
return;
|
|
@@ -3291,10 +3548,12 @@ var DomSurface = class DomSurface {
|
|
|
3291
3548
|
* Shift axis-lock for point-level drags (gridaco/grida#848) — the
|
|
3292
3549
|
* vertex / endpoint counterpart of whole-object translate's
|
|
3293
3550
|
* `axis_lock: "by_dominance"` (see `current_translate_modifiers`).
|
|
3294
|
-
* Collapses the lesser axis of a
|
|
3295
|
-
*
|
|
3296
|
-
*
|
|
3297
|
-
*
|
|
3551
|
+
* Collapses the lesser axis of a delta when Shift is held, via the same
|
|
3552
|
+
* cmath rule the translate pipeline's `axis_lock` stage uses; identity when
|
|
3553
|
+
* Shift is up. Space-agnostic: the point path applies it to the raw screen
|
|
3554
|
+
* delta (before snap), so dominance reflects the user's drag intent. Pull-
|
|
3555
|
+
* at-consume from the HUD modifier store so a mid-drag Shift press/release
|
|
3556
|
+
* reflects on the next frame.
|
|
3298
3557
|
*/
|
|
3299
3558
|
axis_lock_point_delta(dx, dy) {
|
|
3300
3559
|
if (!this.hud.modifiers().shift) return [dx, dy];
|
|
@@ -3307,19 +3566,21 @@ var DomSurface = class DomSurface {
|
|
|
3307
3566
|
* delta so it aligns with (or lands on) a sibling point: a path's other
|
|
3308
3567
|
* vertices, or a `<line>`'s opposite endpoint.
|
|
3309
3568
|
*
|
|
3310
|
-
* Runs
|
|
3311
|
-
* point[s]) and
|
|
3312
|
-
* the
|
|
3313
|
-
*
|
|
3314
|
-
*
|
|
3315
|
-
*
|
|
3316
|
-
*
|
|
3569
|
+
* Runs in **screen space** (container CSS-px, the HUD's identity frame). The
|
|
3570
|
+
* caller projects the moving point[s] (agents) and the static sibling points
|
|
3571
|
+
* (neighbors) through the element CTM first, and feeds the drag delta in the
|
|
3572
|
+
* same space. Screen space is the right frame because the threshold and the
|
|
3573
|
+
* rendered guide are both screen-pixel concepts: the configured
|
|
3574
|
+
* `snap_threshold_px` stays user-visible pixels on BOTH axes for any element
|
|
3575
|
+
* CTM — even a non-uniform / rotated one, which a single area-scale would
|
|
3576
|
+
* distort (gridaco/grida#844 review) — and the guide needs no reprojection.
|
|
3577
|
+
* The caller converts the returned screen delta back to local for the write.
|
|
3317
3578
|
*
|
|
3318
|
-
* Honors the global snap toggle (`style.snap_enabled`) and threshold
|
|
3319
|
-
* (`style.snap_threshold_px`, converted to local units by the CTM scale) —
|
|
3579
|
+
* Honors the global snap toggle (`style.snap_enabled`) and threshold —
|
|
3320
3580
|
* snap off ⇒ free point dragging, per the issue. Bypassed when
|
|
3321
|
-
* `suppress_point_snap` is set (keyboard nudge)
|
|
3322
|
-
*
|
|
3581
|
+
* `suppress_point_snap` is set (keyboard nudge) or the point hasn't moved
|
|
3582
|
+
* (a pure tap / select must never relocate a control). Identity when there
|
|
3583
|
+
* are no neighbors / agents.
|
|
3323
3584
|
*
|
|
3324
3585
|
* The shared `SnapSession` drops 0-area rects (its empty-`<g>` "jerk to
|
|
3325
3586
|
* origin" defense), so each point is modeled as a sub-pixel square centered
|
|
@@ -3327,14 +3588,12 @@ var DomSurface = class DomSurface {
|
|
|
3327
3588
|
* matched same-edge offset carries the same ±eps on both sides), so eps
|
|
3328
3589
|
* magnitude is immaterial as long as it stays far below the threshold.
|
|
3329
3590
|
*/
|
|
3330
|
-
|
|
3591
|
+
snap_point_delta_screen(raw_dx, raw_dy, agents_screen, neighbors_screen) {
|
|
3331
3592
|
this.point_snap_guide = void 0;
|
|
3332
3593
|
const style = this.editor.style;
|
|
3333
|
-
if (this.suppress_point_snap || !style.snap_enabled ||
|
|
3334
|
-
const
|
|
3335
|
-
const
|
|
3336
|
-
const threshold_local = style.snap_threshold_px / scale;
|
|
3337
|
-
const eps = threshold_local * 1e-6 || 1e-9;
|
|
3594
|
+
if (this.suppress_point_snap || !style.snap_enabled || agents_screen.length === 0 || neighbors_screen.length === 0 || raw_dx === 0 && raw_dy === 0) return [raw_dx, raw_dy];
|
|
3595
|
+
const threshold = style.snap_threshold_px;
|
|
3596
|
+
const eps = threshold * 1e-6 || 1e-9;
|
|
3338
3597
|
const to_rect = (p) => ({
|
|
3339
3598
|
x: p[0] - eps / 2,
|
|
3340
3599
|
y: p[1] - eps / 2,
|
|
@@ -3342,23 +3601,23 @@ var DomSurface = class DomSurface {
|
|
|
3342
3601
|
height: eps
|
|
3343
3602
|
});
|
|
3344
3603
|
const session = new SnapSession({
|
|
3345
|
-
agents:
|
|
3346
|
-
neighbors:
|
|
3604
|
+
agents: agents_screen.map(to_rect),
|
|
3605
|
+
neighbors: neighbors_screen.map(to_rect)
|
|
3347
3606
|
});
|
|
3348
3607
|
const { delta, guide } = session.snap({
|
|
3349
3608
|
x: raw_dx,
|
|
3350
3609
|
y: raw_dy
|
|
3351
3610
|
}, {
|
|
3352
3611
|
enabled: true,
|
|
3353
|
-
threshold_px:
|
|
3612
|
+
threshold_px: threshold
|
|
3354
3613
|
});
|
|
3355
3614
|
session.dispose();
|
|
3356
|
-
|
|
3615
|
+
this.point_snap_guide = guide;
|
|
3357
3616
|
return [delta.x, delta.y];
|
|
3358
3617
|
}
|
|
3359
3618
|
/** Split a path's baseline vertices into the snap agents (the moving
|
|
3360
|
-
* sub-selection) and neighbors (everything else) for
|
|
3361
|
-
*
|
|
3619
|
+
* sub-selection) and neighbors (everything else) for the point-snap pass.
|
|
3620
|
+
* Both in path-local space; the caller projects them to screen. */
|
|
3362
3621
|
vertex_snap_points(model, moving) {
|
|
3363
3622
|
const verts = model.snapshot().vertices;
|
|
3364
3623
|
const moving_set = new Set(moving);
|
|
@@ -3370,31 +3629,6 @@ var DomSurface = class DomSurface {
|
|
|
3370
3629
|
neighbors
|
|
3371
3630
|
};
|
|
3372
3631
|
}
|
|
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
3632
|
/** Translation from screen/page CSS-px to the HUD's container-identity
|
|
3399
3633
|
* space: subtract the container's page offset, add its scroll. Used at
|
|
3400
3634
|
* every `getScreenCTM` projection boundary (chrome, marquee, point snap)
|
|
@@ -3404,24 +3638,45 @@ var DomSurface = class DomSurface {
|
|
|
3404
3638
|
return [-cr.left + this.container.scrollLeft, -cr.top + this.container.scrollTop];
|
|
3405
3639
|
}
|
|
3406
3640
|
/**
|
|
3641
|
+
* Local-frame drag delta for a point sub-selection (vertices or a `<line>`
|
|
3642
|
+
* endpoint), composed exactly like whole-object translate: **axis-lock then
|
|
3643
|
+
* snap** (translate-pipeline stage order `axis_lock → snap`). The raw delta
|
|
3644
|
+
* and the agent / neighbor points are all in screen space (container
|
|
3645
|
+
* CSS-px):
|
|
3646
|
+
*
|
|
3647
|
+
* 1. Shift axis-lock collapses the lesser axis of the RAW screen delta —
|
|
3648
|
+
* dominance is the user's intent, decided before snap can perturb it
|
|
3649
|
+
* (gridaco/grida#844 review: locking the snapped delta could flip the
|
|
3650
|
+
* axis). The lock is screen-aligned, matching the drag and the guides.
|
|
3651
|
+
* 2. Point snap aligns the locked delta to the sibling points.
|
|
3652
|
+
* 3. The result projects back into the element's local frame via the
|
|
3653
|
+
* inverse CTM for `translateVertices` / the endpoint write.
|
|
3654
|
+
*
|
|
3655
|
+
* `agents_local` / `neighbors_local` are projected to screen here so the
|
|
3656
|
+
* snap threshold stays user-visible pixels under any element CTM.
|
|
3657
|
+
*/
|
|
3658
|
+
point_drag_local_delta(ctm, raw_screen_dx, raw_screen_dy, agents_local, neighbors_local) {
|
|
3659
|
+
let [sx, sy] = this.axis_lock_point_delta(raw_screen_dx, raw_screen_dy);
|
|
3660
|
+
const offset = this.container_offset();
|
|
3661
|
+
const to_screen = (p) => project_point_through_ctm(p[0], p[1], ctm, offset);
|
|
3662
|
+
[sx, sy] = this.snap_point_delta_screen(sx, sy, agents_local.map(to_screen), neighbors_local.map(to_screen));
|
|
3663
|
+
return ctm.a * ctm.d - ctm.c * ctm.b !== 0 ? project_delta_inverse_ctm(sx, sy, ctm) : [sx, sy];
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3407
3666
|
* Local-frame drag delta for a vertex sub-selection, from the HUD's
|
|
3408
|
-
* container-space `dx/dy`.
|
|
3409
|
-
*
|
|
3410
|
-
*
|
|
3411
|
-
*
|
|
3412
|
-
*
|
|
3413
|
-
*
|
|
3414
|
-
* the caller applies axis-lock and feeds the result to `translateVertices`.
|
|
3667
|
+
* container-space `dx/dy`. Resolves the element CTM and the snap neighbor
|
|
3668
|
+
* set (the path's other vertices), then defers to `point_drag_local_delta`
|
|
3669
|
+
* for the axis-lock → snap → project composition. Falls back to a plain
|
|
3670
|
+
* axis-lock of the raw delta when the element has no usable CTM. A
|
|
3671
|
+
* tangent-only drag (empty `indices`) has no vertex agents, so snap is a
|
|
3672
|
+
* no-op. Shared by the two vertex-translate handlers.
|
|
3415
3673
|
*/
|
|
3416
3674
|
vertex_drag_local_delta(node_id, baseline_model, indices, dx, dy) {
|
|
3417
3675
|
const el = this.element_index.get(node_id);
|
|
3418
3676
|
const ctm = el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function" ? el.getScreenCTM() : null;
|
|
3419
|
-
if (!ctm) return
|
|
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);
|
|
3677
|
+
if (!ctm) return this.axis_lock_point_delta(dx, dy);
|
|
3423
3678
|
const { agents, neighbors } = this.vertex_snap_points(baseline_model, indices);
|
|
3424
|
-
return this.
|
|
3679
|
+
return this.point_drag_local_delta(ctm, dx, dy, agents, neighbors);
|
|
3425
3680
|
}
|
|
3426
3681
|
/** Snapshot of HUD modifier state mapped to the orchestrator's `GestureModifiers`.
|
|
3427
3682
|
* Pull-at-consume: HUD is the canonical store (see `sync_modifiers`),
|
|
@@ -3437,8 +3692,10 @@ var DomSurface = class DomSurface {
|
|
|
3437
3692
|
/** Snapshot of HUD modifier state mapped to `ResizeModifiers`. Same
|
|
3438
3693
|
* pull-at-consume discipline as `current_translate_modifiers`. */
|
|
3439
3694
|
current_resize_modifiers() {
|
|
3695
|
+
const mods = this.hud.modifiers();
|
|
3440
3696
|
return {
|
|
3441
|
-
aspect_lock:
|
|
3697
|
+
aspect_lock: mods.shift ? "uniform" : "off",
|
|
3698
|
+
from_center: mods.alt,
|
|
3442
3699
|
force_disable_snap: false
|
|
3443
3700
|
};
|
|
3444
3701
|
}
|
|
@@ -3531,17 +3788,21 @@ var DomSurface = class DomSurface {
|
|
|
3531
3788
|
const target_y = pos_own.y;
|
|
3532
3789
|
const base_x = endpoint === "p1" ? initial.x1 : initial.x2;
|
|
3533
3790
|
const base_y = endpoint === "p1" ? initial.y1 : initial.y2;
|
|
3534
|
-
let dx = target_x - base_x;
|
|
3535
|
-
let dy = target_y - base_y;
|
|
3536
3791
|
const el = this.element_index.get(id);
|
|
3537
3792
|
const ctm = el instanceof SVGGraphicsElement && typeof el.getScreenCTM === "function" ? el.getScreenCTM() : null;
|
|
3793
|
+
let final_x;
|
|
3794
|
+
let final_y;
|
|
3538
3795
|
if (ctm) {
|
|
3796
|
+
const base_screen = project_point_through_ctm(base_x, base_y, ctm, this.container_offset());
|
|
3539
3797
|
const other = endpoint === "p1" ? [initial.x2, initial.y2] : [initial.x1, initial.y1];
|
|
3540
|
-
[
|
|
3798
|
+
const [ldx, ldy] = this.point_drag_local_delta(ctm, intent.pos[0] - base_screen[0], intent.pos[1] - base_screen[1], [[base_x, base_y]], [other]);
|
|
3799
|
+
final_x = base_x + ldx;
|
|
3800
|
+
final_y = base_y + ldy;
|
|
3801
|
+
} else {
|
|
3802
|
+
const [ldx, ldy] = this.axis_lock_point_delta(target_x - base_x, target_y - base_y);
|
|
3803
|
+
final_x = base_x + ldx;
|
|
3804
|
+
final_y = base_y + ldy;
|
|
3541
3805
|
}
|
|
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;
|
|
3545
3806
|
const apply = () => {
|
|
3546
3807
|
if (endpoint === "p1") {
|
|
3547
3808
|
doc.set_attr(id, "x1", String(final_x));
|
|
@@ -4015,6 +4276,7 @@ var DomSurface = class DomSurface {
|
|
|
4015
4276
|
}
|
|
4016
4277
|
this.vector_edit = null;
|
|
4017
4278
|
this.vector_edit_region_baseline = null;
|
|
4279
|
+
this.point_snap_guide = void 0;
|
|
4018
4280
|
this.hud.setVectorSelection(null);
|
|
4019
4281
|
if (this.current_tool.type === "lasso" || this.current_tool.type === "bend") this.editor.set_tool({ type: "cursor" });
|
|
4020
4282
|
this.editor.commands.set_mode("select");
|
|
@@ -4286,8 +4548,7 @@ var DomSurface = class DomSurface {
|
|
|
4286
4548
|
}
|
|
4287
4549
|
const baseline_d = this.active_preview.initial_d;
|
|
4288
4550
|
const indices = this.active_preview.indices;
|
|
4289
|
-
|
|
4290
|
-
[local_dx, local_dy] = this.axis_lock_point_delta(local_dx, local_dy);
|
|
4551
|
+
const [local_dx, local_dy] = this.vertex_drag_local_delta(node_id, this.active_preview.baseline_model, indices, intent.dx, intent.dy);
|
|
4291
4552
|
const preview_model = this.active_preview.baseline_model.translateVertices(indices, [local_dx, local_dy]);
|
|
4292
4553
|
const target_d = preview_model.toSvgPathD();
|
|
4293
4554
|
this.active_preview.preview_model = preview_model;
|
|
@@ -4379,8 +4640,7 @@ var DomSurface = class DomSurface {
|
|
|
4379
4640
|
};
|
|
4380
4641
|
}
|
|
4381
4642
|
const baseline_d = this.active_preview.initial_d;
|
|
4382
|
-
|
|
4383
|
-
[local_dx, local_dy] = this.axis_lock_point_delta(local_dx, local_dy);
|
|
4643
|
+
const [local_dx, local_dy] = this.vertex_drag_local_delta(node_id, this.active_preview.baseline_model, indices, intent.dx, intent.dy);
|
|
4384
4644
|
const baseline_model = this.active_preview.baseline_model;
|
|
4385
4645
|
const baseline_tangent_abs = this.active_preview.baseline_tangent_abs;
|
|
4386
4646
|
let preview_model = indices.length > 0 ? baseline_model.translateVertices(indices, [local_dx, local_dy]) : baseline_model;
|
package/dist/dom.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_dom = require("./dom-
|
|
2
|
+
const require_dom = require("./dom-H4PvmPe3.js");
|
|
3
3
|
exports.Camera = require_dom.Camera;
|
|
4
4
|
exports.DEFAULT_SNAP_OPTIONS = require_dom.DEFAULT_SNAP_OPTIONS;
|
|
5
5
|
exports.Gestures = require_dom.Gestures;
|
package/dist/dom.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as project_point_through_ctm, c as MemoizedGeometryProvider, i as project_delta_inverse_ctm, l as Camera, n as install_font_load_geometry_bump, o as Gestures, r as inverse_project_rect, s as DEFAULT_SNAP_OPTIONS, t as attach_dom_surface } from "./dom-
|
|
1
|
+
import { a as project_point_through_ctm, c as MemoizedGeometryProvider, i as project_delta_inverse_ctm, l as Camera, n as install_font_load_geometry_bump, o as Gestures, r as inverse_project_rect, s as DEFAULT_SNAP_OPTIONS, t as attach_dom_surface } from "./dom-BIjCxCgx.mjs";
|
|
2
2
|
export { Camera, DEFAULT_SNAP_OPTIONS, Gestures, MemoizedGeometryProvider, attach_dom_surface, install_font_load_geometry_bump, inverse_project_rect, project_delta_inverse_ctm, project_point_through_ctm };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as TOOL_SET, S as is_text_input_focused, T as registerDefaultCommands, _ as SVG_NS, a as paint, b as XMLNS_NS, g as subtree, h as transform, i as TOOL_CURSOR, m as group, n as insertions, p as translate_pipeline, r as DEFAULT_STYLE, s as resize_pipeline, u as rotate_pipeline, v as SvgDocument, x as array_shallow_equal, y as WELL_KNOWN_NS_PREFIXES } from "./model-
|
|
1
|
+
import { C as TOOL_SET, S as is_text_input_focused, T as registerDefaultCommands, _ as SVG_NS, a as paint, b as XMLNS_NS, g as subtree, h as transform, i as TOOL_CURSOR, m as group, n as insertions, p as translate_pipeline, r as DEFAULT_STYLE, s as resize_pipeline, u as rotate_pipeline, v as SvgDocument, x as array_shallow_equal, y as WELL_KNOWN_NS_PREFIXES } from "./model-zMPCOVAr.mjs";
|
|
2
2
|
import { HistoryImpl } from "@grida/history";
|
|
3
3
|
import { KeyCode, M, chunkKey, eventToChunk, getKeyboardOS, kb, keybindingsToKeyCodes } from "@grida/keybinding";
|
|
4
4
|
import cmath from "@grida/cmath";
|
|
@@ -1205,15 +1205,16 @@ function _create_svg_editor_internal(opts) {
|
|
|
1205
1205
|
});
|
|
1206
1206
|
}
|
|
1207
1207
|
function set_selection(next) {
|
|
1208
|
-
|
|
1208
|
+
const pruned = doc.prune_nested_nodes(next);
|
|
1209
|
+
if (pruned.length === selection.length) {
|
|
1209
1210
|
let same = true;
|
|
1210
|
-
for (let i = 0; i <
|
|
1211
|
+
for (let i = 0; i < pruned.length; i++) if (pruned[i] !== selection[i]) {
|
|
1211
1212
|
same = false;
|
|
1212
1213
|
break;
|
|
1213
1214
|
}
|
|
1214
1215
|
if (same) return;
|
|
1215
1216
|
}
|
|
1216
|
-
selection = Object.freeze([...
|
|
1217
|
+
selection = Object.freeze([...pruned]);
|
|
1217
1218
|
emit();
|
|
1218
1219
|
}
|
|
1219
1220
|
function select(target, opts) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_model = require("./model-
|
|
1
|
+
const require_model = require("./model-HEKGO-56.js");
|
|
2
2
|
let _grida_history = require("@grida/history");
|
|
3
3
|
let _grida_keybinding = require("@grida/keybinding");
|
|
4
4
|
let _grida_cmath = require("@grida/cmath");
|
|
@@ -1206,15 +1206,16 @@ function _create_svg_editor_internal(opts) {
|
|
|
1206
1206
|
});
|
|
1207
1207
|
}
|
|
1208
1208
|
function set_selection(next) {
|
|
1209
|
-
|
|
1209
|
+
const pruned = doc.prune_nested_nodes(next);
|
|
1210
|
+
if (pruned.length === selection.length) {
|
|
1210
1211
|
let same = true;
|
|
1211
|
-
for (let i = 0; i <
|
|
1212
|
+
for (let i = 0; i < pruned.length; i++) if (pruned[i] !== selection[i]) {
|
|
1212
1213
|
same = false;
|
|
1213
1214
|
break;
|
|
1214
1215
|
}
|
|
1215
1216
|
if (same) return;
|
|
1216
1217
|
}
|
|
1217
|
-
selection = Object.freeze([...
|
|
1218
|
+
selection = Object.freeze([...pruned]);
|
|
1218
1219
|
emit();
|
|
1219
1220
|
}
|
|
1220
1221
|
function select(target, opts) {
|