feat(stage-*): allow setting character offset in landscape (#1696)
This commit is contained in:
+36
-21
@@ -1,42 +1,57 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { supportedControl as threeSupportedControl, useThreeViewControl } from '@proj-airi/stage-ui-three'
|
||||
import { supportedControl as l2dSupportedCtrl, useL2dViewControl } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { useSettingsStageModel } from '@proj-airi/stage-ui/stores/settings/stage-model'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'reset'): void
|
||||
}>()
|
||||
const { stageModelRenderer } = storeToRefs(useSettingsStageModel())
|
||||
const { viewControlsEnabled: l2dViewCtrlEnabled, viewControlMode: l2dCtrlMode, reset: l2dReset } = useL2dViewControl()
|
||||
const { viewControlsEnabled: threePlainCtrlEnabled, viewControlMode: threeCtrlMode, reset: threeReset } = useThreeViewControl()
|
||||
const controlEnabled = computed(() => {
|
||||
if (stageModelRenderer.value === 'live2d')
|
||||
return { enabled: l2dViewCtrlEnabled, mode: l2dCtrlMode, supported: l2dSupportedCtrl, reset: l2dReset }
|
||||
if (stageModelRenderer.value === 'vrm')
|
||||
return { enabled: threePlainCtrlEnabled, mode: threeCtrlMode, supported: threeSupportedControl, reset: threeReset }
|
||||
return null
|
||||
})
|
||||
|
||||
const { stageModelRenderer, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
|
||||
const mode = defineModel<'x' | 'y' | 'z' | 'scale'>({ required: true })
|
||||
|
||||
function handleViewControlsToggle(targetMode: 'x' | 'y' | 'z' | 'scale') {
|
||||
if (mode.value === targetMode) {
|
||||
emits('reset')
|
||||
function handleViewControlsToggle(targetMode: string) {
|
||||
if (!controlEnabled.value || !controlEnabled.value.supported.includes(targetMode as any))
|
||||
return
|
||||
if (controlEnabled.value.mode.value === targetMode) {
|
||||
controlEnabled.value.reset(controlEnabled.value.mode.value as any)
|
||||
return
|
||||
}
|
||||
|
||||
mode.value = targetMode
|
||||
controlEnabled.value.mode.value = targetMode as any
|
||||
}
|
||||
|
||||
// watch()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div w-full flex flex-1 items-center self-end justify-end gap-2>
|
||||
<Transition name="fade">
|
||||
<div v-if="stageViewControlsEnabled" w-full flex justify-between gap-2>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'x'" w-full @click="handleViewControlsToggle('x')">
|
||||
<div v-if="controlEnabled?.enabled.value" w-full flex justify-between gap-2>
|
||||
<Button variant="secondary-muted" :toggled="controlEnabled.mode.value === 'x'" w-full @click="handleViewControlsToggle('x')">
|
||||
X
|
||||
</Button>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'y'" w-full @click="handleViewControlsToggle('y')">
|
||||
<Button variant="secondary-muted" :toggled="controlEnabled.mode.value === 'y'" w-full @click="handleViewControlsToggle('y')">
|
||||
Y
|
||||
</Button>
|
||||
<Button v-if="stageModelRenderer === 'vrm'" variant="secondary-muted" :toggled="mode === 'z'" w-full @click="handleViewControlsToggle('z')">
|
||||
<Button v-if="controlEnabled.supported.includes('z' as any)" variant="secondary-muted" :toggled="controlEnabled.mode.value === 'z'" w-full @click="handleViewControlsToggle('z')">
|
||||
Z
|
||||
</Button>
|
||||
<Button variant="secondary-muted" :toggled="mode === 'scale'" w-full @click="handleViewControlsToggle('scale')">
|
||||
<Button v-if="controlEnabled.supported.includes('cameraFOV' as any)" variant="secondary-muted" :toggled="controlEnabled.mode.value === 'cameraFOV'" w-full @click="handleViewControlsToggle('cameraFOV')">
|
||||
FOV
|
||||
</Button>
|
||||
<Button v-if="controlEnabled.supported.includes('scale' as any)" variant="secondary-muted" :toggled="controlEnabled.mode.value === 'scale'" w-full @click="handleViewControlsToggle('scale')">
|
||||
Scale
|
||||
</Button>
|
||||
<Button v-if="controlEnabled.supported.includes('cameraDistance' as any)" variant="secondary-muted" :toggled="controlEnabled.mode.value === 'cameraDistance'" w-full @click="handleViewControlsToggle('cameraDistance')">
|
||||
Dis
|
||||
</Button>
|
||||
</div>
|
||||
</Transition>
|
||||
<button
|
||||
@@ -45,11 +60,11 @@ function handleViewControlsToggle(targetMode: 'x' | 'y' | 'z' | 'scale') {
|
||||
bg="neutral-50/70 dark:neutral-800/70"
|
||||
title="View"
|
||||
text="neutral-500 dark:neutral-400"
|
||||
@click="stageViewControlsEnabled = !stageViewControlsEnabled"
|
||||
@click="controlEnabled && (controlEnabled.enabled.value = !controlEnabled.enabled.value)"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div v-if="!stageViewControlsEnabled" i-solar:tuning-outline size-5 />
|
||||
<div v-else i-solar:alt-arrow-right-outline size-5 />
|
||||
<div v-if="controlEnabled?.enabled.value" i-solar:alt-arrow-right-outline size-5 />
|
||||
<div v-else i-solar:tuning-outline size-5 />
|
||||
</Transition>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat'
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
|
||||
import { useThreeViewControl } from '@proj-airi/stage-ui-three'
|
||||
import { ChatHistory, HearingConfigDialog } from '@proj-airi/stage-ui/components'
|
||||
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
@@ -9,20 +10,20 @@ import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useChatMaintenanceStore } from '@proj-airi/stage-ui/stores/chat/maintenance'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { useChatStreamStore } from '@proj-airi/stage-ui/stores/chat/stream-store'
|
||||
import { useL2dViewControl } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { BasicTextarea, useTheme } from '@proj-airi/ui'
|
||||
import { useResizeObserver, useScreenSafeArea } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import ViewControls from '../Layouts/InteractiveArea/Actions/ViewControls.vue'
|
||||
import IndicatorMicVolume from '../Widgets/IndicatorMicVolume.vue'
|
||||
import ActionAbout from './InteractiveArea/Actions/About.vue'
|
||||
import ActionViewControls from './InteractiveArea/Actions/ViewControls.vue'
|
||||
import ViewControlInputs from './ViewControls/Inputs.vue'
|
||||
|
||||
import { BackgroundDialogPicker } from '../Backgrounds'
|
||||
|
||||
@@ -41,9 +42,6 @@ function handleDeleteMessage(index: number) {
|
||||
messages.value = messages.value.filter((_, messageIndex) => messageIndex !== index)
|
||||
}
|
||||
|
||||
const viewControlsActiveMode = ref<'x' | 'y' | 'z' | 'scale'>('scale')
|
||||
const viewControlsInputsRef = useTemplateRef<InstanceType<typeof ViewControlInputs>>('viewControlsInputs')
|
||||
|
||||
const messageInput = ref('')
|
||||
const isComposing = ref(false)
|
||||
const backgroundDialogOpen = ref(false)
|
||||
@@ -53,7 +51,9 @@ const providersStore = useProvidersStore()
|
||||
const { activeProvider, activeModel } = storeToRefs(useConsciousnessStore())
|
||||
|
||||
useResizeObserver(document.documentElement, () => screenSafeArea.update())
|
||||
const { themeColorsHueDynamic, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
const { viewControlsEnabled: l2dViewCtrlEnabled } = useL2dViewControl()
|
||||
const { viewControlsEnabled: threeViewCtrlEnabled } = useThreeViewControl()
|
||||
const settingsAudioDevice = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(settingsAudioDevice)
|
||||
const { ingest, onAfterMessageComposed } = chatOrchestrator
|
||||
@@ -149,7 +149,7 @@ onMounted(() => {
|
||||
<KeepAlive>
|
||||
<Transition name="fade">
|
||||
<ChatHistory
|
||||
v-if="!stageViewControlsEnabled"
|
||||
v-if="!threeViewCtrlEnabled && !l2dViewCtrlEnabled"
|
||||
variant="mobile"
|
||||
:messages="historyMessages"
|
||||
:sending="sending"
|
||||
@@ -165,9 +165,6 @@ onMounted(() => {
|
||||
</Transition>
|
||||
</KeepAlive>
|
||||
<div relative w-full self-end>
|
||||
<div top="50%" translate-y="[-50%]" fixed z-15 px-3>
|
||||
<ViewControlInputs ref="viewControlsInputs" :mode="viewControlsActiveMode" />
|
||||
</div>
|
||||
<div translate-y="[-100%]" absolute left-0 px-3 pb-3 font-sans>
|
||||
<div flex="~ col" gap-1>
|
||||
<slot name="status" />
|
||||
@@ -223,7 +220,7 @@ onMounted(() => {
|
||||
>
|
||||
<div class="i-solar:trash-bin-2-bold-duotone" />
|
||||
</button>
|
||||
<ActionViewControls v-model="viewControlsActiveMode" @reset="() => viewControlsInputsRef?.resetOnMode()" />
|
||||
<ViewControls />
|
||||
</div>
|
||||
</div>
|
||||
<div bg="white dark:neutral-800" max-h-100dvh max-w-100dvw w-full flex gap-1 overflow-auto px-3 pt-2 :style="{ paddingBottom: `${Math.max(Number.parseFloat(screenSafeArea.bottom.value.replace('px', '')), 12)}px` }">
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useModelStore } from '@proj-airi/stage-ui-three'
|
||||
import { useLive2d } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { RoundRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'x' | 'y' | 'z' | 'scale'
|
||||
}>()
|
||||
|
||||
const { stageModelRenderer, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const { modelOffset: vrmPosition, modelSize: vrmModelSize, cameraDistance: vrmCameraDistance, sceneMutationLocked: vrmSceneMutationLocked } = storeToRefs(useModelStore())
|
||||
const { scale: live2dScale, position: live2dPosition } = storeToRefs(useLive2d())
|
||||
|
||||
const vrmControlsDisabled = computed(() => {
|
||||
return stageModelRenderer.value === 'vrm' && vrmSceneMutationLocked.value
|
||||
})
|
||||
|
||||
const viewControlsValueX = computed({
|
||||
get: () => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
return live2dPosition.value.x
|
||||
case 'vrm':
|
||||
return vrmPosition.value.x * 100
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
live2dPosition.value.x = value
|
||||
break
|
||||
case 'vrm':
|
||||
if (vrmControlsDisabled.value)
|
||||
break
|
||||
vrmPosition.value.x = value / 100
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueXMin = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? -1000 : (-vrmModelSize.value.x - 10) * 10
|
||||
})
|
||||
|
||||
const viewControlsValueXMax = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? 1000 : (vrmModelSize.value.x + 10) * 10
|
||||
})
|
||||
|
||||
const viewControlsValueY = computed({
|
||||
get: () => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
return -live2dPosition.value.y
|
||||
case 'vrm':
|
||||
return vrmPosition.value.y * 100
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
live2dPosition.value.y = -value
|
||||
break
|
||||
case 'vrm':
|
||||
if (vrmControlsDisabled.value)
|
||||
break
|
||||
vrmPosition.value.y = value / 100
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueYMin = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? -1000 : (-vrmModelSize.value.y - 10) * 10
|
||||
})
|
||||
|
||||
const viewControlsValueYMax = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? 1000 : (vrmModelSize.value.y + 10) * 10
|
||||
})
|
||||
|
||||
const viewControlsValueZ = computed({
|
||||
get: () => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
return 0
|
||||
case 'vrm':
|
||||
return vrmPosition.value.z
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
switch (stageModelRenderer.value) {
|
||||
case 'live2d':
|
||||
break
|
||||
case 'vrm':
|
||||
if (vrmControlsDisabled.value)
|
||||
break
|
||||
vrmPosition.value.z = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const viewControlsValueZMin = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? -500 : -vrmModelSize.value.z - 10
|
||||
})
|
||||
|
||||
const viewControlsValueZMax = computed(() => {
|
||||
return stageModelRenderer.value === 'live2d' ? 500 : vrmModelSize.value.z + 10
|
||||
})
|
||||
|
||||
const viewControlsValueScale = computed({
|
||||
get: () => {
|
||||
if (stageModelRenderer.value === 'live2d') {
|
||||
return live2dScale.value
|
||||
}
|
||||
|
||||
return vrmCameraDistance.value
|
||||
},
|
||||
set: (value) => {
|
||||
if (stageModelRenderer.value === 'live2d') {
|
||||
live2dScale.value = value
|
||||
}
|
||||
else if (!vrmControlsDisabled.value) {
|
||||
vrmCameraDistance.value = value
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function resetOnMode() {
|
||||
switch (props.mode) {
|
||||
case 'x':
|
||||
viewControlsValueX.value = 0
|
||||
break
|
||||
case 'y':
|
||||
viewControlsValueY.value = 0
|
||||
break
|
||||
case 'z':
|
||||
viewControlsValueZ.value = 0
|
||||
break
|
||||
case 'scale':
|
||||
viewControlsValueScale.value = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetOnMode,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade-side-pops-in">
|
||||
<div v-if="stageViewControlsEnabled" :class="vrmControlsDisabled ? ['opacity-60', 'pointer-events-none'] : []">
|
||||
<Transition name="fade-side-pops-in" mode="out-in">
|
||||
<div v-if="props.mode === 'x'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueX" :min="viewControlsValueXMin" :max="viewControlsValueXMax" :step="0.01" :disabled="vrmControlsDisabled" data-direction="vertical" h="50%" write-vertical-left />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueX.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.mode === 'y'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueY" :min="viewControlsValueYMin" :max="viewControlsValueYMax" :step="0.01" :disabled="vrmControlsDisabled" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueY.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="stageModelRenderer === 'vrm' && props.mode === 'z'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueZ" :min="viewControlsValueZMin" :max="viewControlsValueZMax" :step="0.01" :disabled="vrmControlsDisabled" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueZ.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.mode === 'scale'" relative class="[&_.round-range-tooltip]:hover:opacity-100">
|
||||
<RoundRange v-model="viewControlsValueScale" :min="0" :max="3" :step="0.0001" :disabled="vrmControlsDisabled" write-vertical-left h="50%" data-direction="vertical" />
|
||||
<div class="round-range-tooltip" top="50%" translate-y="[-50%]" absolute left-10 font-mono op-0 transition="all duration-200 ease-in-out">
|
||||
{{ viewControlsValueScale.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-side-pops-in-enter-active,
|
||||
.fade-side-pops-in-leave-active {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-side-pops-in-enter-from,
|
||||
.fade-side-pops-in-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
}
|
||||
|
||||
.fade-side-pops-in-enter-to,
|
||||
.fade-side-pops-in-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,8 @@ import { useChatMaintenanceStore } from '@proj-airi/stage-ui/stores/chat/mainten
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ViewControls from '../Layouts/InteractiveArea/Actions/ViewControls.vue'
|
||||
|
||||
import { BackgroundDialogPicker } from '../Backgrounds'
|
||||
|
||||
const { cleanupMessages } = useChatMaintenanceStore()
|
||||
@@ -14,6 +16,7 @@ const backgroundDialogOpen = ref(false)
|
||||
<template>
|
||||
<BackgroundDialogPicker v-model="backgroundDialogOpen" />
|
||||
<div absolute bottom--8 right-0 flex gap-2>
|
||||
<ViewControls />
|
||||
<button
|
||||
class="max-h-[10lh] min-h-[1lh]"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
|
||||
Reference in New Issue
Block a user