Place a peg's pivot with ⌃/⌘ rather than ⌥
⌥-drag does not survive the trip. Most Linux window managers grab Alt-drag to move the window, so the page never sees the pointer and the gesture is simply absent — on the machine it is absent from, with no error and nothing to find. Reported from one. ⌃ (⌘ on a Mac) now places the pivot, which is the modifier the rest of this stage already reaches for. ⌥ keeps working for anyone whose desktop leaves it alone; ⇧ is deliberately not it, since it means CONSTRAIN everywhere else here — uniform scale, 15° turn steps — and is what a snap to the child's corners will want when this drag grows one. The menu row says so, because a modifier nothing mentions is a modifier nobody finds: "add peg · ⌃/⌘-drag its cross to place the pivot". `peg.mjs` now asserts each modifier through real pointer events instead of reading the handler, which is the only way this class of bug shows up — all three place the pivot with a child drift of 0, and an unmodified drag still translates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HkinzDz1VtahZVsujAGRBD
This commit is contained in:
parent
357ff1f3b9
commit
39c436584b
3 changed files with 93 additions and 39 deletions
|
|
@ -325,9 +325,20 @@
|
||||||
;; to 8.8 from the middle, so the disc has to stop short of 5.2.
|
;; to 8.8 from the middle, so the disc has to stop short of 5.2.
|
||||||
[:circle.peg-grab
|
[:circle.peg-grab
|
||||||
{:cx px :cy py :r 5
|
{:cx px :cy py :r 5
|
||||||
;; ⌥ moves the PIVOT instead of the peg, which is the pan-behind
|
;; ⌃/⌘ moves the PIVOT instead of the peg, which is the pan-behind
|
||||||
;; split: a plain drag writes `pos` and carries the children with
|
;; split: a plain drag writes `pos` and carries the children with
|
||||||
;; it, and that is a translate, not a new pivot.
|
;; it, and that is a translate, not a new pivot.
|
||||||
|
;;
|
||||||
|
;; NOT ⌥, which this was and which does not survive the trip. Most
|
||||||
|
;; Linux window managers grab Alt-drag to move the window, so the
|
||||||
|
;; page never sees the pointer at all and the gesture is simply
|
||||||
|
;; missing — on the machine it is missing from, with no error. ⌥ is
|
||||||
|
;; still accepted for anyone whose desktop leaves it alone, and the
|
||||||
|
;; command key is the one the rest of this stage already reaches for.
|
||||||
|
;;
|
||||||
|
;; ⇧ is deliberately NOT it: it means CONSTRAIN everywhere else
|
||||||
|
;; here — uniform scale, 15° turn steps — and it is what a snap to
|
||||||
|
;; the child's corners would want when this drag grows one.
|
||||||
:on-pointer-down
|
:on-pointer-down
|
||||||
(fn [^js event]
|
(fn [^js event]
|
||||||
(.stopPropagation event)
|
(.stopPropagation event)
|
||||||
|
|
@ -335,7 +346,7 @@
|
||||||
(let [svg (.-ownerSVGElement (.-currentTarget event))
|
(let [svg (.-ownerSVGElement (.-currentTarget event))
|
||||||
p (xy svg event (:w ctx) (:h ctx))]
|
p (xy svg event (:w ctx) (:h ctx))]
|
||||||
(.setPointerCapture svg (.-pointerId event))
|
(.setPointerCapture svg (.-pointerId event))
|
||||||
(if (.-altKey event)
|
(if (or (.-ctrlKey event) (.-metaKey event) (.-altKey event))
|
||||||
(reset! repivot {:path path :p p})
|
(reset! repivot {:path path :p p})
|
||||||
(begin! ctx :move path p))))}]
|
(begin! ctx :move path p))))}]
|
||||||
[:line.knob-arm {:x1 px :y1 (- py r) :x2 px :y2 (- py r 8)}]
|
[:line.knob-arm {:x1 px :y1 (- py r) :x2 px :y2 (- py r 8)}]
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
;; measured part by hand — which is what `gesture/refusal` tells
|
;; measured part by hand — which is what `gesture/refusal` tells
|
||||||
;; you to do, so it has to be reachable from somewhere.
|
;; you to do, so it has to be reachable from somewhere.
|
||||||
{:label "add peg"
|
{:label "add peg"
|
||||||
:sub "A transform node over the selection, to pivot, turn and key from"
|
:sub "A transform node over the selection · ⌃/⌘-drag its cross to place the pivot"
|
||||||
:disabled? (nil? one-node)
|
:disabled? (nil? one-node)
|
||||||
:on-click #(rf/dispatch [::ui/peg one-node])}]}]
|
:on-click #(rf/dispatch [::ui/peg one-node])}]}]
|
||||||
[snapshots/view]
|
[snapshots/view]
|
||||||
|
|
|
||||||
|
|
@ -201,27 +201,14 @@ try {
|
||||||
String(dom2.cornerHit));
|
String(dom2.cornerHit));
|
||||||
|
|
||||||
if (dom2.grab) {
|
if (dom2.grab) {
|
||||||
const before2 = await evalJS(`
|
// Every modifier that has to place the pivot. CDP modifiers: 1=alt, 2=ctrl,
|
||||||
const c = cljs.core, k = c.keyword, v = c.vector;
|
// 4=meta. ⌥ alone was the whole gesture once, and on a desktop whose window
|
||||||
const db = c.deref(re_frame.db.app_db);
|
// manager grabs Alt-drag the page never sees it — so each is asserted
|
||||||
const e = arthur.footage.store.entry(c.get(db, k('clip/current')));
|
// through real pointer events rather than by reading the handler.
|
||||||
const doc = c.get(e, k('clip'));
|
|
||||||
const res = arthur.domain.clip.resolver(doc, k('main'), c.get(e,k('store')),
|
|
||||||
arthur.domain.palette.index_of, null);
|
|
||||||
return Array.from(c.get(c.first(res.call(null, 0)), k('pts')));
|
|
||||||
`);
|
|
||||||
const { x, y } = dom2.grab;
|
const { x, y } = dom2.grab;
|
||||||
const steps = [['mousePressed', 0, 0], ['mouseMoved', -40, 25], ['mouseMoved', -80, 50],
|
let after2;
|
||||||
['mouseReleased', -80, 50]];
|
for (const [name, modifiers, sign] of [['alt', 1, -1], ['ctrl', 2, 1], ['meta', 4, -1]]) {
|
||||||
for (const [type, dx, dy] of steps) {
|
const pre = await evalJS(`
|
||||||
await send('Input.dispatchMouseEvent', {
|
|
||||||
type, x: x + dx, y: y + dy, button: 'left', buttons: 1, clickCount: 1,
|
|
||||||
pointerType: 'mouse', modifiers: 1, // alt
|
|
||||||
});
|
|
||||||
await sleep(90);
|
|
||||||
}
|
|
||||||
await sleep(500);
|
|
||||||
const after2 = await evalJS(`
|
|
||||||
const c = cljs.core, k = c.keyword, v = c.vector;
|
const c = cljs.core, k = c.keyword, v = c.vector;
|
||||||
const db = c.deref(re_frame.db.app_db);
|
const db = c.deref(re_frame.db.app_db);
|
||||||
const e = arthur.footage.store.entry(c.get(db, k('clip/current')));
|
const e = arthur.footage.store.entry(c.get(db, k('clip/current')));
|
||||||
|
|
@ -230,16 +217,72 @@ try {
|
||||||
const pegId = c.get(c.get(nodes, k('shape')), k('parent'));
|
const pegId = c.get(c.get(nodes, k('shape')), k('parent'));
|
||||||
const res = arthur.domain.clip.resolver(doc, k('main'), c.get(e,k('store')),
|
const res = arthur.domain.clip.resolver(doc, k('main'), c.get(e,k('store')),
|
||||||
arthur.domain.palette.index_of, null);
|
arthur.domain.palette.index_of, null);
|
||||||
return {pos: c.clj__GT_js(c.get_in(nodes, v(pegId, k('channels'), v(k('xform'),k('pos')), k('value')))),
|
return {pts: Array.from(c.get(c.first(res.call(null, 0)), k('pts'))),
|
||||||
pts: Array.from(c.get(c.first(res.call(null, 0)), k('pts'))),
|
pos: c.clj__GT_js(c.get_in(nodes, v(pegId, k('channels'), v(k('xform'),k('pos')), k('value'))))};
|
||||||
refusal: c.clj__GT_js(c.get_in(db, v(k('ui'), k('refusal'))))};
|
|
||||||
`);
|
`);
|
||||||
const drift = Math.max(...before2.map((b, i) => Math.abs(b - after2.pts[i])));
|
// Where the cross is NOW, since the previous modifier already moved it.
|
||||||
console.log('after alt-drag: peg pos', JSON.stringify(after2.pos), 'child drift', drift);
|
const g = await evalJS(`
|
||||||
ok(drift < 1e-6, 'alt-dragging the cross moved NOTHING under the peg', `drift ${drift}`);
|
const q = document.querySelector('.paint-overlay .handles .peg-grab');
|
||||||
ok(Math.abs(after2.pos[0] - made.pegPos[0]) > 2, 'but the pivot did move',
|
const r = q && q.getBoundingClientRect();
|
||||||
`${JSON.stringify(made.pegPos)} -> ${JSON.stringify(after2.pos)}`);
|
return r ? {x: r.x + r.width/2, y: r.y + r.height/2} : null;
|
||||||
|
`);
|
||||||
|
const dx = 30 * sign, dy = 20 * sign;
|
||||||
|
for (const [type, fx, fy] of [['mousePressed', 0, 0], ['mouseMoved', dx/2, dy/2],
|
||||||
|
['mouseMoved', dx, dy], ['mouseReleased', dx, dy]]) {
|
||||||
|
await send('Input.dispatchMouseEvent', {
|
||||||
|
type, x: g.x + fx, y: g.y + fy, button: 'left', buttons: 1, clickCount: 1,
|
||||||
|
pointerType: 'mouse', modifiers,
|
||||||
|
});
|
||||||
|
await sleep(90);
|
||||||
}
|
}
|
||||||
|
await sleep(450);
|
||||||
|
const post = await evalJS(`
|
||||||
|
const c = cljs.core, k = c.keyword, v = c.vector;
|
||||||
|
const db = c.deref(re_frame.db.app_db);
|
||||||
|
const e = arthur.footage.store.entry(c.get(db, k('clip/current')));
|
||||||
|
const doc = c.get(e, k('clip'));
|
||||||
|
const nodes = c.get_in(doc, v(k('symbols'), k('main'), k('nodes')));
|
||||||
|
const pegId = c.get(c.get(nodes, k('shape')), k('parent'));
|
||||||
|
const res = arthur.domain.clip.resolver(doc, k('main'), c.get(e,k('store')),
|
||||||
|
arthur.domain.palette.index_of, null);
|
||||||
|
return {pts: Array.from(c.get(c.first(res.call(null, 0)), k('pts'))),
|
||||||
|
pos: c.clj__GT_js(c.get_in(nodes, v(pegId, k('channels'), v(k('xform'),k('pos')), k('value'))))};
|
||||||
|
`);
|
||||||
|
const drift = Math.max(...pre.pts.map((b, i) => Math.abs(b - post.pts[i])));
|
||||||
|
const moved = Math.hypot(post.pos[0] - pre.pos[0], post.pos[1] - pre.pos[1]);
|
||||||
|
ok(drift < 1e-6, `${name}-drag places the pivot and moves NOTHING under the peg`,
|
||||||
|
`drift ${drift}`);
|
||||||
|
ok(moved > 2, `${name}-drag actually moved the pivot`, `by ${moved.toFixed(2)}`);
|
||||||
|
after2 = post;
|
||||||
|
}
|
||||||
|
// And a plain drag still translates, which is the other half of the split.
|
||||||
|
{
|
||||||
|
const g = await evalJS(`
|
||||||
|
const q = document.querySelector('.paint-overlay .handles .peg-grab');
|
||||||
|
const r = q && q.getBoundingClientRect();
|
||||||
|
return r ? {x: r.x + r.width/2, y: r.y + r.height/2} : null;
|
||||||
|
`);
|
||||||
|
const pre = after2;
|
||||||
|
for (const [type, fx] of [['mousePressed', 0], ['mouseMoved', 12], ['mouseReleased', 12]]) {
|
||||||
|
await send('Input.dispatchMouseEvent', {
|
||||||
|
type, x: g.x + fx, y: g.y, button: 'left', buttons: 1, clickCount: 1, pointerType: 'mouse',
|
||||||
|
});
|
||||||
|
await sleep(90);
|
||||||
|
}
|
||||||
|
await sleep(450);
|
||||||
|
const post = await evalJS(`
|
||||||
|
const c = cljs.core, k = c.keyword, v = c.vector;
|
||||||
|
const db = c.deref(re_frame.db.app_db);
|
||||||
|
const e = arthur.footage.store.entry(c.get(db, k('clip/current')));
|
||||||
|
const res = arthur.domain.clip.resolver(c.get(e, k('clip')), k('main'), c.get(e,k('store')),
|
||||||
|
arthur.domain.palette.index_of, null);
|
||||||
|
return Array.from(c.get(c.first(res.call(null, 0)), k('pts')));
|
||||||
|
`);
|
||||||
|
ok(Math.abs(post[0] - pre.pts[0]) > 2, 'and an UNMODIFIED drag still translates the child',
|
||||||
|
`dx=${(post[0] - pre.pts[0]).toFixed(2)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(failures ? `\n${failures} FAILED` : '\nPASS: a peg can be made, moved, and have its pivot placed');
|
console.log(failures ? `\n${failures} FAILED` : '\nPASS: a peg can be made, moved, and have its pivot placed');
|
||||||
} finally {
|
} finally {
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue