修正有時候卡住無法拖曳:點穿狀態改由主程序輪詢驅動
摘要:
點穿的命中判定不再只依賴 renderer 的 mousemove,改由主程序每 50 ms 的游標輪詢一起驅動,
並在每格畫完後用最後已知座標重驗一次。
根本原因:
視窗預設 setIgnoreMouseEvents(true, { forward: true }),而唯一會把它切回可互動的地方
是 renderer 的 mousemove。問題是視窗處於點穿狀態時,Electron 的 forward 不保證會把
mousemove 轉發進來——本次開發過程中已實測到:游標直接跳到寵物上收不到事件,
慢慢掃過去才收得到。
於是只要在「游標還停在寵物身上」時進入點穿(例如游標停在某一點,動畫換格後
該點的像素變成透明),就再也收不到 mousemove,ignoringMouse 永遠是 true,
滑鼠按下也傳不進視窗,就變成按不動也拖不動,而且不會自己好。
另外 startCursorPolling 的內容原本被 `!config.followCursor` 整段擋掉,
而追視預設為關,所以主程序根本沒有在追游標,沒有任何備援路徑。
影響:
寵物有時候完全無法拖曳或點擊,只能重啟;游標停在人物邊緣或半透明處時特別容易發生。
修法:
- startCursorPolling 的守衛從 `!win || !config.followCursor || dragTimer` 改為
`!win || dragTimer`,輪詢一律執行;只有送 pet:cursor(追視用)那一行仍看 followCursor。
- 主程序新增 pet:cursor-pos,送出視窗相對座標;preload 開放該頻道。
- renderer 把命中判定集中成 refreshHitTest(),mousemove 與 pet:cursor-pos 走同一條路,
並在 pressed / drag 期間不動點穿狀態,避免拖曳中途鬆手。
- tick 畫完後若最後已知座標落在人物方框內就重驗一次,處理「游標沒動但換格導致
該點像素改變」的情況。
- mouseout 時清掉 lastPoint。
- README 的設計重點補上為何不能只靠 mousemove。
驗證:
- 實機加暫時診斷確認 pet:cursor-pos 確實送達 renderer,且回報的 canvasRect
(57,72,183,209)與依視窗尺寸手算的結果一致;確認後移除診斷碼。
- 以 DOM stub 載入完整 renderer.js 並讓 getImageData 的 alpha 可切換,14 項全過:
壓在人物上會解除點穿並觸發 hover 跳躍、有通知主程序、方框內但透明維持點穿、
方框外點穿且非 hover、拖曳中與按住中都不改點穿、pet:cursor-pos 有註冊且
直接跳到人物上也能解除點穿、以及最關鍵的一組——先進入點穿狀態,
再由 pet:cursor-pos 在同一點換格後把它救回來,這正是原本卡死的情境。
- 以 ELECTRON_RUN_AS_NODE 讀打包後 app.asar,確認版本 2.0.6 與九項改動都在出貨檔案裡。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+22
-3
@@ -150,6 +150,7 @@ let bubbleTimer = null;
|
||||
let bubbleSticky = false;
|
||||
let ignoringMouse = true;
|
||||
let hovered = false; // 游標是否壓在人物的不透明像素上
|
||||
let lastPoint = null; // 最後已知的游標位置(視窗相對),主程序輪詢或 mousemove 都會更新
|
||||
let pressed = null;
|
||||
let lastReported = "";
|
||||
|
||||
@@ -545,6 +546,9 @@ function tick(now) {
|
||||
prevBase = base;
|
||||
|
||||
draw(anim);
|
||||
// 換格會改變游標底下的像素。游標沒移動時主程序不會再送座標,
|
||||
// 所以這裡自己重驗一次,否則會卡在點穿狀態動不了。
|
||||
if (lastPoint && overPetArea(lastPoint.x, lastPoint.y)) refreshHitTest();
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
@@ -656,6 +660,15 @@ function setHovered(on) {
|
||||
if (overlay?.fromHover) overlay = null;
|
||||
}
|
||||
|
||||
// 命中判定集中在這裡:mousemove 與主程序輪詢都走同一條路。
|
||||
// 主程序每 50 ms 一定會送座標過來,所以就算 mousemove 沒被轉發進來也能復原。
|
||||
function refreshHitTest() {
|
||||
if (!lastPoint || pressed || drag) return; // 拖曳中不要動點穿狀態
|
||||
const { x, y } = lastPoint;
|
||||
setHovered(overPetArea(x, y));
|
||||
setIgnore(!(overSprite(x, y) || overBubble(x, y)));
|
||||
}
|
||||
|
||||
function setIgnore(ignore) {
|
||||
if (ignore === ignoringMouse) return;
|
||||
ignoringMouse = ignore;
|
||||
@@ -673,13 +686,13 @@ window.addEventListener("mousemove", (e) => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
setHovered(overPetArea(e.clientX, e.clientY));
|
||||
// 點穿判定仍看實際像素,透明處要能點到底下的視窗
|
||||
setIgnore(!(overSprite(e.clientX, e.clientY) || overBubble(e.clientX, e.clientY)));
|
||||
lastPoint = { x: e.clientX, y: e.clientY };
|
||||
refreshHitTest();
|
||||
});
|
||||
|
||||
document.addEventListener("mouseout", (e) => {
|
||||
if (e.relatedTarget || pressed) return;
|
||||
lastPoint = null;
|
||||
setHovered(false);
|
||||
setIgnore(true);
|
||||
});
|
||||
@@ -707,6 +720,12 @@ window.addEventListener("mouseup", (e) => {
|
||||
|
||||
window.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
|
||||
// 主程序的游標輪詢:不管 mousemove 有沒有被轉發進來都會到
|
||||
window.pet.on("pet:cursor-pos", ({ x, y }) => {
|
||||
lastPoint = { x, y };
|
||||
refreshHitTest();
|
||||
});
|
||||
|
||||
window.pet.on("pet:drag-move", ({ dx }) => {
|
||||
// Codex:單一取樣的水平位移 >= 4 才切 running-right,<= -4 才切 running-left
|
||||
if (drag && Math.abs(dx) >= 4) drag.dir = dx < 0 ? "left" : "right";
|
||||
|
||||
Reference in New Issue
Block a user