From 36ec31e2fac11a180a6bded397af892b5eff1b13 Mon Sep 17 00:00:00 2001 From: Disaster-Terminator <2557058999@qq.com> Date: Fri, 17 Apr 2026 18:56:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=86=B7=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E6=97=B6=E7=AA=97=E5=8F=A3=E5=B0=BA=E5=AF=B8=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=BC=82=E5=B8=B8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useModel.ts | 73 +++++++++++++--- src/pages/main/index.vue | 167 +++++++++++++++++++++++++++++------- 2 files changed, 194 insertions(+), 46 deletions(-) diff --git a/src/composables/useModel.ts b/src/composables/useModel.ts index a9c4ae96..7f3bbbe8 100644 --- a/src/composables/useModel.ts +++ b/src/composables/useModel.ts @@ -1,6 +1,6 @@ import type { PhysicalPosition } from '@tauri-apps/api/dpi' -import { LogicalSize } from '@tauri-apps/api/dpi' +import { PhysicalSize } from '@tauri-apps/api/dpi' import { resolveResource, sep } from '@tauri-apps/api/path' import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' import { message } from 'ant-design-vue' @@ -18,6 +18,8 @@ import live2d from '../utils/live2d' const appWindow = getCurrentWebviewWindow() const digitKeys = '1234567890'.split('') as readonly string[] const letterKeys = 'QWERTYUIOPASDFGHJKLZXCVBNM'.split('') as readonly string[] +let suppressScaleWriteback = false +let hasCompletedInitialWindowSizeSync = false export interface ModelSize { width: number @@ -65,11 +67,16 @@ export function useModel() { return `${modelId}:expression:${index}` } - async function handleLoad() { + async function handleLoad( + model = modelStore.currentModel, + options: { showError?: boolean } = {}, + ) { try { - if (!modelStore.currentModel) return + if (!model) return false - const { path } = modelStore.currentModel + hasCompletedInitialWindowSizeSync = false + + const { path } = model await resolveResource(path) @@ -83,7 +90,7 @@ export function useModel() { handleResize() - const modelId = modelStore.currentModel.id + const modelId = model.id const behaviorIds: string[] = [] @@ -106,8 +113,15 @@ export function useModel() { modelStore.shortcuts[id] = shortcut } + return true } catch (error) { - message.error(String(error)) + modelSize.value = void 0 + + if (options.showError ?? true) { + message.error(String(error)) + } + + return false } } @@ -120,20 +134,52 @@ export function useModel() { live2d.resizeModel(modelSize.value) + if (!hasCompletedInitialWindowSizeSync || suppressScaleWriteback) { + suppressScaleWriteback = false + return + } + + const { width } = modelSize.value + + const size = await appWindow.size() + + catStore.window.scale = round((size.width / width) * 100) + } + + async function syncWindowSize() { + if (!modelSize.value) return false + const { width, height } = modelSize.value + const nextWidth = Math.round(width * (catStore.window.scale / 100)) + const nextHeight = Math.round(height * (catStore.window.scale / 100)) + const size = await appWindow.size() + + if (size.width === nextWidth && size.height === nextHeight) { + hasCompletedInitialWindowSizeSync = true + return true + } + + suppressScaleWriteback = true - if (round(innerWidth / innerHeight, 1) !== round(width / height, 1)) { + try { await appWindow.setSize( - new LogicalSize({ - width: innerWidth, - height: Math.ceil(innerWidth * (height / width)), + new PhysicalSize({ + width: nextWidth, + height: nextHeight, }), ) - } - const size = await appWindow.size() + hasCompletedInitialWindowSizeSync = true - catStore.window.scale = round((size.width / width) * 100) + return true + } catch (error) { + suppressScaleWriteback = false + hasCompletedInitialWindowSizeSync = false + + message.error(String(error)) + + return false + } } const handlePress = (key: string) => { @@ -238,6 +284,7 @@ export function useModel() { handleLoad, handleDestroy, handleResize, + syncWindowSize, handleKeyChange, handleMouseChange, handleMouseMove, diff --git a/src/pages/main/index.vue b/src/pages/main/index.vue index f57b1f2a..0757fbd2 100644 --- a/src/pages/main/index.vue +++ b/src/pages/main/index.vue @@ -2,7 +2,6 @@ import type { MotionInfo } from 'easy-live2d' import { convertFileSrc } from '@tauri-apps/api/core' -import { PhysicalSize } from '@tauri-apps/api/dpi' import { Menu, PredefinedMenuItem } from '@tauri-apps/api/menu' import { sep } from '@tauri-apps/api/path' import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' @@ -10,7 +9,7 @@ import { exists, readDir } from '@tauri-apps/plugin-fs' import { useDebounceFn, useEventListener } from '@vueuse/core' import { round } from 'es-toolkit' import { nth } from 'es-toolkit/compat' -import { onMounted, onUnmounted, ref, watch } from 'vue' +import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue' import { useAppMenu } from '@/composables/useAppMenu' import { useDevice } from '@/composables/useDevice' @@ -29,7 +28,7 @@ import { clearObject } from '@/utils/shared' const { startListening } = useDevice() const appWindow = getCurrentWebviewWindow() -const { modelSize, handleLoad, handleDestroy, handleResize, handleKeyChange } = useModel() +const { modelSize, handleLoad, handleDestroy, handleResize, syncWindowSize, handleKeyChange } = useModel() const catStore = useCatStore() const { getBaseMenu, getExitMenu } = useAppMenu() const modelStore = useModelStore() @@ -37,8 +36,11 @@ const generalStore = useGeneralStore() const resizing = ref(false) const backgroundImagePath = ref() const { stickActive } = useGamepad() - -onMounted(startListening) +const isCanvasReady = ref(false) +const INITIAL_MODEL_LOAD_RETRY_DELAY_MS = 100 +let loadedModelId: string | undefined +let modelLoadVersion = 0 +let modelLoadBarrier = Promise.resolve() onUnmounted(handleDestroy) @@ -54,49 +56,148 @@ useEventListener('resize', () => { debouncedResize() }) -watch(() => modelStore.currentModel, async (model) => { - if (!model) return +function waitForAnimationFrame() { + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) +} + +function waitForDelay(delayMs: number) { + return new Promise((resolve) => { + setTimeout(resolve, delayMs) + }) +} + +async function ensureCanvasReady() { + await nextTick() + + let canvas = document.getElementById('live2dCanvas') + + if (!(canvas instanceof HTMLCanvasElement)) { + await waitForAnimationFrame() + canvas = document.getElementById('live2dCanvas') + } + + if (!(canvas instanceof HTMLCanvasElement)) { + throw new TypeError('[main] #live2dCanvas is not ready') + } + + isCanvasReady.value = true +} + +async function loadCurrentModel( + model = modelStore.currentModel, + options: { retryOnFailure?: boolean } = {}, +) { + if (!model) return false + + const version = ++modelLoadVersion + let completed = false + const previousLoad = modelLoadBarrier + let releaseCurrentLoad!: () => void + const currentLoad = new Promise((resolve) => { + releaseCurrentLoad = resolve + }) + + modelLoadBarrier = currentLoad + + await previousLoad + + const isActiveLoad = () => { + return version === modelLoadVersion && modelStore.currentModel?.id === model.id + } + + try { + if (!isActiveLoad()) { + return false + } + + modelStore.modelReady = false - await handleLoad() + let didLoad = await handleLoad(model, { showError: !options.retryOnFailure }) - const path = join(model.path, 'resources', 'background.png') + if (!didLoad && options.retryOnFailure) { + await waitForDelay(INITIAL_MODEL_LOAD_RETRY_DELAY_MS) - const existed = await exists(path) + if (!isActiveLoad()) { + return false + } - backgroundImagePath.value = existed ? convertFileSrc(path) : void 0 + didLoad = await handleLoad(model) + } + + if (!didLoad || !isActiveLoad()) { + return false + } + + const didSyncWindowSize = await syncWindowSize() + + if (!didSyncWindowSize) { + return false + } - clearObject([modelStore.supportKeys, modelStore.pressedKeys]) + if (!isActiveLoad()) { + return false + } + + const path = join(model.path, 'resources', 'background.png') + + const existed = await exists(path) - const resourcePath = join(model.path, 'resources') - const groups = ['left-keys', 'right-keys'] + backgroundImagePath.value = existed ? convertFileSrc(path) : void 0 - for await (const groupName of groups) { - const groupDir = join(resourcePath, groupName) - const files = await readDir(groupDir).catch(() => []) - const imageFiles = files.filter(file => isImage(file.name)) + clearObject([modelStore.supportKeys, modelStore.pressedKeys]) - for (const file of imageFiles) { - const fileName = file.name.split('.')[0] + const resourcePath = join(model.path, 'resources') + const groups = ['left-keys', 'right-keys'] - modelStore.supportKeys[fileName] = join(groupDir, file.name) + for await (const groupName of groups) { + const groupDir = join(resourcePath, groupName) + const files = await readDir(groupDir).catch(() => []) + const imageFiles = files.filter(file => isImage(file.name)) + + for (const file of imageFiles) { + const fileName = file.name.split('.')[0] + + modelStore.supportKeys[fileName] = join(groupDir, file.name) + } } + + if (!isActiveLoad()) { + return false + } + + loadedModelId = model.id + modelStore.modelReady = true + completed = true + + return true + } finally { + if (!completed && isActiveLoad()) { + modelStore.modelReady = true + } + + releaseCurrentLoad() } +} - modelStore.modelReady = true -}, { deep: true, immediate: true }) +onMounted(async () => { + startListening() + await ensureCanvasReady() + await loadCurrentModel(modelStore.currentModel, { retryOnFailure: true }) +}) -watch([() => catStore.window.scale, modelSize], async ([scale, modelSize]) => { - if (!modelSize) return +watch(() => modelStore.currentModel?.id, async (modelId) => { + if (!isCanvasReady.value || !modelId || modelId === loadedModelId) return - const { width, height } = modelSize + await loadCurrentModel() +}) - appWindow.setSize( - new PhysicalSize({ - width: Math.round(width * (scale / 100)), - height: Math.round(height * (scale / 100)), - }), - ) -}, { immediate: true }) +watch(() => catStore.window.scale, async () => { + if (!modelSize.value) return + + await syncWindowSize() +}) watch([modelStore.pressedKeys, stickActive], ([keys, stickActive]) => { const dirs = Object.values(keys).map((path) => {