From d2ce04b21be8fc8123400abe35ff5c14f9289608 Mon Sep 17 00:00:00 2001 From: Lilia_Chen Date: Fri, 18 Jul 2025 18:17:41 +0100 Subject: [PATCH] feat(stage-ui): Live2D vs VRM page switch botton and VRM model uploading functionality (#290) --------- Co-authored-by: Neko --- packages/i18n/src/locales/en/settings.yaml | 24 +++++ .../i18n/src/locales/zh-Hans/settings.yaml | 23 +++++ .../Settings/ModelSettings/Live2D.vue | 14 +++ .../Scenarios/Settings/ModelSettings/VRM.vue | 98 ++++++++++++++++++- .../Settings/ModelSettings/VRMScene.vue | 55 ++++++++++- .../Settings/ModelSettings/index.vue | 12 ++- .../stage-ui/src/components/Scenes/VRM.vue | 13 ++- packages/stage-ui/src/stores/index.ts | 1 + packages/stage-ui/src/stores/vrm.ts | 51 ++++++++++ 9 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 packages/stage-ui/src/stores/vrm.ts diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index 994a95252..219d78f23 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -56,6 +56,9 @@ live2d: scale: Scale x: X 'y': 'Y' + switch-to-vrm: + title: Switch to 3D Avator? + change-to-vrm: Click here to switch to the 3D avator setting (VRM) theme-color-from-model: title: Extract colors from model button-extract: @@ -536,3 +539,24 @@ theme: Switch the base theme of AIRI, Light mode or Dark mode. title: Settings voices: Voice +vrm: + change-model: + from-file: Load from File + from-file-select: Select + from-url: Load from URL + from-url-confirm: Load + from-url-placeholder: Enter VRM model URL + title: Change Model + title: VRM Settings + scale-and-position: + title: Scale And Position + scale: Scale + x: X + 'y': 'Y' + switch-to-vrm: + title: Switch to Live2D Avatar? + change-to-vrm: Click here to switch to the Live2D avatar setting + theme-color-from-model: + title: Extract colors from model + button-extract: + title: Extract diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index 2f3dc3e1d..39b7eae5d 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -52,6 +52,9 @@ live2d: scale: 缩放 x: X 'y': 'Y' + switch-to-vrm: + title: 想切换至3D虚拟形象? + change-to-vrm: 切换至3D虚拟形象设定页面(VRM) theme-color-from-model: button-extract: title: 提取 @@ -512,3 +515,23 @@ theme: 切换 AIRI 的基础主题,亮色模式或暗色模式。 title: 设置 voices: 声线 +vrm: + change-model: + from-file: 从文件加载 + from-file-select: 选择 + from-url: 从 URL 加载 + from-url-confirm: 加载 + from-url-placeholder: 输入 VRM 模型 URL + title: 更换模型 + title: VRM 设置 + scale-and-position: + title: 缩放与位置 + scale: 缩放 + x: X + 'y': 'Y' + switch-to-vrm: + title: 想切换至Live2D虚拟形象? + change-to-vrm: 切换至Live2D虚拟形象设定页面 + theme-color-from-model: + button-extract: + title: 提取 diff --git a/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/Live2D.vue b/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/Live2D.vue index b009158ab..5d8659ad7 100644 --- a/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/Live2D.vue +++ b/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/Live2D.vue @@ -20,6 +20,7 @@ defineProps<{ defineEmits<{ (e: 'extractColorsFromModel'): void + (e: 'switchToVRM'): void }>() const { t } = useI18n() @@ -120,6 +121,19 @@ const exportObjectUrl = useObjectUrl(modelFile) + + - diff --git a/packages/stage-ui/src/components/Scenes/VRM.vue b/packages/stage-ui/src/components/Scenes/VRM.vue index c1eae1beb..01431fdb4 100644 --- a/packages/stage-ui/src/components/Scenes/VRM.vue +++ b/packages/stage-ui/src/components/Scenes/VRM.vue @@ -2,7 +2,8 @@ import { TransitionVertical } from '@proj-airi/ui' import { OrbitControls } from '@tresjs/cientos' import { TresCanvas } from '@tresjs/core' -import { ref } from 'vue' +import { storeToRefs } from 'pinia' +import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' import DataGuiRange from '../DataGui/Range.vue' @@ -10,9 +11,9 @@ import Collapsable from '../Misc/Collapsable.vue' import Screen from '../Misc/Screen.vue' import VRMModel from './VRM/Model.vue' +import { useVRM } from '../../stores' + const props = defineProps<{ - model: string - idleAnimation: string paused: boolean }>() @@ -23,6 +24,8 @@ const emit = defineEmits<{ const show = ref(false) +const { modelFile, modelUrl, loadSource, selectedModel } = storeToRefs(useVRM()) + const cameraPositionX = ref(-0.17) const cameraPositionY = ref(0) const cameraPositionZ = ref(-1) @@ -52,8 +55,8 @@ defineExpose({ { + const modelFile = ref() + const modelUrl = ref('/assets/vrm/models/AvatarSample-B/AvatarSample_B.vrm') + const loadSource = ref<'file' | 'url'>('url') + const loadingModel = ref(false) + const position = useLocalStorage('settings/vrm/position', { x: 0, y: 0 }) + const positionInPercentageString = computed(() => ({ + x: `${position.value.x}%`, + y: `${position.value.y}%`, + })) + + const modelObjectUrl = ref() + + // Manage the object URL lifecycle to prevent memory leaks + watch(modelFile, (newFile) => { + if (modelObjectUrl.value) { + URL.revokeObjectURL(modelObjectUrl.value) + modelObjectUrl.value = undefined + } + if (newFile) { + modelObjectUrl.value = URL.createObjectURL(newFile) + } + }) + + // Centralized computed property for the model source + const selectedModel = computed(() => { + if (loadSource.value === 'file' && modelObjectUrl.value) { + return modelObjectUrl.value + } + if (loadSource.value === 'url' && modelUrl.value) { + return modelUrl.value + } + // Fallback model + return '/assets/vrm/models/AvatarSample-B/AvatarSample_B.vrm' + }) + + return { + modelFile, + modelUrl, + loadSource, + loadingModel, + position, + positionInPercentageString, + selectedModel, // Expose the new computed property + } +}) +