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