From eb53744c08b6855fe4363fc0a64a515725216080 Mon Sep 17 00:00:00 2001 From: Iro <155815508+Iro96@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:23:04 +0700 Subject: [PATCH] fix(stage-tamagotchi): harden caption overlay follow+mouse handling (#1402) --- .../src/main/windows/caption/index.ts | 53 +++++++++++++++---- .../locales/vi/tamagotchi/electron/tray.yaml | 2 +- .../stage-ui-three/src/trace/snapshots.ts | 4 +- pnpm-lock.yaml | 35 ++++++++++-- 4 files changed, 75 insertions(+), 19 deletions(-) diff --git a/apps/stage-tamagotchi/src/main/windows/caption/index.ts b/apps/stage-tamagotchi/src/main/windows/caption/index.ts index a471f5658..87cf6a871 100644 --- a/apps/stage-tamagotchi/src/main/windows/caption/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/caption/index.ts @@ -194,9 +194,12 @@ export function setupCaptionWindowManager(params: { const settleTo = (toX: number, toY: number) => { if (win.isDestroyed()) return + if (!Number.isFinite(toX) || !Number.isFinite(toY)) + return + const b = win.getBounds() - state.x = b.x - state.y = b.y + state.x = Number.isFinite(b.x) ? b.x : 0 + state.y = Number.isFinite(b.y) ? b.y : 0 animation?.pause() animation = animate(state, { x: toX, @@ -207,8 +210,13 @@ export function setupCaptionWindowManager(params: { onRender: () => { if (win.isDestroyed()) return + if (!Number.isFinite(state.x) || !Number.isFinite(state.y)) + return + + const toX = Math.round(state.x) + const toY = Math.round(state.y) lastProgrammaticMoveAt = Date.now() - win.setPosition(state.x, state.y) + win.setPosition(toX, toY) }, }) } @@ -219,6 +227,9 @@ export function setupCaptionWindowManager(params: { let lastAppliedTy = Number.NaN const moveThrottled = throttle(() => { + if (win.isDestroyed()) + return + const stored = getConfig()?.matrices[matrixHash]?.relativeToMain ?? initialOffset const main = params.mainWindow.getBounds() const b = win.getBounds() @@ -277,6 +288,18 @@ export function setupCaptionWindowManager(params: { } } + function applyIgnoreMouseEvents(win: BrowserWindow, ignore: boolean) { + try { + if (ignore) + win.setIgnoreMouseEvents(true, { forward: true }) + else + win.setIgnoreMouseEvents(false) + } + catch { + // ignore failures during early window lifecycle + } + } + const reusable = createReusableWindow(async () => { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and @@ -290,6 +313,8 @@ export function setupCaptionWindowManager(params: { await setupBaseWindowElectronInvokes({ context, window, serverChannel: params.serverChannel, i18n: params.i18n }) + applyIgnoreMouseEvents(window, isFollowing) + const cfg = getConfig() const saved = cfg?.matrices?.[matrixHash]?.bounds @@ -359,22 +384,26 @@ export function setupCaptionWindowManager(params: { async function setFollowWindow(isFollowingWindow: boolean) { isFollowing = isFollowingWindow const window = await reusable.getWindow() + + applyIgnoreMouseEvents(window, isFollowing) + if (isFollowing) { - // Compute and persist current relative offset based on existing positions const rel = computeRelativeOffset(window) - const cfg = getConfig() ?? { isFollowing, matrices: {} } - cfg.matrices[matrixHash] = { ...cfg.matrices[matrixHash], relativeToMain: rel } - updateConfig(cfg) + const config = getConfig() ?? { isFollowing, matrices: {} } + config.isFollowing = isFollowing + config.matrices[matrixHash] = { ...config.matrices[matrixHash], relativeToMain: rel } + updateConfig(config) + // Start following main without re-docking; keep current position followMainWindow(window) } else { detachFromMain() - } - const config = getConfig() ?? { isFollowing, matrices: {} } - config.isFollowing = isFollowing - updateConfig(config) + const config = getConfig() ?? { isFollowing, matrices: {} } + config.isFollowing = isFollowing + updateConfig(config) + } // Keep window visible after toggle window.show() @@ -399,6 +428,8 @@ export function setupCaptionWindowManager(params: { async function resetToSide() { const window = await reusable.getWindow() + applyIgnoreMouseEvents(window, isFollowing) + // Prevent user-move persistence from overwriting our programmatic move lastProgrammaticMoveAt = Date.now() const initialBounds = computeInitialCaptionBounds({ mainWindow: params.mainWindow }) diff --git a/packages/i18n/src/locales/vi/tamagotchi/electron/tray.yaml b/packages/i18n/src/locales/vi/tamagotchi/electron/tray.yaml index 76c451d58..29ed05942 100644 --- a/packages/i18n/src/locales/vi/tamagotchi/electron/tray.yaml +++ b/packages/i18n/src/locales/vi/tamagotchi/electron/tray.yaml @@ -21,7 +21,7 @@ menu: close_caption: Close Caption... caption_overlay: Lớp phủ phụ đề follow_window: Theo cửa sổ - reset_position: Đặt lại vị trí + reset_position: Đặt lại vị trí devtools: Công cụ phát triển troubleshoot_beatsync: Sửa lỗi BeatSync... quit: Thoát ra diff --git a/packages/stage-ui-three/src/trace/snapshots.ts b/packages/stage-ui-three/src/trace/snapshots.ts index 71d4fc834..3a4a0bbed 100644 --- a/packages/stage-ui-three/src/trace/snapshots.ts +++ b/packages/stage-ui-three/src/trace/snapshots.ts @@ -1,5 +1,5 @@ import type { VRM } from '@pixiv/three-vrm' -import type { AnimationMixer, Material, WebGLRenderer } from 'three' +import type { AnimationMixer, Material, Object3D, WebGLRenderer } from 'three' import type { ThreeRendererMemorySnapshot, @@ -74,7 +74,7 @@ export function createVrmSceneSummarySnapshot(input?: VrmSceneSnapshotInput | VR const materials = new Set() const textures = new Set() - activeVrm.scene.traverse((child) => { + activeVrm.scene.traverse((child: Object3D) => { if (child instanceof Mesh) meshCount += 1 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e0fdbdd0..1b4683285 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1836,7 +1836,7 @@ importers: version: 5.0.0(vue@3.5.29(typescript@5.9.3)) '@intlify/unplugin-vue-i18n': specifier: ^11.0.7 - version: 11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)) + version: 11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)) '@mdit/plugin-footnote': specifier: ^0.22.4 version: 0.22.4(markdown-it@14.1.1) @@ -1896,7 +1896,7 @@ importers: version: 0.1.3 unplugin-yaml: specifier: ^4.0.0 - version: 4.0.0(esbuild@0.27.2)(rolldown@1.0.0-rc.3)(rollup@4.59.0)(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.3)(rollup@4.59.0)(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) vitepress: specifier: ^2.0.0-alpha.16 version: 2.0.0-alpha.16(@types/node@24.10.14)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(nprogress@0.2.0)(postcss@8.5.6)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) @@ -3234,7 +3234,7 @@ importers: version: 14.1.0(vue@3.5.29(typescript@5.9.3)) '@wxt-dev/module-vue': specifier: ^1.0.3 - version: 1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) nanoid: specifier: ^5.1.6 version: 5.1.6 @@ -19667,6 +19667,31 @@ snapshots: - supports-color - typescript + '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) + '@intlify/bundle-utils': 11.0.7(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3))) + '@intlify/shared': 11.2.8 + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.2.8)(@vue/compiler-dom@3.5.29)(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + debug: 4.4.3 + fast-glob: 3.3.3 + pathe: 2.0.3 + picocolors: 1.1.1 + unplugin: 2.3.11 + vite: 7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vue: 3.5.29(typescript@5.9.3) + optionalDependencies: + vue-i18n: 11.2.8(vue@3.5.29(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/compiler-dom' + - eslint + - rollup + - supports-color + - typescript + '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) @@ -23891,9 +23916,9 @@ snapshots: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 - '@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@wxt-dev/module-vue@1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@vitejs/plugin-vue': 6.0.4(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + '@vitejs/plugin-vue': 6.0.4(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) wxt: 0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - vite