From 42083acec51427b27dcd4cf575336d967a317e04 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Mon, 24 Aug 2026 13:51:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=9C=89=E6=99=82=E5=80=99?= =?UTF-8?q?=E5=8D=A1=E4=BD=8F=E7=84=A1=E6=B3=95=E6=8B=96=E6=9B=B3=EF=BC=9A?= =?UTF-8?q?=E9=BB=9E=E7=A9=BF=E7=8B=80=E6=85=8B=E6=94=B9=E7=94=B1=E4=B8=BB?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E8=BC=AA=E8=A9=A2=E9=A9=85=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 摘要: 點穿的命中判定不再只依賴 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 --- README.md | 2 +- main.js | 10 +++++++++- package-lock.json | 4 ++-- package.json | 2 +- preload.js | 1 + renderer/renderer.js | 25 ++++++++++++++++++++++--- 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fd8a50a..804e6e9 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Claude Code ──hook(stdin JSON)──▶ hook/claude-pet-hook.js ──PO 設計重點: -- **點穿**:視窗預設 `setIgnoreMouseEvents(true, { forward: true })`,renderer 在 mousemove 時讀 canvas 該點 alpha(或是否在氣泡上)來即時切換,所以透明處永遠點得到底下的視窗。 +- **點穿**:視窗預設 `setIgnoreMouseEvents(true, { forward: true })`,renderer 讀 canvas 該點的 alpha(或是否壓在氣泡上)來即時切換,所以透明處永遠點得到底下的視窗。判定的座標有兩個來源:renderer 自己的 mousemove,以及**主程序每 50 ms 的游標輪詢**。後者是必要的——視窗處於點穿狀態時 Electron 的 `forward` 不保證會把 mousemove 轉發進來(游標直接跳到寵物上常常收不到),只靠 mousemove 會在「游標還停在寵物身上時進入點穿」之後再也收不到事件,變成永遠卡在點穿、按不下去也拖不動。另外每一格畫完也會用最後已知座標重驗一次,因為換格會改變游標底下的像素。 - **拖曳**:由主程序用 `screen.getCursorScreenPoint()` 每 16 ms 更新視窗位置,游標離開視窗也不會掉;方向由水平位移決定。 - **行為邏輯以 Codex 為準**,細節見[與 Codex 的對照](#與-codex-的對照)。 - **顯示尺寸與 sprite 來源尺寸分開**:`CELL_W/CELL_H`(192 × 208)只用來從 spritesheet 取格子,畫到畫面上的是 `PET_W/PET_H`(126 × 137,Codex 的 mascot 尺寸)。視窗寬度另有 `MIN_WIN_W = 240` 的下限,免得寵物變小後氣泡被截掉。 diff --git a/main.js b/main.js index e7c0ece..c727253 100644 --- a/main.js +++ b/main.js @@ -328,11 +328,19 @@ function endDrag(save) { function startCursorPolling() { cursorTimer = setInterval(() => { - if (!win || !config.followCursor || dragTimer) return; + if (!win || dragTimer) return; const p = screen.getCursorScreenPoint(); if (p.x === lastCursor.x && p.y === lastCursor.y) return; lastCursor = p; const b = win.getBounds(); + // 點穿判定不能只靠 renderer 的 mousemove。視窗處於點穿狀態時,Electron 的 + // setIgnoreMouseEvents(true, { forward: true }) 並不保證一定會把 mousemove 轉發進來 + // (游標直接跳到寵物上常常收不到,慢慢移過去才會)。一旦在游標還停在寵物身上時 + // 進入點穿,就再也收不到事件、ignoringMouse 永遠是 true,滑鼠按下也傳不進視窗 + // → 就是「有時候卡住無法拖曳」。主程序自己輪詢絕對不會漏,所以改由這裡把 + // 視窗相對座標送過去,讓 renderer 重新做一次判定。 + send("pet:cursor-pos", { x: p.x - b.x, y: p.y - b.y }); + if (!config.followCursor) return; const cx = b.x + b.width / 2; const cy = b.y + BUBBLE_H + (PET_H * config.scale) / 2; // 人物方框的中心,與 Codex 相同 send("pet:cursor", { dx: p.x - cx, dy: p.y - cy }); diff --git a/package-lock.json b/package-lock.json index 8feff53..49e34f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-pet", - "version": "2.0.5", + "version": "2.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-pet", - "version": "2.0.5", + "version": "2.0.6", "devDependencies": { "electron": "^43.4.1", "electron-builder": "^26.0.12" diff --git a/package.json b/package.json index 4bd8b44..4831901 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-pet", - "version": "2.0.5", + "version": "2.0.6", "description": "Codex Pets 相容的 Claude Code 桌面寵物(spriteVersionNumber 2:9 個動作列 + 16 方向追視)", "main": "main.js", "private": true, diff --git a/preload.js b/preload.js index f6085a5..23c9674 100644 --- a/preload.js +++ b/preload.js @@ -20,6 +20,7 @@ const ON = new Set([ "pet:settings", "pet:event", "pet:cursor", + "pet:cursor-pos", "pet:drag-move", ]); diff --git a/renderer/renderer.js b/renderer/renderer.js index 3d7f875..8b8d553 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -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";