fix(stage-*): improve cursor tracking (#1842)

This commit is contained in:
DrHuangMHT
2026-05-20 22:30:38 +08:00
committed by GitHub
parent 4dbf630f32
commit 7f9eeb715d
38 changed files with 983 additions and 471 deletions
@@ -15,6 +15,7 @@ import {
useElectronRelativeMouse,
} from '@proj-airi/electron-vueuse'
import { IS_DEV } from '@proj-airi/stage-shared'
import { useSettingsLive2d } from '@proj-airi/stage-ui-live2d'
import { useModelStore, useThreeSceneIsTransparentAtPoint } from '@proj-airi/stage-ui-three'
import { HoloCoupon } from '@proj-airi/stage-ui/components'
import {
@@ -41,7 +42,6 @@ import { modelSettingsRuntimeSnapshotChannelName } from '../../shared/model-sett
import { useChatSyncStore } from '../stores/chat-sync'
import { useControlsIslandStore } from '../stores/controls-island'
import { useStageWindowLifecycleStore } from '../stores/stage-window-lifecycle'
import { useWindowStore } from '../stores/window'
import { shouldSampleStageTransparency } from '../utils/stage-three-transparency'
const controlsIslandRef = ref<InstanceType<typeof ControlsIsland>>()
@@ -112,8 +112,6 @@ const isAroundWindowBorderFor250Ms = refDebounced(isAroundWindowBorder, 250)
const setIgnoreMouseEvents = useElectronEventaInvoke(electron.window.setIgnoreMouseEvents)
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
const { pause, resume } = watch(isTransparent, (transparent) => {
shouldFadeOnCursorWithin.value = fadeOnHoverEnabled.value && !transparent
}, { immediate: true })
@@ -457,6 +455,17 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
})
// Assistant caption is broadcast from Stage.vue via the same channel
const { live2dEyeTrackingSource } = storeToRefs(useSettingsLive2d())
live2dEyeTrackingSource.value = computed(() => ({
x: relativeMouseX.value,
y: relativeMouseY.value,
}))
const { trackingSource } = storeToRefs(useModelStore())
trackingSource.value = computed(() => ({
x: relativeMouseX.value,
y: relativeMouseY.value,
}))
</script>
<template>
@@ -492,7 +501,6 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
h-full w-full
flex-1
:paused="stagePaused"
:focus-at="{ x: live2dLookAtX, y: live2dLookAtY }"
/>
<HoloCoupon />
<ControlsIsland
+1
View File
@@ -39,6 +39,7 @@
"@proj-airi/stage-pages": "workspace:^",
"@proj-airi/stage-shared": "workspace:^",
"@proj-airi/stage-ui": "workspace:^",
"@proj-airi/stage-ui-live2d": "workspace:^",
"@proj-airi/stage-ui-three": "workspace:^",
"@proj-airi/stream-kit": "workspace:^",
"@proj-airi/ui": "workspace:^",
+14 -5
View File
@@ -10,6 +10,8 @@ import workletUrl from '@proj-airi/stage-ui/workers/vad/process.worklet?worker&u
import { BackgroundProvider } from '@proj-airi/stage-layouts/components/Backgrounds'
import { useBackgroundThemeColor } from '@proj-airi/stage-layouts/composables/theme-color'
import { useBackgroundStore } from '@proj-airi/stage-layouts/stores/background'
import { useSettingsLive2d } from '@proj-airi/stage-ui-live2d'
import { useModelStore } from '@proj-airi/stage-ui-three'
import { HoloCoupon } from '@proj-airi/stage-ui/components'
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
import { useAudioRecorder } from '@proj-airi/stage-ui/composables/audio/audio-recorder'
@@ -29,7 +31,6 @@ function handleSettingsOpen(open: boolean) {
paused.value = open
}
const positionCursor = useMouse()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('md')
@@ -147,6 +148,18 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
}
}
})
const { live2dEyeTrackingSource } = storeToRefs(useSettingsLive2d())
const { x: mouseX, y: mouseY } = useMouse()
live2dEyeTrackingSource.value = computed(() => ({
x: mouseX.value,
y: mouseY.value,
}))
const { trackingSource } = storeToRefs(useModelStore())
trackingSource.value = computed(() => ({
x: mouseX.value,
y: mouseY.value,
}))
</script>
<template>
@@ -167,10 +180,6 @@ watch([stream, () => vadLoaded.value], async ([s, loaded]) => {
<WidgetStage
flex-1 min-w="1/2"
:paused="paused"
:focus-at="{
x: positionCursor.x.value,
y: positionCursor.y.value,
}"
/>
<InteractiveArea v-if="!isMobile" h="85dvh" absolute right-4 flex flex-1 flex-col max-w="500px" min-w="30%" />
<MobileInteractiveArea v-if="isMobile" @settings-open="handleSettingsOpen" />
+68 -11
View File
@@ -114,6 +114,22 @@ analytics:
description: Turn this off to opt out of analytics collection.
disabled:
title: Analytics disabled for this development build
model-select:
panel-callout:
support-status-header: We support both 2D and 3D models
support-status: >
Click {select-button} to select or import models.
Currently, {zip} (for Live2D models) and {vrm} (for VRM models) are supported.
model-type-example: >
Neuro-sama uses 2D model driven by framework developed by Live2D Inc,
while Grok Ani (in Grok Companion) uses 3D model driven by VRM / MMD open formats.
select-model:
title: Model Selector
button: Select model
select: Pick
import: Import
remove: Remove
confirm: Confirm
live2d:
change-model:
from-file: Load from File
@@ -138,19 +154,60 @@ live2d:
change-to-vrm: Click here to switch to the 3D avatar setting (VRM)
theme-color-from-model:
title: Extract colors from model
description: >
Extract prominent colors from current rendering canvas.
Click on the color to apply it to AIRI.
Try to extract multiple times, with the model in different poses,
if the colors are not desirable.
button-extract:
title: Extract
focus:
title: Disable model mouse tracking
button-disable:
title: Disable model mouse tracking
render-scale:
title: Render scale
fps:
title: FPS
description: Limit rendering frame rate
options:
unlimited:
animation:
title: Animation
focus:
title: Enable mouse tracking
description: Track current mouse position. May conflict with force idle eye animation
offset: Eye offset(%)
force-idle-eye-animation:
title: Force eye animation when idle
description: Randomly look around when idle
auto-blink:
title: Auto blink
description: Use default blinking animation of the model
force-auto-blink:
title: Force auto blink
description: Force blinking animation with external timer
idle-motion:
title: Idle animation
no-motion: No animation available
placeholder: Select animation
disable-motion: Disabled
disable-motion-description: Disable idle animation
parameters:
title: Model Parameters
render-scale:
title: Render scale
fps:
title: FPS
description: Limit rendering frame rate
options:
unlimited:
shadow: Drop shadow
reset-parameters: Reset to default parameters
clear-model-cache: Clear model cache
expressions:
title: Expressions
override-toggle: Expression system
sdk-preset-preserved-notice: >
SDK expression manager and eye blink are preserved when disabled.
no-expression: No expressions available for this model.
llm-integration-wip: LLM tool integration is not yet wired up.
save-default: Save expression defaults
reset: Reset all expressions
expose-to-llm-toggle: Expose to LLM
expose-to-llm-options:
none: None
all: All
custom: Custom
microphone: Microphone
models: Model
pages:
+15 -11
View File
@@ -131,17 +131,21 @@ live2d:
title: Extraer colores del modelo
button-extract:
title: Extraer
focus:
title: Desactivar seguimiento del ratón del modelo
button-disable:
title: Desactivar seguimiento del ratón del modelo
render-scale:
title: Escala de renderizado
fps:
title: FPS
description: Limitar velocidad de fotogramas
options:
unlimited: '∞'
animation:
title: Animación
focus:
title: Habilitar el seguimiento del cursor
parameters:
title: Parámetros del modelo
render-scale:
title: Escala de renderizado
fps:
title: FPS
description: Limitar velocidad de fotogramas
options:
unlimited: '∞'
expressions:
title: Expresión
microphone: Micrófono
models: Modelo
pages:
+15 -11
View File
@@ -131,17 +131,21 @@ live2d:
title: Extraire les couleurs du modèle
button-extract:
title: Extraire
focus:
title: Désactiver le suivi du modèle par la souris
button-disable:
title: Désactiver le suivi souris du modèle
render-scale:
title: Échelle de rendu
fps:
title: FPS
description: Limiter la fréquence d'image du rendu
options:
unlimited: '∞'
animation:
title: Animation
focus:
title: Activer le suivi du curseur
parameters:
title: Paramètres du modèle
render-scale:
title: Échelle de rendu
fps:
title: FPS
description: Limiter la fréquence d'image du rendu
options:
unlimited: '∞'
expressions:
title: Expressions
microphone: Microphone
models: Modèle
pages:
+15 -11
View File
@@ -131,17 +131,21 @@ live2d:
title: モデルから色を抽出
button-extract:
title: 抽出
focus:
title: モデルのマウストラッキングを無効にする
button-disable:
title: モデルのマウストラッキングを無効にする
render-scale:
title: レンダリングスケール
fps:
title: FPS
description: レンダリングフレームレートを制限
options:
unlimited: '∞'
animation:
title: アニメーション
focus:
title: カーソル追跡を有効化
parameters:
title: モデルパラメータ
render-scale:
title: レンダリングスケール
fps:
title: FPS
description: レンダリングフレームレートを制限
options:
unlimited: '∞'
expressions:
title: 表情
microphone: マイク
models: モデル
pages:
+4 -4
View File
@@ -131,10 +131,10 @@ live2d:
title: 모델로부터 색상 추출
button-extract:
title: 추출
focus:
title: 마우스 포인터 추적 비활성화
button-disable:
title: Disable model mouse tracking
animation:
title: 애니메이션
focus:
title: 커서 추적 사용
render-scale:
title: Render scale
fps:
+4 -4
View File
@@ -131,10 +131,10 @@ live2d:
title: Извлечь цвета из модели
button-extract:
title: Извлечь
focus:
title: Отключить слежение за мышью
button-disable:
title: Отключить отслеживание мыши модели
animation:
title: анимация
focus:
title: Включить отслеживание курсора
render-scale:
title: Масштаб рендеринга
fps:
+4 -4
View File
@@ -131,10 +131,10 @@ live2d:
title: Trích xuất màu từ mô hình
button-extract:
title: Trích xuất
focus:
title: Tắt theo dõi chuột cho mô hình
button-disable:
title: Disable model mouse tracking
animation:
title: Diễn hoạt
focus:
title: Bật theo dõi con trỏ
render-scale:
title: Render scale
fps:
+66 -11
View File
@@ -105,6 +105,22 @@ analytics:
description: 关闭任何非必要使用数据及崩溃分析的采集与上传
disabled:
title: 此开发版本的使用数据及崩溃分析功能已禁用
model-select:
panel-callout:
support-status-header: 我们支持2D和3D模型
support-status: >
点击 {select-button} 以选择或导入模型。
目前我们支持从 {zip} 导入Live2D模型,和从 {vrm} 导入VRM模型。
model-type-example: >
Neuro-sama 采用来自Live2D Inc.公司的技术所制作的2D模型,
而 Grok Ani (来自 Grok Companion)使用 VRM / MMD 相关开源技术制作的3D模型。
select-model:
title: 模型选择
button: 选择模型
select: 选择
import: 导入
remove: 移除
confirm: 确认
live2d:
change-model:
from-file: 从文件加载
@@ -129,19 +145,58 @@ live2d:
change-to-vrm: 切换至3D虚拟形象设定页面(VRM
theme-color-from-model:
title: 从模型提取主题颜色
description: >
从当前模型画布中提取主题色,点击颜色即可将其应用至 AIRI 界面。
若没有获取到想要的颜色,可改变模型动作,多次尝试提取。
button-extract:
title: 提取
focus:
title: 禁用模型鼠标跟踪
button-disable:
title: 禁用 Live2D 模型的鼠标追踪
render-scale:
title: 渲染精度
fps:
title: 帧率
description: 限制渲染帧率
options:
unlimited: '∞'
animation:
title: 动画
focus:
title: 启用鼠标追踪
description: 让模型看向当前光标所在位置。可能与强制眼球动画冲突。
offset: 眼部偏移量(%)
force-idle-eye-animation:
title: 强制眼球动画
description: 待机时随机看向屏幕各处
auto-blink:
title: 使用眨眼动画
description: 使用模型默认眨眼动画
force-auto-blink:
title: 强制眨眼动画
description: 强制模型眨眼,模型无眨眼动画时可用。
idle-motion:
title: 待机动作
no-motion: 无可用动作
placeholder: 选择待机动作
disable-motion: 禁用
disable-motion-description: 禁用待机动作
parameters:
title: 模型参数
render-scale:
title: 渲染精度
fps:
title: 帧率
description: 限制渲染帧率
options:
unlimited:
shadow: 阴影
reset-parameters: 重置所有参数至默认
clear-model-cache: 清除模型缓存
expressions:
title: 表情
override-toggle: 启用表情
sdk-preset-preserved-notice: >
SDK提供的表情和眼部动作被禁用后会保留
no-expression: 当前模型无可用表情
llm-integration-wip: 表情系统暂未接入语言模型
save-default: 保存为默认表情
reset: 重置表情状态
expose-to-llm-toggle: 允许LLM使用模型表情
expose-to-llm-options:
none:
all: 全部
custom: 仅指定
microphone: 麦克风
models: 模型
pages:
@@ -131,10 +131,10 @@ live2d:
title: 從模型中提取顏色
button-extract:
title: 提取
focus:
title: 停用模型滑鼠追蹤
button-disable:
title: Disable model mouse tracking
animation:
title: 動畫
focus:
title: 啟用游標追蹤
render-scale:
title: Render scale
fps:
+1 -1
View File
@@ -23,7 +23,7 @@
"./composables/live2d": "./src/composables/live2d/index.ts",
"./constants/emotions": "./src/constants/emotions.ts",
"./stores": "./src/stores/index.ts",
"./stores/live2d": "./src/stores/live2d.ts",
"./stores/model-parameters": "./src/stores/model-parameters.ts",
"./stores/expression-store": "./src/stores/expression-store.ts",
"./tools/expression-tools": "./src/tools/expression-tools.ts",
"./utils/eye-motions": "./src/utils/eye-motions.ts",
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { Screen } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { ref, watch } from 'vue'
import SliderControls from '../ViewControls/SliderControls.vue'
import Live2DCanvas from './live2d/Canvas.vue'
import Live2DModel from './live2d/Model.vue'
import { useEyeTracking, useSettingsLive2d } from '../../composables/live2d'
import '../../utils/live2d-zip-loader'
import '../../utils/live2d-opfs-registration'
@@ -16,31 +19,14 @@ withDefaults(defineProps<{
paused?: boolean
mouthOpenSize?: number
nowSpeaking?: boolean
focusAt?: { x: number, y: number }
disableFocusAt?: boolean
themeColorsHue?: number
themeColorsHueDynamic?: boolean
live2dIdleAnimationEnabled?: boolean
live2dAutoBlinkEnabled?: boolean
live2dForceAutoBlinkEnabled?: boolean
live2dExpressionEnabled?: boolean
live2dShadowEnabled?: boolean
live2dMaxFps?: number
live2dRenderScale?: number
}>(), {
paused: false,
focusAt: () => ({ x: 0, y: 0 }),
mouthOpenSize: 0,
nowSpeaking: false,
themeColorsHue: 220.44,
themeColorsHueDynamic: false,
live2dIdleAnimationEnabled: true,
live2dAutoBlinkEnabled: true,
live2dForceAutoBlinkEnabled: false,
live2dExpressionEnabled: true,
live2dShadowEnabled: true,
live2dMaxFps: 0,
live2dRenderScale: 2,
})
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
@@ -48,6 +34,27 @@ const componentStateCanvas = defineModel<'pending' | 'loading' | 'mounted'>('can
const componentStateModel = defineModel<'pending' | 'loading' | 'mounted'>('modelState', { default: 'pending' })
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
const live2dModelRef = ref<InstanceType<typeof Live2DModel>>()
const {
live2dEyeTracking,
live2dIdleAnimationEnabled,
live2dForceIdleEyeAnimation,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dExpressionEnabled,
live2dMaxFps,
live2dRenderScale,
live2dShadowEnabled,
} = storeToRefs(useSettingsLive2d())
const mouseFocus = useEyeTracking(
() => live2dCanvasRef.value?.canvasElement(),
() => ({
normalizedScale: live2dModelRef.value?.modelNormalizeParams.scale ?? 1,
modelWidth: live2dModelRef.value?.initialModelWidth ?? 1000,
modelHeight: live2dModelRef.value?.initialModelHeight ?? 1000,
}),
)
watch([componentStateModel, componentStateCanvas], () => {
componentState.value = (componentStateModel.value === 'mounted' && componentStateCanvas.value === 'mounted')
@@ -81,6 +88,7 @@ defineExpose({
max-h="100dvh"
>
<Live2DModel
ref="live2dModelRef"
v-model:state="componentStateModel"
:model-src="modelSrc"
:model-id="modelId"
@@ -90,11 +98,12 @@ defineExpose({
:width="width"
:height="height"
:paused="paused"
:focus-at="focusAt"
:disable-focus-at="disableFocusAt"
:focus-at="mouseFocus"
:eye-tracking="live2dEyeTracking"
:theme-colors-hue="themeColorsHue"
:theme-colors-hue-dynamic="themeColorsHueDynamic"
:live2d-idle-animation-enabled="live2dIdleAnimationEnabled"
:live2d-force-idle-eye-animation="live2dForceIdleEyeAnimation"
:live2d-auto-blink-enabled="live2dAutoBlinkEnabled"
:live2d-force-auto-blink-enabled="live2dForceAutoBlinkEnabled"
:live2d-expression-enabled="live2dExpressionEnabled"
@@ -5,7 +5,7 @@ import type { PixiLive2DInternalModel } from '../../../composables/live2d'
import { listenBeatSyncBeatSignal } from '@proj-airi/stage-shared/beat-sync'
import { useTheme } from '@proj-airi/ui'
import { breakpointsTailwind, until, useBreakpoints } from '@vueuse/core'
import { until } from '@vueuse/core'
import { animate } from 'animejs'
import { formatHex } from 'culori'
import { Mutex } from 'es-toolkit'
@@ -26,8 +26,9 @@ import {
useMotionUpdatePluginIdleFocus,
useMotionUpdatePluginLipSync,
} from '../../../composables/live2d'
import { useFitModel } from '../../../composables/live2d/fit-model'
import { Emotion, EmotionNeutralMotionName } from '../../../constants/emotions'
import { useL2dViewControl, useLive2d } from '../../../stores/live2d'
import { useL2dViewControl, useLive2dParams } from '../../../stores'
const props = withDefaults(defineProps<{
modelSrc?: string
@@ -40,10 +41,11 @@ const props = withDefaults(defineProps<{
height: number
paused?: boolean
focusAt?: { x: number, y: number }
disableFocusAt?: boolean
eyeTracking?: boolean
themeColorsHue?: number
themeColorsHueDynamic?: boolean
live2dIdleAnimationEnabled?: boolean
live2dForceIdleEyeAnimation?: boolean
live2dAutoBlinkEnabled?: boolean
live2dForceAutoBlinkEnabled?: boolean
live2dExpressionEnabled?: boolean
@@ -53,11 +55,13 @@ const props = withDefaults(defineProps<{
nowSpeaking: false,
paused: false,
focusAt: () => ({ x: 0, y: 0 }),
eyeTracking: false,
disableFocusAt: false,
scale: 1,
themeColorsHue: 220.44,
themeColorsHueDynamic: false,
live2dIdleAnimationEnabled: true,
live2dForceIdleEyeAnimation: true,
live2dAutoBlinkEnabled: true,
live2dForceAutoBlinkEnabled: false,
live2dExpressionEnabled: true,
@@ -72,16 +76,6 @@ const emits = defineEmits<{
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
const { position, scale } = useL2dViewControl()
function parsePropsOffset() {
const xOffset = (position.value.x / 100) * props.width
const yOffset = -(position.value.y / 100) * props.height
return {
xOffset,
yOffset,
}
}
const modelSrcRef = toRef(() => props.modelSrc)
const modelLoading = ref(false)
@@ -90,7 +84,10 @@ let isUnmounted = false
const modelLoadMutex = new Mutex()
const offset = computed(() => parsePropsOffset())
const offset = computed(() => ({
x: (position.value.x / 100) * props.width,
y: -(position.value.y / 100) * props.height,
}))
const pixiApp = toRef(() => props.app)
const paused = toRef(() => props.paused)
@@ -103,8 +100,6 @@ const nowSpeaking = toRef(() => props.nowSpeaking)
const lastUpdateTime = ref(0)
const { isDark: dark } = useTheme()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = computed(() => breakpoints.between('sm', 'md').value || breakpoints.smaller('sm').value)
const dropShadowFilter = shallowRef(new DropShadowFilter({
alpha: 0.2,
blur: 0,
@@ -114,37 +109,25 @@ const dropShadowFilter = shallowRef(new DropShadowFilter({
let resizeAnimation: ReturnType<typeof animate> | undefined
function computeScaleAndPosition() {
let offsetFactor = 2.2
if (isMobile.value) {
offsetFactor = 2.2
}
const modelNormalizeParams = useFitModel(
() => ({ width: props.width, height: props.height }),
() => ({ width: initialModelWidth.value, height: initialModelHeight.value }),
)
const heightScale = (props.height * 0.95 / initialModelHeight.value * offsetFactor)
const widthScale = (props.width * 0.95 / initialModelWidth.value * offsetFactor)
let minScale = Math.min(heightScale, widthScale)
if (Number.isNaN(minScale) || minScale <= 0) {
minScale = 1e-6
}
return {
scale: minScale * scale.value,
x: (props.width / 2) + offset.value.xOffset,
y: props.height + offset.value.yOffset,
}
}
watch([offset, scale, modelNormalizeParams], () => {
setScaleAndPosition()
})
function setScaleAndPosition(animated = false) {
if (!model.value)
return
const target = computeScaleAndPosition()
const normalized = modelNormalizeParams.value
if (!animated) {
model.value.scale.set(target.scale, target.scale)
model.value.x = target.x
model.value.y = target.y
model.value.scale.set(normalized.scale * scale.value, normalized.scale * scale.value)
model.value.x = normalized.x + offset.value.x
model.value.y = normalized.y + offset.value.y
return
}
@@ -157,9 +140,9 @@ function setScaleAndPosition(animated = false) {
}
resizeAnimation = animate(current, {
scale: target.scale,
x: target.x,
y: target.y,
scale: normalized.scale * scale.value,
x: normalized.x + offset.value.x,
y: normalized.y + offset.value.y,
duration: 200,
ease: 'outQuad',
onUpdate: () => {
@@ -172,7 +155,7 @@ function setScaleAndPosition(animated = false) {
})
}
const live2dStore = useLive2d()
const live2dStore = useLive2dParams()
const {
currentMotion,
availableMotions,
@@ -183,6 +166,7 @@ const {
const themeColorsHue = toRef(() => props.themeColorsHue)
const themeColorsHueDynamic = toRef(() => props.themeColorsHueDynamic)
const live2dIdleAnimationEnabled = toRef(() => props.live2dIdleAnimationEnabled)
const live2dForceIdleEyeAnimation = toRef(() => props.live2dForceIdleEyeAnimation)
const live2dAutoBlinkEnabled = toRef(() => props.live2dAutoBlinkEnabled)
const live2dForceAutoBlinkEnabled = toRef(() => props.live2dForceAutoBlinkEnabled)
const live2dExpressionEnabled = toRef(() => props.live2dExpressionEnabled)
@@ -355,6 +339,7 @@ async function loadModel() {
motionManager,
modelParameters,
live2dIdleAnimationEnabled,
live2dForceIdleEyeAnimation,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
lastUpdateTime,
@@ -512,10 +497,6 @@ async function setMotion(motionName: string, index?: number) {
}
}
function handleResize() {
setScaleAndPosition(true)
}
const dropShadowColorComputer = ref<HTMLDivElement>()
const dropShadowAnimationId = ref(0)
@@ -536,12 +517,10 @@ function updateDropShadowFilter() {
model.value.filters = [dropShadowFilter.value]
}
watch([() => props.width, () => props.height], handleResize)
watch(modelSrcRef, async () => await loadModel(), { immediate: true })
watch(dark, updateDropShadowFilter, { immediate: true })
watch([model, themeColorsHue], updateDropShadowFilter)
watch(live2dShadowEnabled, updateDropShadowFilter)
watch([offset, scale], () => setScaleAndPosition())
// TODO: This is hacky!
function updateDropShadowFilterLoop() {
@@ -749,7 +728,7 @@ watch(live2dExpressionEnabled, (enabled) => {
watch(focusAt, (value) => {
if (!model.value)
return
if (props.disableFocusAt)
if (!props.eyeTracking)
return
model.value.focus(value.x, value.y)
@@ -778,6 +757,9 @@ function listMotionGroups() {
defineExpose({
setMotion,
listMotionGroups,
modelNormalizeParams,
initialModelHeight,
initialModelWidth,
})
import.meta.hot?.dispose(() => {
@@ -0,0 +1,46 @@
import type { ComputedRef, MaybeRefOrGetter } from 'vue'
import { storeToRefs } from 'pinia'
import { computed, toValue } from 'vue'
import { useL2dViewControl } from '../../stores'
import { useSettingsLive2d } from './live2d'
const { live2dRenderScale, live2dModelEyeOffset, live2dEyeTrackingSource } = storeToRefs(useSettingsLive2d())
const { scale } = useL2dViewControl()
/**
* Maps the tracking source's position into the model's rendering space. Does not enable tracking by itself.
* @param canvas the canvas that draws the avatar
* @param model normalized scale and dimensions of the model
* @returns a computed ref that maps the tracking source's position into the model's rendering space
*/
export function useEyeTracking(
canvas: MaybeRefOrGetter<HTMLCanvasElement | undefined>,
model: MaybeRefOrGetter<{
normalizedScale: number
modelWidth: number
modelHeight: number
}>,
): ComputedRef<{ x: number, y: number }> {
const mouseFocus = computed(() => {
const { normalizedScale, modelWidth, modelHeight } = toValue(model)
const renderScale = live2dRenderScale.value
// does not require further unwrapping for some reason
const trackingSource = live2dEyeTrackingSource.value as { x: number, y: number } | null
const canvasRect = toValue(canvas)?.getBoundingClientRect()
if (!trackingSource || !(canvasRect)) {
return { x: 1000, y: 1000 }
}
const eyeOffset = {
x: live2dModelEyeOffset.value.x / 100 * modelWidth * normalizedScale * scale.value,
y: live2dModelEyeOffset.value.y / 100 * modelHeight * normalizedScale * scale.value,
}
return {
x: (trackingSource.x - canvasRect.left + eyeOffset.x) * renderScale,
y: (trackingSource.y - canvasRect.top + eyeOffset.y) * renderScale,
}
})
return mouseFocus
}
@@ -0,0 +1,42 @@
import type { MaybeRefOrGetter } from 'vue'
import { isStageWeb } from '@proj-airi/stage-shared'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { computed, toValue } from 'vue'
const breakpoints = useBreakpoints(breakpointsTailwind)
const startingOffsetY = computed(() => {
if (isStageWeb()) // showing upper half of the body in landscape, 3/4 in portrait, in web targets
return breakpoints.smallerOrEqual('md').value ? 0.75 : 1
return 1 // upper half
})
/**
* Normalizes the model so that user `scale == 1` means twice the viewport height,
* and the model centered horizontally when `position.x == 0`,
* showing upper half of the body when `position.y == 0`
*/
export function useFitModel(
canvasDim: MaybeRefOrGetter<{ width: number, height: number }>,
modelDim: MaybeRefOrGetter<{ width: number, height: number }>,
) {
const normalizedParam = computed(() => {
const canvas = toValue(canvasDim)
const model = toValue(modelDim)
const heightScale = (canvas.height / model.height * 2)
const widthScale = (canvas.width / model.width * 2)
let minScale = Math.min(heightScale, widthScale)
if (Number.isNaN(minScale) || minScale <= 0) {
minScale = 1e-6
}
return {
scale: minScale,
x: canvas.width / 2,
y: canvas.height * startingOffsetY.value,
}
})
return normalizedParam
}
@@ -1,4 +1,6 @@
export * from './animation'
export * from './beat-sync'
export * from './expression-controller'
export * from './eye-tracking'
export * from './live2d'
export * from './motion-manager'
@@ -0,0 +1,74 @@
import type { ComputedRef, Ref } from 'vue'
import { useLocalStorageManualReset, useVersionedLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
import { ref } from 'vue'
const live2dEyeTracking = useLocalStorageManualReset<boolean>('settings/live2d/eye-tracking', false)
/**
* A position to perform eye-tracking on.
* Should be a position relative to the application window:
* - for browser targets, it should be the top-left corner of the viewport
* - for tamagotchi targets, it should be the top-left corner of
* the application window that renders the model
*/
const live2dEyeTrackingSource: Ref<ComputedRef<{ x: number, y: number }> | null> = ref(null)
/** Offset from model center to the eyes of the model, in percentages of full model width/height */
const live2dModelEyeOffset = useLocalStorageManualReset('settings/live2d/model-eye-offset', { x: 0, y: 0 })
const live2dIdleAnimationEnabled = useLocalStorageManualReset<boolean>('settings/live2d/idle-animation-enabled', true)
/** Force the avatar to look around. May conflict with eye tracking and idle animation. */
const live2dForceIdleEyeAnimation = useLocalStorageManualReset<boolean>('settings/live2d/idle-eye-animation-enabled', false)
const live2dAutoBlinkEnabled = useVersionedLocalStorageManualReset<boolean>('settings/live2d/auto-blink-enabled', false, {
defaultVersion: '2.0.0',
satisfiesVersionBy(beforeVersion, afterVersion) {
if (beforeVersion === afterVersion) {
return true
}
return false
},
})
const live2dForceAutoBlinkEnabled = useVersionedLocalStorageManualReset<boolean>('settings/live2d/force-auto-blink-enabled', true, {
defaultVersion: '2.0.0',
satisfiesVersionBy(beforeVersion, afterVersion) {
if (beforeVersion === afterVersion) {
return true
}
return false
},
})
const live2dExpressionEnabled = useLocalStorageManualReset<boolean>('settings/live2d/expression-enabled', false)
const live2dShadowEnabled = useLocalStorageManualReset<boolean>('settings/live2d/shadow-enabled', true)
const live2dMaxFps = useLocalStorageManualReset<number>('settings/live2d/max-fps', 0)
const live2dRenderScale = useLocalStorageManualReset<number>('settings/live2d/render-scale', 2)
function resetState() {
live2dEyeTracking.reset()
live2dModelEyeOffset.reset()
live2dIdleAnimationEnabled.reset()
live2dForceIdleEyeAnimation.reset()
live2dAutoBlinkEnabled.reset()
live2dForceAutoBlinkEnabled.reset()
live2dExpressionEnabled.reset()
live2dShadowEnabled.reset()
live2dMaxFps.reset()
live2dRenderScale.reset()
}
export const useSettingsLive2d = defineStore('settings-live2d', () => {
return {
live2dEyeTracking,
live2dEyeTrackingSource,
live2dModelEyeOffset,
live2dIdleAnimationEnabled,
live2dForceIdleEyeAnimation,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dExpressionEnabled,
live2dShadowEnabled,
live2dMaxFps,
live2dRenderScale,
resetState,
}
})
@@ -28,6 +28,7 @@ export type MotionManagerPluginContext = MotionManagerUpdateContext & {
motionManager: PixiLive2DInternalModel['motionManager']
modelParameters: Ref<any>
live2dIdleAnimationEnabled: Ref<boolean>
live2dForceIdleEyeAnimation: Ref<boolean>
live2dAutoBlinkEnabled: Ref<boolean>
live2dForceAutoBlinkEnabled: Ref<boolean>
isIdleMotion: boolean
@@ -42,6 +43,7 @@ export interface UseLive2DMotionManagerUpdateOptions {
motionManager: PixiLive2DInternalModel['motionManager']
modelParameters: Ref<any>
live2dIdleAnimationEnabled: Ref<boolean>
live2dForceIdleEyeAnimation: Ref<boolean>
live2dAutoBlinkEnabled: Ref<boolean>
live2dForceAutoBlinkEnabled: Ref<boolean>
lastUpdateTime: Ref<number>
@@ -53,6 +55,7 @@ export function useLive2DMotionManagerUpdate(options: UseLive2DMotionManagerUpda
motionManager,
modelParameters,
live2dIdleAnimationEnabled,
live2dForceIdleEyeAnimation,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
lastUpdateTime,
@@ -95,6 +98,7 @@ export function useLive2DMotionManagerUpdate(options: UseLive2DMotionManagerUpda
motionManager,
modelParameters,
live2dIdleAnimationEnabled,
live2dForceIdleEyeAnimation,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
isIdleMotion,
@@ -206,8 +210,8 @@ export function useMotionUpdatePluginIdleDisable(idleEyeFocus = useLive2DIdleEye
if (!ctx.live2dIdleAnimationEnabled.value && ctx.isIdleMotion) {
ctx.motionManager.stopAllMotions()
// Still update eye focus and blink even if idle motion is stopped
idleEyeFocus.update(ctx.internalModel, ctx.now)
if (ctx.live2dForceIdleEyeAnimation.value)
idleEyeFocus.update(ctx.internalModel, ctx.now)
if (ctx.internalModel.eyeBlink != null) {
ctx.internalModel.eyeBlink.updateParameters(ctx.model, ctx.timeDelta / 1000)
}
+1 -1
View File
@@ -1,3 +1,3 @@
export * from './expression-store'
export * from './live2d'
export * from './model-parameters'
export * from './view-control'
@@ -37,7 +37,7 @@ export const defaultModelParameters = {
breath: 0,
}
export const useLive2d = defineStore('live2d', () => {
export const useLive2dParams = defineStore('live2d', () => {
const { post, data } = useBroadcastChannel<BroadcastChannelEvents, BroadcastChannelEvents>({ name: 'airi-stores-stage-ui-live2d' })
const shouldUpdateViewHooks = ref(new Set<() => void>())
@@ -72,7 +72,7 @@ export function useL2dViewControl() {
return {
/** model position relative to the center of the screen, in pixels */
position,
/** model scaling in percentages. `100` means no scaling. */
/** model scaling in percentages. `1` means no scaling. */
scale,
/** reset the given control to its default value. */
set,
@@ -17,7 +17,6 @@ import type {
Texture,
WebGLRenderer,
} from 'three'
import type { Ref, WatchStopHandle } from 'vue'
import type {
VrmDisposeHookContext,
@@ -32,7 +31,7 @@ import type { ManagedVrmInstance } from './vrm-instance-cache'
import { VRMUtils } from '@pixiv/three-vrm'
import { useLoop, useTresContext } from '@tresjs/core'
import { until, useMouse } from '@vueuse/core'
import { until } from '@vueuse/core'
import {
AnimationMixer,
Box3,
@@ -40,11 +39,9 @@ import {
Mesh,
MeshPhysicalMaterial,
MeshStandardMaterial,
Plane,
Raycaster,
SRGBColorSpace,
Vector2,
Vector3,
} from 'three'
import {
@@ -60,6 +57,7 @@ import {
} from 'vue'
import { useEyeTracking } from '../../composables/eye-tracking'
import {
createIblProbeController,
injectDiffuseIBL,
@@ -121,9 +119,9 @@ const props = withDefaults(defineProps<{
modelOffset: Vec3
modelRotationY: number
lookAtTarget: Vec3
trackingMode: string
eyeHeight: number
screenBoundingBox: () => { top: number, left: number, width: number, height: number }
cameraPosition: Vec3
camera: PerspectiveCamera
@@ -141,7 +139,6 @@ const emit = defineEmits<{
(e: 'loadingProgress', value: number): void
(e: 'loadStart', value: 'initial-load' | 'model-reload' | 'model-switch'): void
(e: 'sceneBootstrap', value: SceneBootstrap): void
(e: 'lookAtTarget', value: Vec3): void
(e: 'error', value: unknown): void
(e: 'loaded', value: string): void
@@ -161,10 +158,7 @@ const {
modelOffset,
modelRotationY,
lookAtTarget,
trackingMode,
eyeHeight,
cameraPosition,
camera,
} = toRefs(props)
@@ -176,12 +170,7 @@ const vrmGroup = shallowRef<Group>()
const modelLoaded = ref<boolean>(false)
let loadSequence = 0
// for eye tracking modes
const { x: mouseX, y: mouseY } = useMouse()
const raycaster = new Raycaster()
const mouse = new Vector2()
const mouseTarget = shallowRef<Vec3>()
let stopMouseWatch: WatchStopHandle | undefined
let stopCameraWatch: WatchStopHandle | undefined
// Animation related ref
const vrmAnimationMixer = ref<AnimationMixer>()
@@ -454,7 +443,6 @@ function bindManagedVrmInstanceRenderLoop() {
})
const blinkAndSaccadeMs = measureFrameStep(tracingEnabled, () => {
blink.update(activeVrm, delta)
idleEyeSaccades.update(activeVrm, lookAtTarget, delta)
})
const emoteMs = measureFrameStep(tracingEnabled, () => {
vrmEmote.value?.update(delta)
@@ -563,40 +551,7 @@ function componentCleanUp(
}
}
// look at mouse
function lookAtMouse(
mouseX: number,
mouseY: number,
camera: Ref<PerspectiveCamera>,
): Vec3 {
mouse.x = (mouseX / window.innerWidth) * 2 - 1
mouse.y = -(mouseY / window.innerHeight) * 2 + 1
// Raycast from the mouse position
raycaster.setFromCamera(mouse, camera.value)
// Create a plane in front of the camera
const cameraDirection = new Vector3()
camera.value.getWorldDirection(cameraDirection) // Get camera's forward direction
const plane = new Plane()
plane.setFromNormalAndCoplanarPoint(
cameraDirection,
camera.value.position.clone().add(cameraDirection.multiplyScalar(1)), // 1 unit in front of the camera
)
const intersection = new Vector3()
raycaster.ray.intersectPlane(plane, intersection)
return { x: intersection.x, y: intersection.y, z: intersection.z }
}
function defaultTookAt(eyeHeight: number): Vec3 {
return {
x: 0,
y: eyeHeight,
z: -100,
}
}
const defaultTookAt = computed(() => new Vector3(0, eyeHeight.value, -100))
function computeBoundingBox(vrmScene: Object3D) {
const box = new Box3()
@@ -661,7 +616,7 @@ function buildSceneBootstrap(activeVrm: VRM, cacheHit: boolean): SceneBootstrap
cameraDistance: cameraPosition.distanceTo(modelCenter),
cameraPosition: { x: cameraPosition.x, y: cameraPosition.y, z: cameraPosition.z },
eyeHeight: eyePositionY,
lookAtTarget: defaultTookAt(eyePositionY),
lookAtTarget: defaultTookAt.value,
modelOffset: {
x: bootstrapRoot.position.x,
y: bootstrapRoot.position.y,
@@ -940,6 +895,12 @@ async function loadModel() {
}
}
const focusPos = useEyeTracking(() => ({
camera: camera.value,
raycaster,
defaultLookAt: defaultTookAt.value,
}), props.screenBoundingBox)
onMounted(async () => {
// watch if the model needs to be reloaded
// Registered BEFORE the initial load to avoid missing src changes
@@ -1023,32 +984,9 @@ onMounted(async () => {
})
airiIblProbe?.update(mode, skyBoxIntensity.value, nprIrrSH.value ?? null)
}, { immediate: true })
// update eye tracking mode
watch(trackingMode, (newMode) => {
stopCameraWatch?.()
stopCameraWatch = undefined
stopMouseWatch?.()
stopMouseWatch = undefined
if (newMode === 'camera') {
stopCameraWatch = watch(cameraPosition, (newPosition) => {
// watch to update look at target to camera
emit('lookAtTarget', newPosition)
}, { immediate: true, deep: true })
}
else if (newMode === 'mouse') {
stopMouseWatch = watch([mouseX, mouseY], ([newX, newY]) => {
mouseTarget.value = lookAtMouse(newX, newY, camera)
// watch to update look at target to mouse
emit('lookAtTarget', mouseTarget.value)
}, { immediate: true, deep: true })
}
else {
emit('lookAtTarget', defaultTookAt(eyeHeight.value))
}
watch(focusPos, (newPos) => {
idleEyeSaccades.instantUpdate(vrm.value, newPos)
}, { immediate: true })
watch(lookAtTarget, (newTarget) => {
idleEyeSaccades.instantUpdate(vrm.value, newTarget)
}, { deep: true })
})
onUnmounted(() => {
@@ -133,7 +133,8 @@ const vrmFrameRuntimeHook = shallowRef<VrmFrameRuntimeHook>()
const camera = shallowRef(new PerspectiveCamera())
const controlsRef = shallowRef<InstanceType<typeof OrbitControls>>()
const tresCanvasRef = shallowRef<TresContext>()
const tresContextRef = shallowRef<TresContext>()
const screenRef = ref<InstanceType<typeof Screen>>()
const skyBoxEnvRef = ref<InstanceType<typeof SkyBox>>()
const dirLightRef = ref<InstanceType<typeof DirectionalLight>>()
const stageThreeRuntimeTraceContext = getStageThreeRuntimeTraceContext()
@@ -276,10 +277,10 @@ function applySceneBootstrap(value: SceneBootstrap) {
}
const { readRenderTargetRegionAtClientPoint, disposeRenderTarget } = useRenderTargetRegionAtClientPoint({
getRenderer: () => tresCanvasRef.value?.renderer.instance as WebGLRenderer | undefined,
getScene: () => tresCanvasRef.value?.scene.value,
getRenderer: () => tresContextRef.value?.renderer.instance as WebGLRenderer | undefined,
getScene: () => tresContextRef.value?.scene.value,
getCamera: () => camera.value,
getCanvas: () => tresCanvasRef.value?.renderer.instance.domElement,
getCanvas: () => tresContextRef.value?.renderer.instance.domElement,
})
/*
@@ -447,7 +448,7 @@ function onVRMSceneBootstrap(value: SceneBootstrap) {
pendingSceneBootstrap.value = value
}
function onVRMModelLookAtTarget(value: Vec3) {
function onVRMModelLookAtTarget(value: Vector3) {
lookAtTarget.value.x = value.x
lookAtTarget.value.y = value.y
lookAtTarget.value.z = value.z
@@ -482,7 +483,7 @@ function onSkyBoxReady(EnvPayload: {
// === Tres Canvas ===
function onTresReady(context: TresContext) {
tresCanvasRef.value = context
tresContextRef.value = context
canvasReady.value = true
emitSceneSubtreeTrace('tresCanvasRef', 'attached')
setScenePhaseWithTrace(resolveScenePhaseAfterBinding(), 'tres:ready')
@@ -492,7 +493,7 @@ function onTresRender() {
if (!isStageThreeRuntimeTraceEnabled())
return
const renderer = tresCanvasRef.value?.renderer.instance
const renderer = tresContextRef.value?.renderer.instance
if (!renderer)
return
@@ -515,11 +516,11 @@ onMounted(() => {
onUnmounted(() => {
invalidateBindingRevision()
if (tresCanvasRef.value)
if (tresContextRef.value)
emitSceneSubtreeTrace('tresCanvasRef', 'detached')
canvasReady.value = false
tresCanvasRef.value = undefined
tresContextRef.value = undefined
activeModelSrc.value = undefined
pendingSceneBootstrap.value = undefined
resetSceneBindingTransactions()
@@ -669,6 +670,8 @@ function updateDirLightTarget(newRotation: { x: number, y: number, z: number })
directionalLightTarget.value = { x: target.x, y: target.y, z: target.z }
}
const getScreenBBox = () => screenRef.value?.containerRef?.getBoundingClientRect() ?? { top: 0, left: 0, width: 500, height: 500 }
watch(directionalLightRotation, (newRotation) => {
updateDirLightTarget(newRotation)
}, { deep: true })
@@ -685,17 +688,17 @@ defineExpose({
applyVrmFrameRuntimeHook()
},
canvasElement: () => {
return tresCanvasRef.value?.renderer.instance.domElement
return tresContextRef.value?.renderer.instance.domElement
},
camera: () => camera.value,
renderer: () => tresCanvasRef.value?.renderer.instance,
renderer: () => tresContextRef.value?.renderer.instance,
scene: () => modelRef.value?.scene,
readRenderTargetRegionAtClientPoint,
captureFrame: async () => {
if (!tresCanvasRef.value)
if (!tresContextRef.value)
return null
const { renderer, scene } = tresCanvasRef.value
const { renderer, scene } = tresContextRef.value
renderer.instance.render(scene.value, camera.value)
return new Promise<Blob | null>((resolve) => {
@@ -706,7 +709,7 @@ defineExpose({
</script>
<template>
<Screen v-slot="{ width, height }" relative>
<Screen ref="screenRef" v-slot="{ width, height }" relative>
<div top="50%" translate-y="[-50%]" fixed z-15 px-3>
<SliderControls />
</div>
@@ -779,6 +782,7 @@ defineExpose({
:eye-height="eyeHeight"
:camera-position="cameraPosition"
:camera="camera"
:screen-bounding-box="getScreenBBox"
@loading-progress="(val: number) => emit('loadModelProgress', val)"
@load-start="onVRMModelLoadStart"
@scene-bootstrap="onVRMSceneBootstrap"
@@ -0,0 +1,55 @@
import type { PerspectiveCamera, Raycaster } from 'three'
import type { MaybeRefOrGetter } from 'vue'
import { storeToRefs } from 'pinia'
import { Vector2, Vector3 } from 'three'
import { computed, toValue } from 'vue'
import { useModelStore } from '../stores/model-store'
interface ThreeWorldContext {
raycaster: Raycaster
camera: PerspectiveCamera
defaultLookAt: Vector3
}
const { trackingSource, trackingMode, cameraPosition } = storeToRefs(useModelStore())
// look at mouse
export function useEyeTracking(
context: MaybeRefOrGetter<ThreeWorldContext>,
screenBoundingBox: MaybeRefOrGetter<{ top: number, left: number, height: number, width: number }>,
) {
const focusPos = computed<Vector3>(() => {
if (trackingMode.value === 'camera')
return new Vector3(cameraPosition.value.x, cameraPosition.value.y, cameraPosition.value.z)
const ctx = toValue(context)
if (trackingMode.value === 'none' || !trackingSource.value)
return ctx.defaultLookAt
const screen = toValue(screenBoundingBox)
if (trackingMode.value === 'mouse') {
const trackingPos = trackingSource.value as unknown as { x: number, y: number }
// from tracking origin to camera center(screen center)
const castedPos = castScreenToCam(
ctx,
new Vector2(
((trackingPos.x - screen.left) / screen.width) * 2 - 1,
-((trackingPos.y - screen.top) / screen.height) * 2 + 1,
),
)
return castedPos
}
return ctx.defaultLookAt
})
return focusPos
}
function castScreenToCam(ctx: ThreeWorldContext, point: Vector2): Vector3 {
ctx.raycaster.setFromCamera(point, ctx.camera)
const nearPlaneDistance = ctx.camera.near
const direction = ctx.raycaster.ray.direction.clone().normalize().multiplyScalar(8)
const pointOnNearPlane = ctx.raycaster.ray.origin.clone()
.add(direction.multiplyScalar(nearPlaneDistance))
return pointOnNearPlane
}
@@ -1,4 +1,5 @@
import type { Vector3 } from 'three'
import type { ComputedRef, Ref } from 'vue'
import { useBroadcastChannel, useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
@@ -82,6 +83,18 @@ const vrmViewUpdateRuntimeInstanceId = Math.random().toString(36).slice(2, 10)
let vrmViewUpdateMessageSequence = 0
const { modelOffset, set: setViewControl } = useThreeViewControl()
const { cameraDistance, cameraFOV, cameraPosition } = useThreeCamera()
const modelRotationY = useLocalStorage('settings/stage-ui-three/modelRotationY', 0)
const trackingMode = useLocalStorage('settings/stage-ui-three/trackingMode', 'none' as 'camera' | 'mouse' | 'none')
/**
* A position to perform eye-tracking on.
* Should be a position relative to the application window:
* - for browser targets, it should be the top-left corner of the viewport
* - for tamagotchi targets, it should be the top-left corner of
* the application window that renders the model
*/
const trackingSource: Ref<ComputedRef<{ x: number, y: number }> | null> = ref(null)
export const useModelStore = defineStore('modelStore', () => {
const { post, data } = useBroadcastChannel<BroadcastChannelEvents, BroadcastChannelEvents>({ name: 'airi-stores-stage-ui-three-vrm' })
const shouldUpdateViewHooks = ref(new Set<() => void>())
@@ -144,11 +157,6 @@ export const useModelStore = defineStore('modelStore', () => {
const modelOrigin = useLocalStorage('settings/stage-ui-three/modelOrigin', { x: 0, y: 0, z: 0 })
const eyeHeight = useLocalStorage('settings/stage-ui-three/eyeHeight', 0)
// === User scene settings ===
// These values are intended to survive model reloads and direct edits from settings UI.
const modelRotationY = useLocalStorage('settings/stage-ui-three/modelRotationY', 0)
const trackingMode = useLocalStorage('settings/stage-ui-three/trackingMode', 'none' as 'camera' | 'mouse' | 'none')
// === View state ===
/** current runtime pose. may be recalculated when a new model bootstrap is applied. */
const lookAtTarget = useLocalStorage('settings/stage-ui-three/lookAtTarget', { x: 0, y: 0, z: 0 })
@@ -237,6 +245,7 @@ export const useModelStore = defineStore('modelStore', () => {
lookAtTarget,
trackingMode,
trackingSource,
eyeHeight,
renderScale,
multisampling,
@@ -136,6 +136,7 @@ function stopDrag() {
</div>
<div />
<label
v-if="props.xConfig !== undefined"
h-fit w-full inline-flex items-center rounded-md px="1.5" py="0.5"
:class="[isDragging === 'x' ? 'bg-red-100/50 dark:bg-red-900/50' : 'bg-neutral-100 dark:bg-neutral-900', props.disabled ? 'opacity-60' : '']"
transition="colors duration-200 ease-in-out"
@@ -149,9 +150,9 @@ function stopDrag() {
:value="xNormalized"
type="number"
:disabled="props.disabled"
:min="props.xConfig?.min"
:max="props.xConfig?.max"
:step="0.0001"
:min="props.xConfig.min"
:max="props.xConfig.max"
:step="props.xConfig.step"
max-w-4lh w-full appearance-none bg-transparent text-right font-mono outline-none
class="[&::-webkit-inner-spin-button]:(m-0 appearance-none)"
@change="(e) => handleChange('x', e)"
@@ -159,6 +160,7 @@ function stopDrag() {
</span>
</label>
<label
v-if="props.yConfig !== undefined"
h-fit inline-flex items-center rounded-md px="1.5" py="0.5"
:class="[isDragging === 'y' ? 'bg-blue-100/50 dark:bg-blue-900/50' : 'bg-neutral-100 dark:bg-neutral-900', props.disabled ? 'opacity-60' : '']"
transition="colors duration-200 ease-in-out"
@@ -172,9 +174,9 @@ function stopDrag() {
:value="yNormalized"
type="number"
:disabled="props.disabled"
:min="props.yConfig?.min"
:max="props.yConfig?.max"
:step="0.0001"
:min="props.yConfig.min"
:max="props.yConfig.max"
:step="props.yConfig.step"
max-w-4lh w-full appearance-none bg-transparent text-right font-mono outline-none
class="[&::-webkit-inner-spin-button]:(m-0 appearance-none)"
@change="(e) => handleChange('y', e)"
@@ -182,6 +184,7 @@ function stopDrag() {
</span>
</label>
<label
v-if="props.zConfig !== undefined"
h-fit inline-flex items-center rounded-md px="1.5" py="0.5"
:class="[isDragging === 'z' ? 'bg-green-100/50 dark:bg-green-900/50' : 'bg-neutral-100 dark:bg-neutral-900', props.disabled ? 'opacity-60' : '']"
transition="colors duration-200 ease-in-out"
@@ -195,9 +198,9 @@ function stopDrag() {
:value="zNormalized"
type="number"
:disabled="props.disabled"
:min="props.zConfig?.min"
:max="props.zConfig?.max"
:step="0.0001"
:min="props.zConfig.min"
:max="props.zConfig.max"
:step="props.zConfig.step"
max-w-4lh w-full appearance-none bg-transparent text-right font-mono outline-none
class="[&::-webkit-inner-spin-button]:(m-0 appearance-none)"
@change="(e) => handleChange('z', e)"
@@ -9,6 +9,7 @@ import { useFileDialog } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuTrigger, EditableArea, EditableEditTrigger, EditableInput, EditablePreview, EditableRoot, EditableSubmitTrigger } from 'reka-ui'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import Live2DReportModal from './Live2DReportModal.vue'
@@ -24,6 +25,7 @@ const emits = defineEmits<{
const displayModelStore = useDisplayModelsStore()
const { displayModelsFromIndexedDBLoading, displayModels } = storeToRefs(displayModelStore)
const { t } = useI18n()
function handleRemoveModel(model: DisplayModel) {
displayModelStore.removeDisplayModel(model.id)
@@ -146,7 +148,7 @@ spineDialog.onChange(handleAddSpineModel)
<div flex items-center>
<div w-full flex-1 text-xl>
Model Selector
{{ t('settings.model-select.select-model.title') }}
</div>
<div>
<DropdownMenuRoot>
@@ -157,7 +159,7 @@ spineDialog.onChange(handleAddSpineModel)
aria-label="Options for Display Models"
>
<div i-solar:add-circle-bold />
<div>Add</div>
<div>{{ t('settings.model-select.select-model.import') }}</div>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
@@ -256,7 +258,7 @@ spineDialog.onChange(handleAddSpineModel)
>
<button flex items-center gap-1 outline-none @click="handleRemoveModel(model)">
<div i-solar:trash-bin-minimalistic-bold-duotone />
<div>Remove</div>
<div>{{ t('settings.model-select.select-model.remove') }}</div>
</button>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -320,14 +322,14 @@ spineDialog.onChange(handleAddSpineModel)
</div>
</div>
<Button class="hidden md:block" variant="secondary" @click="handlePick(model)">
Pick
{{ t('settings.model-select.select-model.select') }}
</Button>
</div>
</div>
</div>
</div>
<Button class="block md:hidden" @click="handleMobilePick()">
Confirm
{{ t('settings.model-select.select-model.confirm') }}
</Button>
</div>
</template>
@@ -1,14 +1,14 @@
<script setup lang="ts">
import type { ModelSettingsRuntimeSnapshot } from './runtime'
import { defaultModelParameters, useExpressionStore, useLive2d } from '@proj-airi/stage-ui-live2d'
import { defaultModelParameters, useExpressionStore, useLive2dParams, useSettingsLive2d } from '@proj-airi/stage-ui-live2d'
import { OPFSCache } from '@proj-airi/stage-ui-live2d/utils/opfs-loader'
import { Button, Checkbox, FieldCheckbox, FieldCombobox, FieldRange, SelectTab } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettings } from '../../../../stores/settings'
import { PropertyPoint } from '../../../data-pane'
import { Section } from '../../../layouts'
import { ColorPalette } from '../../../widgets'
@@ -25,9 +25,10 @@ defineEmits<{
const { t } = useI18n()
const settings = useSettings()
const settings = useSettingsLive2d()
const {
live2dDisableFocus,
live2dEyeTracking,
live2dModelEyeOffset,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
@@ -35,9 +36,10 @@ const {
live2dShadowEnabled,
live2dMaxFps,
live2dRenderScale,
live2dForceIdleEyeAnimation,
} = storeToRefs(settings)
const live2d = useLive2d()
const live2d = useLive2dParams()
const {
scale,
position,
@@ -65,15 +67,25 @@ function isGroupActive(group: { parameters: { parameterId: string, value: number
const selectedRuntimeMotion = ref<string>('')
const runtimeMotions = ref<Array<{ name: string, displayPath: string, group: string, index: number }>>([])
const canExtractColors = computed(() => props.runtimeSnapshot.canCapturePreview)
const runtimeMotionOptions = computed(() => runtimeMotions.value.map(motion => ({
label: motion.name,
value: motion.displayPath,
description: motion.displayPath,
})))
const runtimeMotionOptions = computed(() => {
const options = runtimeMotions.value.map(motion => ({
label: motion.name,
value: motion.displayPath,
description: motion.displayPath,
}))
if (options.length > 0) {
options.unshift({
label: t('settings.live2d.animation.idle-motion.disable-motion'),
value: '<motion disabled>',
description: t('settings.live2d.animation.idle-motion.disable-motion-description'),
})
}
return options
})
const fpsOptions = computed(() => [
{ value: 0, label: t('settings.live2d.fps.options.unlimited') },
{ value: 60, label: '60' },
{ value: 30, label: '30' },
{ value: 60, label: '60' },
{ value: 0, label: t('settings.live2d.parameters.fps.options.unlimited') },
])
watch(() => live2d.availableMotions, (motions) => {
@@ -87,11 +99,11 @@ watch(() => live2d.availableMotions, (motions) => {
console.info('Available motions:', runtimeMotions.value)
}, { immediate: true })
const llmModeOptions = [
{ value: 'none', label: 'None' },
{ value: 'all', label: 'All' },
{ value: 'custom', label: 'Custom' },
]
const llmModeOptions = computed(() => [
{ value: 'none', label: t('settings.live2d.expressions.expose-to-llm-options.none') },
{ value: 'all', label: t('settings.live2d.expressions.expose-to-llm-options.all') },
{ value: 'custom', label: t('settings.live2d.expressions.expose-to-llm-options.custom') },
])
// Get available runtime motions from the model
onMounted(() => {
@@ -126,6 +138,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
const motion = runtimeMotions.value.find(item => item.displayPath === selectedMotionPath)
if (!motion) {
live2dIdleAnimationEnabled.value = false
return
}
@@ -245,6 +258,9 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
size="sm"
:expand="false"
>
<p text="neutral-500 dark:neutral-400">
{{ t('settings.live2d.theme-color-from-model.description') }}
</p>
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
<Button variant="secondary" :disabled="!canExtractColors" @click="$emit('extractColorsFromModel')">
{{ t('settings.live2d.theme-color-from-model.button-extract.title') }}
@@ -291,7 +307,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
</a>
</Section> -->
<Section
title="Parameters"
:title="t('settings.live2d.animation.title')"
icon="i-solar:settings-bold-duotone"
:class="[
'rounded-xl',
@@ -302,64 +318,96 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
:expand="false"
>
<FieldCheckbox
v-model="live2dDisableFocus"
:label="t('settings.live2d.focus.button-disable.title')"
v-model="live2dEyeTracking"
:label="t('settings.live2d.animation.focus.title')"
:description="t('settings.live2d.animation.focus.description')"
placement="right"
/>
<div v-if="live2dEyeTracking" class="grid grid-cols-4">
<PropertyPoint
v-model:x="live2dModelEyeOffset.x"
v-model:y="live2dModelEyeOffset.y"
:x-config="{ min: -100, max: 100, step: 0.01, label: 'X', formatValue: (val: number) => val?.toFixed(2) }"
:y-config="{ min: -100, max: 100, step: 0.01, label: 'Y', formatValue: (val: number) => val?.toFixed(2) }"
>
<template #label>
<p class="text-xs text-neutral-500 dark:text-neutral-400">
{{ t('settings.live2d.animation.focus.offset') }}
</p>
</template>
</PropertyPoint>
</div>
<FieldCheckbox
v-model="live2dForceIdleEyeAnimation"
:label="t('settings.live2d.animation.force-idle-eye-animation.title')"
:description="t('settings.live2d.animation.force-idle-eye-animation.description')"
placement="right"
/>
<FieldCheckbox
v-model="live2dAutoBlinkEnabled"
:label="t('settings.live2d.animation.auto-blink.title')"
:description="t('settings.live2d.animation.auto-blink.description')"
placement="right"
/>
<FieldCheckbox
v-model="live2dForceAutoBlinkEnabled"
:label="t('settings.live2d.animation.force-auto-blink.title')"
:description="t('settings.live2d.animation.force-auto-blink.description')"
placement="right"
/>
<FieldCombobox
v-model="selectedRuntimeMotion"
:label="t('settings.live2d.animation.idle-motion.title')"
:options="runtimeMotionOptions"
:placeholder="t('settings.live2d.animation.idle-motion.placeholder')"
:select-class="['w-full']"
:content-min-width="256"
@update:model-value="handleMotionSelect"
>
<template #empty>
{{ t('settings.live2d.animation.idle-motion.no-motion') }}
</template>
</FieldCombobox>
</Section>
<Section
:title="t('settings.live2d.parameters.title')"
icon="i-solar:settings-bold-duotone"
:class="[
'rounded-xl',
'bg-white/80 dark:bg-black/75',
'backdrop-blur-lg',
]"
size="sm"
:expand="false"
>
<FieldRange
v-model="live2dRenderScale"
as="div"
:min="0.5"
:max="2"
:step="0.25"
:label="t('settings.live2d.render-scale.title')"
:label="t('settings.live2d.parameters.render-scale.title')"
/>
<FieldCombobox
v-model="selectedRuntimeMotion"
label="Idle Animation"
:options="runtimeMotionOptions"
placeholder="Select Motion"
:select-class="['w-full']"
:content-min-width="256"
@update:model-value="handleMotionSelect"
>
<template #empty>
No motions available
</template>
</FieldCombobox>
<label class="flex flex-col gap-4">
<div :class="['flex items-center gap-2', 'flex-row']">
<div class="flex-1">
<div class="flex items-center gap-1 text-sm font-medium">
<slot name="label">
{{ t('settings.live2d.fps.title') }}
</slot>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
<slot name="description">
{{ t('settings.live2d.fps.description') }}
</slot>
</div>
<label class="flex flex-wrap gap-4">
<div class="flex-1">
<div class="flex items-center gap-1 text-sm font-medium">
<slot name="label">
{{ t('settings.live2d.parameters.fps.title') }}
</slot>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
<slot name="description">
{{ t('settings.live2d.parameters.fps.description') }}
</slot>
</div>
<SelectTab v-model="live2dMaxFps" :options="fpsOptions" size="sm" :class="['shrink-0']" />
</div>
<SelectTab v-model="live2dMaxFps" :options="fpsOptions" size="sm" :class="['shrink-0']" />
</label>
<div mt-4 flex items-center justify-between>
<span text-sm text-neutral-600 dark:text-neutral-400>Auto Blink</span>
<Checkbox v-model="live2dAutoBlinkEnabled" />
</div>
<div mt-3 flex items-center justify-between>
<span text-sm text-neutral-600 dark:text-neutral-400>Force Auto Blink (fallback timer)</span>
<Checkbox v-model="live2dForceAutoBlinkEnabled" />
</div>
<div mt-4 flex items-center justify-between>
<span text-sm text-neutral-600 dark:text-neutral-400>Shadow</span>
<span text-sm>{{ t('settings.live2d.parameters.shadow') }}</span>
<Checkbox v-model="live2dShadowEnabled" />
</div>
@@ -368,7 +416,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
class="mt-4 w-full"
@click="resetToDefaultParameters"
>
Reset To Default Parameters
{{ t('settings.live2d.parameters.reset-parameters') }}
</Button>
<Button
@@ -378,7 +426,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
:loading="clearingCache"
@click="clearModelCache"
>
Clear Model Cache
{{ t('settings.live2d.clear-model-cache') }}
</Button>
<!-- Head Rotation -->
@@ -632,7 +680,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
</FieldRange>
</Section>
<Section
title="Expressions"
:title="t('settings.live2d.expressions.title')"
icon="i-solar:face-scan-circle-bold-duotone"
:class="[
'rounded-xl',
@@ -643,15 +691,15 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
:expand="false"
>
<div flex items-center justify-between>
<span text-sm text-neutral-600 dark:text-neutral-400>Expression System</span>
<span text-sm text-neutral-600 dark:text-neutral-400>{{ t('settings.live2d.expressions.override-toggle') }}</span>
<Checkbox v-model="live2dExpressionEnabled" />
</div>
<div v-if="!live2dExpressionEnabled" py-2 text-xs text-neutral-500 dark:text-neutral-400>
SDK expression manager and eye blink are preserved when disabled.
{{ t('settings.live2d.expressions.sdk-preset-preserved-notice') }}
</div>
<template v-else-if="expressionGroups.size === 0">
<div py-2 text-sm text-neutral-500 dark:text-neutral-400>
No expressions available for this model.
{{ t('settings.live2d.expressions.no-expression') }}
</div>
</template>
<template v-else>
@@ -670,8 +718,8 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
</div>
</div>
<div mt-4 flex items-center gap-3>
<span whitespace-nowrap text-sm text-neutral-600 dark:text-neutral-400>Expose to LLM</span>
<div mt-4 flex flex-wrap items-center gap-3>
<span whitespace-nowrap text-sm text-neutral-600 dark:text-neutral-400>{{ t('settings.live2d.expressions.expose-to-llm-toggle') }}</span>
<SelectTab
:model-value="expressionStore.llmMode"
:options="llmModeOptions"
@@ -680,7 +728,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
/>
</div>
<span v-if="expressionStore.llmMode !== 'none'" text-xs text-neutral-500 dark:text-neutral-400>
LLM tool integration is not yet wired up.
{{ t('settings.live2d.expressions.llm-integration-wip') }}
</span>
<!-- Custom per-expression LLM toggles (only when mode = 'custom') -->
@@ -701,10 +749,10 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
<!-- Action buttons -->
<div mt-4 flex gap-2>
<Button variant="secondary" @click="expressionStore.saveDefaults()">
Save Expression Defaults
{{ t('settings.live2d.expressions.save-default') }}
</Button>
<Button variant="secondary" @click="expressionStore.resetAll()">
Reset All Expressions
{{ t('settings.live2d.expressions.reset') }}
</Button>
</div>
</template>
@@ -5,6 +5,7 @@ import type { ModelSettingsRuntimeSnapshot } from './runtime'
import { Button, Callout } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Godot from './godot.vue'
import Live2D from './live2d.vue'
@@ -28,6 +29,7 @@ defineEmits<{
(e: 'extractColorsFromModel'): void
}>()
const { t } = useI18n()
const modelSelectorOpen = ref(false)
const settingsStore = useSettings()
const airiCardStore = useAiriCardStore()
@@ -57,21 +59,26 @@ async function handleModelPick(selectedModel: DisplayModel | undefined) {
...settingsClassList,
]"
>
<Callout label="We support both 2D and 3D models">
<Callout :label="t('settings.model-select.panel-callout.support-status-header')">
<i18n-t keypath="settings.model-select.panel-callout.support-status" tag="p">
<template #select-button>
<strong>{{ t('settings.model-select.select-model.button') }}</strong>
</template>
<template #zip>
<code>.zip</code>
</template>
<template #vrm>
<code>.vrm</code>
</template>
</i18n-t>
<p>
Click <strong>Select Model</strong> to import different formats of
models into catalog, currently, <code>.zip</code> (Live2D, Spine) and <code>.vrm</code> (VRM) are supported.
</p>
<p>
Neuro-sama uses 2D model driven by Live2D Inc. developed framework.
Grok Ani uses 3D model that is driven by VRM / MMD open formats.
Spine 2D models are supported via Esoteric Software's Spine runtime.
{{ t('settings.model-select.panel-callout.model-type-example') }}
</p>
</Callout>
<div :class="['flex flex-wrap items-center gap-2']">
<ModelSelectorDialog v-model:show="modelSelectorOpen" :selected-model="currentSelectedDisplayModel" @pick="handleModelPick">
<Button variant="secondary">
Select Model
{{ t('settings.model-select.select-model.button') }}
</Button>
</ModelSelectorDialog>
<slot name="actions" />
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ModelSettingsRuntimeSnapshot } from './runtime'
import { Live2DScene } from '@proj-airi/stage-ui-live2d'
import { Live2DScene, useSettingsLive2d } from '@proj-airi/stage-ui-live2d'
import { SpineScene } from '@proj-airi/stage-ui-spine'
import { ThreeScene, useModelStore } from '@proj-airi/stage-ui-three'
import { useMouse } from '@vueuse/core'
@@ -24,7 +24,6 @@ const emit = defineEmits<{
(e: 'runtimeSnapshotChanged', value: ModelSettingsRuntimeSnapshot): void
}>()
const positionCursor = useMouse()
const settingsStore = useSettings()
const modelStore = useModelStore()
const live2dSceneRef = ref<{ canvasElement: () => HTMLCanvasElement | undefined }>()
@@ -37,18 +36,14 @@ const vrmPreviewStageInstanceId = `model-settings-preview-stage:${Math.random().
provide('previewStage', true)
const {
live2dDisableFocus,
stageModelSelected,
stageModelSelectedUrl,
stageModelRenderer,
themeColorsHue,
themeColorsHueDynamic,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dShadowEnabled,
live2dMaxFps,
live2dRenderScale,
} = storeToRefs(settingsStore)
const {
spinePremultipliedAlpha,
spineDefaultMixDuration,
spineIdleAnimationEnabled,
@@ -156,6 +151,18 @@ watch(runtimeSnapshot, snapshot => emit('runtimeSnapshotChanged', snapshot), { i
defineExpose({
capturePreviewFrame,
})
const { live2dEyeTrackingSource } = storeToRefs(useSettingsLive2d())
const { x: mouseX, y: mouseY } = useMouse()
live2dEyeTrackingSource.value = computed(() => ({
x: mouseX.value,
y: mouseY.value,
}))
const { trackingSource } = storeToRefs(useModelStore())
trackingSource.value = computed(() => ({
x: mouseX.value,
y: mouseY.value,
}))
</script>
<template>
@@ -164,18 +171,10 @@ defineExpose({
<Live2DScene
ref="live2dSceneRef"
v-model:state="live2dComponentState"
:focus-at="{ x: positionCursor.x.value, y: positionCursor.y.value }"
:model-src="stageModelSelectedUrl"
:model-id="stageModelSelected"
:disable-focus-at="live2dDisableFocus"
:theme-colors-hue="themeColorsHue"
:theme-colors-hue-dynamic="themeColorsHueDynamic"
:live2d-idle-animation-enabled="live2dIdleAnimationEnabled"
:live2d-auto-blink-enabled="live2dAutoBlinkEnabled"
:live2d-force-auto-blink-enabled="live2dForceAutoBlinkEnabled"
:live2d-shadow-enabled="live2dShadowEnabled"
:live2d-max-fps="live2dMaxFps"
:live2d-render-scale="live2dRenderScale"
/>
</div>
</template>
@@ -11,7 +11,7 @@ import { sleep } from '@moeru/std'
import { createLive2DLipSync } from '@proj-airi/model-driver-lipsync'
import { wlipsyncProfile } from '@proj-airi/model-driver-lipsync/shared/wlipsync'
import { createPlaybackManager, createSpeechPipeline, normalizeActPayload } from '@proj-airi/pipelines-audio'
import { Live2DScene, useLive2d } from '@proj-airi/stage-ui-live2d'
import { Live2DScene, useLive2dParams } from '@proj-airi/stage-ui-live2d'
import { SpineScene } from '@proj-airi/stage-ui-spine'
import { ThreeScene } from '@proj-airi/stage-ui-three'
import { animations } from '@proj-airi/stage-ui-three/assets/vrm'
@@ -25,6 +25,7 @@ import { generateSpeech } from '@xsai/generate-speech'
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useSettingsLive2d } from '../../../../stage-ui-live2d/src/composables/live2d/live2d'
import { useAuthProviderSync } from '../../composables/use-auth-provider-sync'
import { useDuckDb } from '../../composables/use-duck-db'
import { useIOTraceBridge } from '../../composables/use-io-trace-bridge'
@@ -45,7 +46,6 @@ import { useSpeechRuntimeStore } from '../../stores/speech-runtime'
const props = withDefaults(defineProps<{
paused?: boolean
focusAt: { x: number, y: number }
}>(), { paused: false, scale: 1 })
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
@@ -61,18 +61,18 @@ const settingsStore = useSettings()
const {
stageModelRenderer,
stageViewControlsEnabled,
live2dDisableFocus,
stageModelSelectedUrl,
stageModelSelected,
themeColorsHue,
themeColorsHueDynamic,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dExpressionEnabled,
} = storeToRefs(settingsStore)
const {
live2dShadowEnabled,
live2dMaxFps,
live2dRenderScale,
} = storeToRefs(useSettingsLive2d())
const {
spinePremultipliedAlpha,
spineDefaultMixDuration,
spineIdleAnimationEnabled,
@@ -91,7 +91,7 @@ const chatHookCleanups: Array<() => void> = []
const providersStore = useProvidersStore()
useAuthProviderSync()
const live2dStore = useLive2d()
const live2dStore = useLive2dParams()
const showStage = ref(true)
const viewUpdateCleanups: Array<() => void> = []
@@ -129,7 +129,7 @@ const speechRuntimeStore = useSpeechRuntimeStore()
const backgroundStore = useBackgroundStore()
const { activeBackgroundUrl } = storeToRefs(backgroundStore)
const { currentMotion } = storeToRefs(useLive2d())
const { currentMotion } = storeToRefs(useLive2dParams())
const emotionsQueue = createQueue<EmotionPayload>({
handlers: [
@@ -867,17 +867,11 @@ defineExpose({
h-full w-full flex-1
:model-src="stageModelSelectedUrl"
:model-id="stageModelSelected"
:focus-at="focusAt"
:mouth-open-size="mouthOpenSize"
:now-speaking="nowSpeaking"
:paused="paused"
:disable-focus-at="live2dDisableFocus"
:theme-colors-hue="themeColorsHue"
:theme-colors-hue-dynamic="themeColorsHueDynamic"
:live2d-idle-animation-enabled="live2dIdleAnimationEnabled"
:live2d-auto-blink-enabled="live2dAutoBlinkEnabled"
:live2d-force-auto-blink-enabled="live2dForceAutoBlinkEnabled"
:live2d-expression-enabled="live2dExpressionEnabled"
:live2d-shadow-enabled="live2dShadowEnabled"
:live2d-max-fps="live2dMaxFps"
:live2d-render-scale="live2dRenderScale"
@@ -1,7 +1,7 @@
import type { ChatSessionsExport } from '../types/chat-session'
import { isStageTamagotchi } from '@proj-airi/stage-shared'
import { useLive2d } from '@proj-airi/stage-ui-live2d'
import { useLive2dParams, useSettingsLive2d } from '@proj-airi/stage-ui-live2d'
import { useModelStore } from '@proj-airi/stage-ui-three'
import { useChatOrchestratorStore } from '../stores/chat'
@@ -27,7 +27,8 @@ export function useDataMaintenance() {
const providersStore = useProvidersStore()
const settingsStore = useSettings()
const audioSettingsStore = useSettingsAudioDevice()
const live2dStore = useLive2d()
const live2dParamsStore = useLive2dParams()
const live2dSettingsStore = useSettingsLive2d()
const threeStore = useModelStore()
const hearingStore = useHearingStore()
const speechStore = useSpeechStore()
@@ -85,7 +86,8 @@ export function useDataMaintenance() {
async function resetSettingsState() {
await settingsStore.resetState()
audioSettingsStore.resetState()
live2dStore.resetState()
live2dParamsStore.resetState()
live2dSettingsStore.resetState()
threeStore.resetModelStore()
mcpStore.resetState()
onboardingStore.resetSetupState()
@@ -4,7 +4,6 @@ import { useSettingsAnalytics } from './analytics'
import { useSettingsControlsIsland } from './controls-island'
import { useSettingsDeveloper } from './developer'
import { useSettingsGeneral } from './general'
import { useSettingsLive2d } from './live2d'
import { useSettingsSpine } from './spine'
import { useSettingsStageModel } from './stage-model'
import { useSettingsTheme } from './theme'
@@ -16,7 +15,6 @@ export * from './beat-sync'
export * from './controls-island'
export * from './developer'
export * from './general'
export * from './live2d'
export * from './spine'
export * from './stage-model'
export * from './theme'
@@ -34,7 +32,6 @@ export const useSettings = defineStore('settings', () => {
const general = useSettingsGeneral()
const analytics = useSettingsAnalytics()
const stageModel = useSettingsStageModel()
const live2d = useSettingsLive2d()
const spine = useSettingsSpine()
const theme = useSettingsTheme()
const controlsIsland = useSettingsControlsIsland()
@@ -44,7 +41,6 @@ export const useSettings = defineStore('settings', () => {
await stageModel.resetState()
analytics.resetState()
general.resetState()
live2d.resetState()
spine.resetState()
theme.resetState()
controlsIsland.resetState()
@@ -55,7 +51,6 @@ export const useSettings = defineStore('settings', () => {
const generalRefs = storeToRefs(general)
const analyticsRefs = storeToRefs(analytics)
const stageModelRefs = storeToRefs(stageModel)
const live2dRefs = storeToRefs(live2d)
const spineRefs = storeToRefs(spine)
const themeRefs = storeToRefs(theme)
const controlsIslandRefs = storeToRefs(controlsIsland)
@@ -76,16 +71,6 @@ export const useSettings = defineStore('settings', () => {
stageModelSelectedDisplayModel: stageModelRefs.stageModelSelectedDisplayModel,
stageViewControlsEnabled: stageModelRefs.stageViewControlsEnabled,
// Live2D settings
live2dDisableFocus: live2dRefs.live2dDisableFocus,
live2dIdleAnimationEnabled: live2dRefs.live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled: live2dRefs.live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled: live2dRefs.live2dForceAutoBlinkEnabled,
live2dExpressionEnabled: live2dRefs.live2dExpressionEnabled,
live2dShadowEnabled: live2dRefs.live2dShadowEnabled,
live2dMaxFps: live2dRefs.live2dMaxFps,
live2dRenderScale: live2dRefs.live2dRenderScale,
// Spine settings
spinePremultipliedAlpha: spineRefs.spinePremultipliedAlpha,
spineDefaultMixDuration: spineRefs.spineDefaultMixDuration,
@@ -1,54 +0,0 @@
import { useLocalStorageManualReset, useVersionedLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
export const useSettingsLive2d = defineStore('settings-live2d', () => {
const live2dDisableFocus = useLocalStorageManualReset<boolean>('settings/live2d/disable-focus', false)
const live2dIdleAnimationEnabled = useLocalStorageManualReset<boolean>('settings/live2d/idle-animation-enabled', true)
const live2dAutoBlinkEnabled = useVersionedLocalStorageManualReset<boolean>('settings/live2d/auto-blink-enabled', false, {
defaultVersion: '2.0.0',
satisfiesVersionBy(beforeVersion, afterVersion) {
if (beforeVersion === afterVersion) {
return true
}
return false
},
})
const live2dForceAutoBlinkEnabled = useVersionedLocalStorageManualReset<boolean>('settings/live2d/force-auto-blink-enabled', true, {
defaultVersion: '2.0.0',
satisfiesVersionBy(beforeVersion, afterVersion) {
if (beforeVersion === afterVersion) {
return true
}
return false
},
})
const live2dExpressionEnabled = useLocalStorageManualReset<boolean>('settings/live2d/expression-enabled', false)
const live2dShadowEnabled = useLocalStorageManualReset<boolean>('settings/live2d/shadow-enabled', true)
const live2dMaxFps = useLocalStorageManualReset<number>('settings/live2d/max-fps', 0)
const live2dRenderScale = useLocalStorageManualReset<number>('settings/live2d/render-scale', 2)
function resetState() {
live2dDisableFocus.reset()
live2dIdleAnimationEnabled.reset()
live2dAutoBlinkEnabled.reset()
live2dForceAutoBlinkEnabled.reset()
live2dExpressionEnabled.reset()
live2dShadowEnabled.reset()
live2dMaxFps.reset()
live2dRenderScale.reset()
}
return {
live2dDisableFocus,
live2dIdleAnimationEnabled,
live2dAutoBlinkEnabled,
live2dForceAutoBlinkEnabled,
live2dExpressionEnabled,
live2dShadowEnabled,
live2dMaxFps,
live2dRenderScale,
resetState,
}
})
@@ -63,6 +63,10 @@ onMounted(async () => {
containerElementBounding.update()
})
defineExpose({
containerRef,
})
</script>
<template>
+239 -24
View File
@@ -1477,7 +1477,7 @@ importers:
version: 3.0.2(electron@41.2.1)
'@electron-toolkit/tsconfig':
specifier: ^2.0.0
version: 2.0.0(@types/node@25.6.0)
version: 2.0.0(@types/node@24.12.2)
'@electron-toolkit/utils':
specifier: ^4.0.0
version: 4.0.0(electron@41.2.1)
@@ -1516,7 +1516,7 @@ importers:
version: 3.1.0
'@intlify/unplugin-vue-i18n':
specifier: ^11.0.7
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@modelcontextprotocol/sdk':
specifier: 'catalog:'
version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
@@ -1552,10 +1552,10 @@ importers:
version: link:../../packages/ui-transitions
'@proj-airi/unplugin-fetch':
specifier: 'catalog:'
version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@proj-airi/unplugin-live2d-sdk':
specifier: ^0.1.7
version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@types/audioworklet':
specifier: 'catalog:'
version: 0.0.97
@@ -1582,7 +1582,7 @@ importers:
version: 2.10.3
'@vitejs/plugin-vue':
specifier: ^6.0.6
version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
'@vue-macros/volar':
specifier: ^3.1.2
version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
@@ -1615,7 +1615,7 @@ importers:
version: 6.8.3
electron-vite:
specifier: ^5.0.0
version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
get-port-please:
specifier: 'catalog:'
version: 3.2.0
@@ -1636,31 +1636,31 @@ importers:
version: 2.2.6
unocss-preset-scrollbar:
specifier: ^4.0.0
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
unplugin-info:
specifier: ^1.3.2
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unplugin-yaml:
specifier: ^4.1.0
version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite:
specifier: 'catalog:'
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-bundle-visualizer:
specifier: ^1.2.1
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
vite-plugin-mkcert:
specifier: 'catalog:'
version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-plugin-vue-devtools:
specifier: ^8.1.1
version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
vite-plugin-vue-layouts:
specifier: ^0.11.0
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
vue-macros:
specifier: ^3.1.2
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
vue-tsc:
specifier: ^3.2.6
version: 3.2.6(typescript@5.9.3)
@@ -1736,6 +1736,9 @@ importers:
'@proj-airi/stage-ui':
specifier: workspace:^
version: link:../../packages/stage-ui
'@proj-airi/stage-ui-live2d':
specifier: workspace:^
version: link:../../packages/stage-ui-live2d
'@proj-airi/stage-ui-three':
specifier: workspace:^
version: link:../../packages/stage-ui-three
@@ -19957,9 +19960,9 @@ snapshots:
dependencies:
electron: 41.2.1
'@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)':
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
dependencies:
'@types/node': 25.6.0
'@types/node': 24.12.2
'@electron-toolkit/utils@4.0.0(electron@41.2.1)':
dependencies:
@@ -20871,6 +20874,31 @@ snapshots:
- supports-color
- typescript
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
'@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))
'@intlify/shared': 11.3.2
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
'@typescript-eslint/scope-manager': 8.58.1
'@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3)
debug: 4.4.3(supports-color@10.2.2)
fast-glob: 3.3.3
pathe: 2.0.3
picocolors: 1.1.1
unplugin: 2.3.11
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.32(typescript@5.9.3)
optionalDependencies:
vue-i18n: 11.3.2(vue@3.5.32(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.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
@@ -22852,11 +22880,35 @@ snapshots:
transitivePeerDependencies:
- magicast
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
ofetch: 1.5.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
ofetch: 1.5.1
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
dependencies:
ofetch: 1.5.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
yauzl: 3.3.0
transitivePeerDependencies:
- '@types/node'
- '@vitejs/devtools'
- esbuild
- jiti
- less
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- yaml
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
dependencies:
ofetch: 1.5.1
@@ -23757,6 +23809,7 @@ snapshots:
'@types/node@25.6.0':
dependencies:
undici-types: 7.19.2
optional: true
'@types/nprogress@0.2.3': {}
@@ -24422,6 +24475,12 @@ snapshots:
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.32(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.13
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.32(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.13
@@ -24724,6 +24783,15 @@ snapshots:
transitivePeerDependencies:
- vue
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
sirv: 3.0.2
vue: 3.5.32(typescript@5.9.3)
optionalDependencies:
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- typescript
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
sirv: 3.0.2
@@ -26865,7 +26933,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
@@ -26873,7 +26941,7 @@ snapshots:
esbuild: 0.25.12
magic-string: 0.30.21
picocolors: 1.1.1
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
@@ -32789,7 +32857,8 @@ snapshots:
undici-types@7.16.0: {}
undici-types@7.19.2: {}
undici-types@7.19.2:
optional: true
undici@6.24.1: {}
@@ -32901,11 +32970,6 @@ snapshots:
'@unocss/preset-mini': 66.6.8
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))):
dependencies:
'@unocss/preset-mini': 66.6.8
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@unocss/cli': 66.6.8
@@ -33002,6 +33066,14 @@ snapshots:
unplugin: 2.3.11
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
esbuild: 0.27.2
rolldown: 1.0.0-rc.16
rollup: 4.60.1
unplugin: 2.3.11
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
esbuild: 0.27.2
@@ -33036,6 +33108,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ci-info: 4.4.0
git-url-parse: 16.1.0
simple-git: 3.36.0
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.27.2
rollup: 4.60.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ci-info: 4.4.0
@@ -33145,6 +33230,17 @@ snapshots:
rollup: 4.60.1
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
unplugin: 3.0.0
yaml: 2.8.3
optionalDependencies:
esbuild: 0.27.2
rolldown: 1.0.0-rc.16
rollup: 4.60.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin@2.3.11:
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -33376,12 +33472,22 @@ snapshots:
- rollup
- supports-color
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
birpc: 2.9.0
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
birpc: 2.9.0
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
@@ -33427,6 +33533,21 @@ snapshots:
- tsx
- yaml
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ansis: 4.2.0
debug: 4.4.3(supports-color@10.2.2)
error-stack-parser-es: 1.0.5
ohash: 2.0.11
open: 10.2.0
perfect-debounce: 2.1.0
sirv: 3.0.2
unplugin-utils: 0.3.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies:
- supports-color
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ansis: 4.2.0
@@ -33458,6 +33579,13 @@ snapshots:
- typescript
- ws
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
supports-color: 10.2.2
undici: 8.1.0
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
@@ -33476,6 +33604,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
'@vue/devtools-kit': 8.1.1
'@vue/devtools-shared': 8.1.1
sirv: 3.0.2
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies:
- '@nuxt/kit'
- supports-color
- vue
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
@@ -33490,6 +33632,21 @@ snapshots:
- supports-color
- vue
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0)
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0)
'@vue/compiler-dom': 3.5.32
kolorist: 1.8.0
magic-string: 0.30.21
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
@@ -33505,6 +33662,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
fast-glob: 3.3.3
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.32(typescript@5.9.3)
vue-router: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
transitivePeerDependencies:
- supports-color
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
@@ -33771,6 +33938,54 @@ snapshots:
- vue-tsc
- webpack
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
'@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3)
'@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
unplugin: 2.3.11
unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3))
vue: 3.5.32(typescript@5.9.3)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
- '@rspack/core'
- '@vueuse/core'
- esbuild
- rolldown
- rollup
- typescript
- vite
- vue-tsc
- webpack
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))