refactor(tamagotchi/stage-*): remove duplicated scene in the setting page for tamagotchi (#1460)
* refactor(tamagotchi): remove setting window duplicate scene * refactor(tamagotchi): some changes * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
autofix-ci[bot]
parent
11bb6b8e0a
commit
9ae795a831
@@ -208,6 +208,7 @@ export default defineConfig({
|
||||
...base,
|
||||
'**/settings/connection/index.vue',
|
||||
'**/settings/data/index.vue',
|
||||
'**/settings/models/index.vue',
|
||||
'**/settings/system/general.vue',
|
||||
'**/settings/modules/mcp.vue',
|
||||
],
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type {
|
||||
ModelSettingsRuntimeSnapshot,
|
||||
} from '@proj-airi/stage-ui/components/scenarios/settings/model-settings/runtime'
|
||||
|
||||
import type { ModelSettingsRuntimeChannelEvent } from '../../shared/model-settings-runtime'
|
||||
|
||||
import {
|
||||
createEmptyModelSettingsRuntimeSnapshot,
|
||||
} from '@proj-airi/stage-ui/components/scenarios/settings/model-settings/runtime'
|
||||
import { useBroadcastChannel } from '@vueuse/core'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
modelSettingsRuntimeSnapshotChannelName,
|
||||
} from '../../shared/model-settings-runtime'
|
||||
|
||||
export function useModelSettingsRuntimeSnapshot() {
|
||||
const runtimeSnapshot = ref<ModelSettingsRuntimeSnapshot>(createEmptyModelSettingsRuntimeSnapshot())
|
||||
const { data, post } = useBroadcastChannel<ModelSettingsRuntimeChannelEvent, ModelSettingsRuntimeChannelEvent>({
|
||||
name: modelSettingsRuntimeSnapshotChannelName,
|
||||
})
|
||||
|
||||
const requestCurrent = () => {
|
||||
post({ type: 'request-current' })
|
||||
}
|
||||
|
||||
const syncFromOwner = () => {
|
||||
requestCurrent()
|
||||
}
|
||||
const syncFromOwnerWhenVisible = () => {
|
||||
if (document.visibilityState === 'visible')
|
||||
requestCurrent()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
requestCurrent()
|
||||
window.addEventListener('focus', syncFromOwner)
|
||||
document.addEventListener('visibilitychange', syncFromOwnerWhenVisible)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('focus', syncFromOwner)
|
||||
document.removeEventListener('visibilitychange', syncFromOwnerWhenVisible)
|
||||
})
|
||||
|
||||
watch(data, (event) => {
|
||||
if (!event)
|
||||
return
|
||||
|
||||
if (event.type === 'snapshot') {
|
||||
runtimeSnapshot.value = event.snapshot
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'owner-gone') {
|
||||
if (runtimeSnapshot.value.ownerInstanceId !== event.ownerInstanceId)
|
||||
return
|
||||
|
||||
runtimeSnapshot.value = createEmptyModelSettingsRuntimeSnapshot()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
runtimeSnapshot,
|
||||
requestCurrent,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelSettingsRuntimeSnapshot } from '@proj-airi/stage-ui/components/scenarios/settings/model-settings/runtime'
|
||||
import type { ChatProvider } from '@xsai-ext/providers/utils'
|
||||
|
||||
import type { ModelSettingsRuntimeChannelEvent } from '../../shared/model-settings-runtime'
|
||||
|
||||
import workletUrl from '@proj-airi/stage-ui/workers/vad/process.worklet?worker&url'
|
||||
|
||||
import { electron } from '@proj-airi/electron-eventa'
|
||||
@@ -11,7 +14,11 @@ import {
|
||||
useElectronMouseInWindow,
|
||||
useElectronRelativeMouse,
|
||||
} from '@proj-airi/electron-vueuse'
|
||||
import { useThreeSceneIsTransparentAtPoint } from '@proj-airi/stage-ui-three'
|
||||
import { useModelStore, useThreeSceneIsTransparentAtPoint } from '@proj-airi/stage-ui-three'
|
||||
import {
|
||||
createEmptyModelSettingsRuntimeSnapshot,
|
||||
resolveComponentStateToRuntimePhase,
|
||||
} from '@proj-airi/stage-ui/components/scenarios/settings/model-settings/runtime'
|
||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
||||
import { useAudioRecorder } from '@proj-airi/stage-ui/composables/audio/audio-recorder'
|
||||
import { useCanvasPixelIsTransparentAtPoint } from '@proj-airi/stage-ui/composables/canvas-alpha'
|
||||
@@ -32,6 +39,9 @@ import ResourceStatusIsland from '../components/stage-islands/resource-status-is
|
||||
import StatusIsland from '../components/stage-islands/status-island/index.vue'
|
||||
|
||||
import { electronOpenOnboarding } from '../../shared/eventa'
|
||||
import {
|
||||
modelSettingsRuntimeSnapshotChannelName,
|
||||
} from '../../shared/model-settings-runtime'
|
||||
import { useControlsIslandStore } from '../stores/controls-island'
|
||||
import { useStageWindowLifecycleStore } from '../stores/stage-window-lifecycle'
|
||||
import { useWindowStore } from '../stores/window'
|
||||
@@ -42,8 +52,8 @@ const statusIslandRef = ref<InstanceType<typeof StatusIsland>>()
|
||||
const widgetStageRef = ref<InstanceType<typeof WidgetStage>>()
|
||||
const stageCanvas = toRef(() => widgetStageRef.value?.canvasElement())
|
||||
const componentStateStage = ref<'pending' | 'loading' | 'mounted'>('pending')
|
||||
|
||||
const isLoading = ref(true)
|
||||
const stageMounted = computed(() => componentStateStage.value === 'mounted')
|
||||
const isLoading = computed(() => !stageMounted.value)
|
||||
|
||||
const isIgnoringMouseEvents = ref(false)
|
||||
const shouldFadeOnCursorWithin = ref(false)
|
||||
@@ -73,9 +83,19 @@ const isTransparentByThree = useThreeSceneIsTransparentAtPoint(
|
||||
{ regionRadius: 25 },
|
||||
)
|
||||
|
||||
const { stageModelRenderer } = storeToRefs(useSettings())
|
||||
const settingsStore = useSettings()
|
||||
const { stageModelRenderer, stageModelSelectedUrl } = storeToRefs(settingsStore)
|
||||
const modelStore = useModelStore()
|
||||
const { sceneMutationLocked, scenePhase } = storeToRefs(modelStore)
|
||||
const { stagePaused } = storeToRefs(useStageWindowLifecycleStore())
|
||||
const { fadeOnHoverEnabled } = storeToRefs(useControlsIslandStore())
|
||||
const modelSettingsRuntimeOwnerInstanceId = `tamagotchi-main-stage:${Math.random().toString(36).slice(2, 10)}`
|
||||
const {
|
||||
data: modelSettingsRuntimeChannelEvent,
|
||||
post: postModelSettingsRuntimeChannelEvent,
|
||||
} = useBroadcastChannel<ModelSettingsRuntimeChannelEvent, ModelSettingsRuntimeChannelEvent>({
|
||||
name: modelSettingsRuntimeSnapshotChannelName,
|
||||
})
|
||||
const shouldUseThreeTransparencyHitTest = computed(() => shouldSampleStageTransparency({
|
||||
componentState: componentStateStage.value,
|
||||
fadeOnHoverEnabled: fadeOnHoverEnabled.value,
|
||||
@@ -104,14 +124,49 @@ const live2dStore = useLive2d()
|
||||
const { scale, positionInPercentageString } = storeToRefs(live2dStore)
|
||||
const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
|
||||
|
||||
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
|
||||
|
||||
const { pause, resume } = watch(isTransparent, (transparent) => {
|
||||
shouldFadeOnCursorWithin.value = fadeOnHoverEnabled.value && !transparent
|
||||
}, { immediate: true })
|
||||
|
||||
const hearingDialogOpen = computed(() => controlsIslandRef.value?.hearingDialogOpen ?? false)
|
||||
|
||||
const modelSettingsRuntimeSnapshot = computed<ModelSettingsRuntimeSnapshot>(() => {
|
||||
const hasModel = !!stageModelSelectedUrl.value
|
||||
|
||||
if (stageModelRenderer.value === 'live2d') {
|
||||
const phase = resolveComponentStateToRuntimePhase(componentStateStage.value, { hasModel })
|
||||
|
||||
return createEmptyModelSettingsRuntimeSnapshot({
|
||||
ownerInstanceId: modelSettingsRuntimeOwnerInstanceId,
|
||||
renderer: 'live2d',
|
||||
phase,
|
||||
controlsLocked: hasModel ? phase !== 'mounted' : false,
|
||||
previewAvailable: hasModel,
|
||||
canCapturePreview: false,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
if (stageModelRenderer.value === 'vrm') {
|
||||
return createEmptyModelSettingsRuntimeSnapshot({
|
||||
ownerInstanceId: modelSettingsRuntimeOwnerInstanceId,
|
||||
renderer: 'vrm',
|
||||
phase: hasModel ? scenePhase.value : 'no-model',
|
||||
controlsLocked: hasModel
|
||||
? (!stageMounted.value || sceneMutationLocked.value)
|
||||
: false,
|
||||
previewAvailable: hasModel,
|
||||
canCapturePreview: false,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
return createEmptyModelSettingsRuntimeSnapshot({
|
||||
ownerInstanceId: modelSettingsRuntimeOwnerInstanceId,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
})
|
||||
|
||||
watch([isOutsideFor250Ms, isOutsideStatusIslandFor250Ms, isAroundWindowBorderFor250Ms, isOutsideWindow, isTransparent, hearingDialogOpen, fadeOnHoverEnabled, stagePaused], () => {
|
||||
if (stagePaused.value) {
|
||||
isIgnoringMouseEvents.value = false
|
||||
@@ -152,6 +207,16 @@ watch([isOutsideFor250Ms, isOutsideStatusIslandFor250Ms, isAroundWindowBorderFor
|
||||
pause()
|
||||
}
|
||||
})
|
||||
// Emit runtime snapshot on change and on request from settings panel
|
||||
watch(modelSettingsRuntimeSnapshot, (snapshot) => {
|
||||
postModelSettingsRuntimeChannelEvent({ type: 'snapshot', snapshot })
|
||||
}, { immediate: true })
|
||||
watch(modelSettingsRuntimeChannelEvent, (event) => {
|
||||
if (event?.type !== 'request-current')
|
||||
return
|
||||
|
||||
postModelSettingsRuntimeChannelEvent({ type: 'snapshot', snapshot: modelSettingsRuntimeSnapshot.value })
|
||||
})
|
||||
|
||||
const settingsAudioDeviceStore = useSettingsAudioDevice()
|
||||
const { stream, enabled } = storeToRefs(settingsAudioDeviceStore)
|
||||
@@ -333,6 +398,10 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
postModelSettingsRuntimeChannelEvent({
|
||||
type: 'owner-gone',
|
||||
ownerInstanceId: modelSettingsRuntimeOwnerInstanceId,
|
||||
})
|
||||
stopAudioInteraction()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { ModelSettingsPanel } from '@proj-airi/stage-ui/components/scenarios/settings/model-settings'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useModelSettingsRuntimeSnapshot } from '../../../composables/model-settings-runtime-snapshot'
|
||||
|
||||
const palette = ref<string[]>([])
|
||||
const { runtimeSnapshot } = useModelSettingsRuntimeSnapshot()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['relative', 'h-full', 'flex', 'justify-center']">
|
||||
<ModelSettingsPanel
|
||||
:allow-extract-colors="false"
|
||||
:palette="palette"
|
||||
:runtime-snapshot="runtimeSnapshot"
|
||||
:settings-class="[
|
||||
'w-full',
|
||||
'max-w-6xl',
|
||||
'h-fit',
|
||||
'sm:max-h-[80dvh]',
|
||||
'overflow-y-scroll',
|
||||
'relative',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
:class="[
|
||||
'fixed',
|
||||
'right--5 top-[calc(100dvh-15rem)] bottom-0 z--1',
|
||||
'pointer-events-none flex size-60 items-center justify-center',
|
||||
'text-neutral-200/50 dark:text-neutral-600/20',
|
||||
]"
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 15 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 0 }"
|
||||
:duration="500"
|
||||
>
|
||||
<div class="i-solar:people-nearby-bold-duotone text-60" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
titleKey: settings.pages.models.title
|
||||
subtitleKey: settings.title
|
||||
descriptionKey: settings.pages.models.description
|
||||
icon: i-solar:people-nearby-bold-duotone
|
||||
settingsEntry: true
|
||||
order: 4
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ModelSettingsRuntimeSnapshot } from '@proj-airi/stage-ui/components/scenarios/settings/model-settings/runtime'
|
||||
|
||||
export const modelSettingsRuntimeSnapshotChannelName = 'airi-model-settings-runtime-snapshot'
|
||||
|
||||
export type ModelSettingsRuntimeChannelEvent
|
||||
= | { type: 'request-current' }
|
||||
| { type: 'snapshot', snapshot: ModelSettingsRuntimeSnapshot }
|
||||
| { type: 'owner-gone', ownerInstanceId: string }
|
||||
@@ -91,15 +91,16 @@ export function resolveCapRunArgs(capArgs: string[], env: NodeJS.ProcessEnv = pr
|
||||
|
||||
const [platformArg, ...rest] = capArgs
|
||||
const platform = parseCapacitorPlatform(platformArg)
|
||||
let target: string | undefined;
|
||||
let target: string | undefined
|
||||
if (platform === 'ios') {
|
||||
target = env.CAPACITOR_DEVICE_ID_IOS;
|
||||
} else if (platform === 'android') {
|
||||
target = env.CAPACITOR_DEVICE_ID_ANDROID;
|
||||
target = env.CAPACITOR_DEVICE_ID_IOS
|
||||
}
|
||||
else if (platform === 'android') {
|
||||
target = env.CAPACITOR_DEVICE_ID_ANDROID
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return capArgs;
|
||||
return capArgs
|
||||
}
|
||||
|
||||
return [platformArg, '--target', target, ...rest]
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { Live2DCanvas } from '@proj-airi/stage-ui/components/scenes'
|
||||
|
||||
import { ModelSettings } from '@proj-airi/stage-ui/components/scenarios/settings/model-settings'
|
||||
import { Vibrant } from 'node-vibrant/browser'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
|
||||
const modelSettingsRef = ref<{ capturePreviewFrame: () => Promise<Blob | undefined> }>()
|
||||
const palette = ref<string[]>([])
|
||||
|
||||
async function extractColorsFromModel() {
|
||||
if (!live2dCanvasRef.value)
|
||||
return
|
||||
|
||||
const frame = await live2dCanvasRef.value.captureFrame()
|
||||
const frame = await modelSettingsRef.value?.capturePreviewFrame()
|
||||
if (!frame) {
|
||||
console.error('No frame captured')
|
||||
return
|
||||
@@ -35,6 +29,7 @@ async function extractColorsFromModel() {
|
||||
<template>
|
||||
<div flex class="relative h-full flex-col-reverse md:flex-row">
|
||||
<ModelSettings
|
||||
ref="modelSettingsRef"
|
||||
settings-class="w-100% md:w-40% lg:w-40% xl:w-25% 2xl:w-30% h-fit sm:max-h-80dvh overflow-y-scroll relative"
|
||||
live-2d-scene-class="absolute max-h-[calc(100dvh-100px-56px)] w-full h-full"
|
||||
vrm-scene-class="absolute max-h-[calc(100dvh-100px-56px)] w-full h-full"
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { default as ModelSettings } from './index.vue'
|
||||
export { default as ModelSettingsPanel } from './panel.vue'
|
||||
export { default as ModelSettingsPreviewStage } from './preview-stage.vue'
|
||||
|
||||
@@ -1,122 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import type { DisplayModel } from '../../../../stores/display-models'
|
||||
import type { ModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
import { Live2DScene, useLive2d } from '@proj-airi/stage-ui-live2d'
|
||||
import { ThreeScene } from '@proj-airi/stage-ui-three'
|
||||
import { Button, Callout } from '@proj-airi/ui'
|
||||
import { useMouse } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Live2D from './live2d.vue'
|
||||
import VRM from './vrm.vue'
|
||||
import ModelSettingsPanel from './panel.vue'
|
||||
import ModelSettingsPreviewStage from './preview-stage.vue'
|
||||
|
||||
import { DisplayModelFormat } from '../../../../stores/display-models'
|
||||
import { useSettings } from '../../../../stores/settings'
|
||||
import { ModelSelectorDialog } from '../../dialogs/model-selector'
|
||||
import { createEmptyModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
const props = defineProps<{
|
||||
withDefaults(defineProps<{
|
||||
palette: string[]
|
||||
settingsClass?: string | string[]
|
||||
|
||||
allowExtractColors?: boolean
|
||||
live2dSceneClass?: string | string[]
|
||||
vrmSceneClass?: string | string[]
|
||||
}>()
|
||||
}>(), {
|
||||
allowExtractColors: true,
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(e: 'extractColorsFromModel'): void
|
||||
}>()
|
||||
|
||||
const modelSelectorOpen = ref(false)
|
||||
const positionCursor = useMouse()
|
||||
const settingsStore = useSettings()
|
||||
const { scale: live2dScale } = storeToRefs(useLive2d())
|
||||
const {
|
||||
live2dDisableFocus,
|
||||
stageModelSelectedUrl,
|
||||
stageModelSelected,
|
||||
stageModelSelectedDisplayModel,
|
||||
stageModelRenderer,
|
||||
themeColorsHue,
|
||||
themeColorsHueDynamic,
|
||||
live2dIdleAnimationEnabled,
|
||||
live2dAutoBlinkEnabled,
|
||||
live2dForceAutoBlinkEnabled,
|
||||
live2dShadowEnabled,
|
||||
live2dMaxFps,
|
||||
} = storeToRefs(settingsStore)
|
||||
const previewStageRef = ref<{ capturePreviewFrame: () => Promise<Blob | undefined> }>()
|
||||
const runtimeSnapshot = ref<ModelSettingsRuntimeSnapshot>(createEmptyModelSettingsRuntimeSnapshot())
|
||||
|
||||
const currentSelectedDisplayModel = computed<DisplayModel | undefined>(() => stageModelSelectedDisplayModel.value)
|
||||
|
||||
async function handleModelPick(selectedModel: DisplayModel | undefined) {
|
||||
stageModelSelected.value = selectedModel?.id ?? ''
|
||||
await settingsStore.updateStageModel()
|
||||
|
||||
if (selectedModel?.format === DisplayModelFormat.Live2dZip)
|
||||
useLive2d().shouldUpdateView()
|
||||
async function capturePreviewFrame() {
|
||||
return previewStageRef.value?.capturePreviewFrame()
|
||||
}
|
||||
|
||||
function handleRuntimeSnapshotChanged(nextSnapshot: ModelSettingsRuntimeSnapshot) {
|
||||
runtimeSnapshot.value = nextSnapshot
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
capturePreviewFrame,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
flex="~ col gap-2" z-10 overflow-y-scroll p-2 :class="[
|
||||
...(props.settingsClass
|
||||
? (typeof props.settingsClass === 'string' ? [props.settingsClass] : props.settingsClass)
|
||||
: []),
|
||||
]"
|
||||
>
|
||||
<Callout label="We support both 2D and 3D models">
|
||||
<p>
|
||||
Click <strong>Select Model</strong> to import different formats of
|
||||
models into catalog, currently, <code>.zip</code> (Live2D) and <code>.vrm</code> (VRM) are supported.
|
||||
</p>
|
||||
<p>
|
||||
Neuro-sama uses 2D model driven by Live2D Inc. developed framework.
|
||||
While Grok Ani (first female character announced in Grok Companion)
|
||||
uses 3D model that is driven by VRM / MMD open formats.
|
||||
</p>
|
||||
</Callout>
|
||||
<div :class="['flex flex-wrap gap-2']">
|
||||
<ModelSelectorDialog v-model:show="modelSelectorOpen" :selected-model="currentSelectedDisplayModel" @pick="handleModelPick">
|
||||
<Button variant="secondary">
|
||||
Select Model
|
||||
</Button>
|
||||
</ModelSelectorDialog>
|
||||
</div>
|
||||
<Live2D
|
||||
v-if="stageModelRenderer === 'live2d'"
|
||||
:palette="palette"
|
||||
@extract-colors-from-model="$emit('extractColorsFromModel')"
|
||||
/>
|
||||
<VRM
|
||||
v-if="stageModelRenderer === 'vrm'"
|
||||
:palette="palette"
|
||||
@extract-colors-from-model="$emit('extractColorsFromModel')"
|
||||
/>
|
||||
</div>
|
||||
<!-- Live2D component for 2D stage view -->
|
||||
<template v-if="stageModelRenderer === 'live2d'">
|
||||
<div :class="[...(props.live2dSceneClass ? (typeof props.live2dSceneClass === 'string' ? [props.live2dSceneClass] : props.live2dSceneClass) : [])]">
|
||||
<Live2DScene
|
||||
:focus-at="{ x: positionCursor.x.value, y: positionCursor.y.value }"
|
||||
:model-src="stageModelSelectedUrl"
|
||||
:model-id="stageModelSelected"
|
||||
:disable-focus-at="live2dDisableFocus"
|
||||
:scale="live2dScale"
|
||||
: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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- VRM component for 3D stage view -->
|
||||
<template v-if="stageModelRenderer === 'vrm'">
|
||||
<div :class="[...(props.vrmSceneClass ? (typeof props.vrmSceneClass === 'string' ? [props.vrmSceneClass] : props.vrmSceneClass) : [])]">
|
||||
<ThreeScene :model-src="stageModelSelectedUrl" />
|
||||
</div>
|
||||
</template>
|
||||
<ModelSettingsPanel
|
||||
:allow-extract-colors="allowExtractColors"
|
||||
:palette="palette"
|
||||
:runtime-snapshot="runtimeSnapshot"
|
||||
:settings-class="settingsClass"
|
||||
@extract-colors-from-model="$emit('extractColorsFromModel')"
|
||||
/>
|
||||
<ModelSettingsPreviewStage
|
||||
ref="previewStageRef"
|
||||
:live2d-scene-class="live2dSceneClass"
|
||||
:vrm-scene-class="vrmSceneClass"
|
||||
@runtime-snapshot-changed="handleRuntimeSnapshotChanged"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
import { defaultModelParameters, useLive2d } from '@proj-airi/stage-ui-live2d'
|
||||
import { OPFSCache } from '@proj-airi/stage-ui-live2d/utils/opfs-loader'
|
||||
import { Button, Checkbox, FieldRange, SelectTab } from '@proj-airi/ui'
|
||||
@@ -10,9 +12,13 @@ import { useSettings } from '../../../../stores/settings'
|
||||
import { Section } from '../../../layouts'
|
||||
import { ColorPalette } from '../../../widgets'
|
||||
|
||||
defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
palette: string[]
|
||||
}>()
|
||||
allowExtractColors?: boolean
|
||||
runtimeSnapshot: ModelSettingsRuntimeSnapshot
|
||||
}>(), {
|
||||
allowExtractColors: true,
|
||||
})
|
||||
defineEmits<{
|
||||
(e: 'extractColorsFromModel'): void
|
||||
}>()
|
||||
@@ -41,6 +47,7 @@ const selectedRuntimeMotion = ref<string>('')
|
||||
const selectedRuntimeMotionName = ref<string>('')
|
||||
const runtimeMotions = ref<Array<{ name: string, fullPath: string, displayPath: string, group: string, index: number }>>([])
|
||||
const showMotionSelector = ref(false)
|
||||
const canExtractColors = computed(() => props.runtimeSnapshot.canCapturePreview)
|
||||
const fpsOptions = computed(() => [
|
||||
{ value: 0, label: t('settings.live2d.fps.options.unlimited') },
|
||||
{ value: 60, label: '60' },
|
||||
@@ -221,6 +228,7 @@ onUnmounted(() => {
|
||||
</FieldRange>
|
||||
</Section>
|
||||
<Section
|
||||
v-if="allowExtractColors"
|
||||
:title="t('settings.live2d.theme-color-from-model.title')"
|
||||
icon="i-solar:magic-stick-3-bold-duotone"
|
||||
inner-class="text-sm"
|
||||
@@ -233,7 +241,7 @@ onUnmounted(() => {
|
||||
:expand="false"
|
||||
>
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" @click="$emit('extractColorsFromModel')">
|
||||
<Button variant="secondary" :disabled="!canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
{{ t('settings.live2d.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import type { DisplayModel } from '../../../../stores/display-models'
|
||||
import type { ModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
import { useLive2d } from '@proj-airi/stage-ui-live2d'
|
||||
import { Button, Callout } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import Live2D from './live2d.vue'
|
||||
import VRM from './vrm.vue'
|
||||
|
||||
import { DisplayModelFormat } from '../../../../stores/display-models'
|
||||
import { useSettings } from '../../../../stores/settings'
|
||||
import { ModelSelectorDialog } from '../../dialogs/model-selector'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
palette: string[]
|
||||
settingsClass?: string | string[]
|
||||
allowExtractColors?: boolean
|
||||
runtimeSnapshot: ModelSettingsRuntimeSnapshot
|
||||
}>(), {
|
||||
allowExtractColors: true,
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(e: 'extractColorsFromModel'): void
|
||||
}>()
|
||||
|
||||
const modelSelectorOpen = ref(false)
|
||||
const settingsStore = useSettings()
|
||||
const { stageModelSelected, stageModelSelectedDisplayModel } = storeToRefs(settingsStore)
|
||||
|
||||
const currentSelectedDisplayModel = computed<DisplayModel | undefined>(() => stageModelSelectedDisplayModel.value)
|
||||
const effectiveRenderer = computed(() => props.runtimeSnapshot.renderer)
|
||||
const settingsClassList = computed(() => {
|
||||
if (!props.settingsClass)
|
||||
return []
|
||||
|
||||
return typeof props.settingsClass === 'string' ? [props.settingsClass] : props.settingsClass
|
||||
})
|
||||
|
||||
async function handleModelPick(selectedModel: DisplayModel | undefined) {
|
||||
stageModelSelected.value = selectedModel?.id ?? ''
|
||||
await settingsStore.updateStageModel()
|
||||
|
||||
if (selectedModel?.format === DisplayModelFormat.Live2dZip)
|
||||
useLive2d().shouldUpdateView()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'flex flex-col gap-2',
|
||||
'z-10 overflow-y-scroll p-2',
|
||||
...settingsClassList,
|
||||
]"
|
||||
>
|
||||
<Callout label="We support both 2D and 3D models">
|
||||
<p>
|
||||
Click <strong>Select Model</strong> to import different formats of
|
||||
models into catalog, currently, <code>.zip</code> (Live2D) and <code>.vrm</code> (VRM) are supported.
|
||||
</p>
|
||||
<p>
|
||||
Neuro-sama uses 2D model driven by Live2D Inc. developed framework.
|
||||
While Grok Ani (first female character announced in Grok Companion)
|
||||
uses 3D model that is driven by VRM / MMD open formats.
|
||||
</p>
|
||||
</Callout>
|
||||
<div :class="['flex flex-wrap gap-2']">
|
||||
<ModelSelectorDialog v-model:show="modelSelectorOpen" :selected-model="currentSelectedDisplayModel" @pick="handleModelPick">
|
||||
<Button variant="secondary">
|
||||
Select Model
|
||||
</Button>
|
||||
</ModelSelectorDialog>
|
||||
</div>
|
||||
<Live2D
|
||||
v-if="effectiveRenderer === 'live2d'"
|
||||
:allow-extract-colors="allowExtractColors"
|
||||
:palette="palette"
|
||||
:runtime-snapshot="runtimeSnapshot"
|
||||
@extract-colors-from-model="$emit('extractColorsFromModel')"
|
||||
/>
|
||||
<VRM
|
||||
v-if="effectiveRenderer === 'vrm'"
|
||||
:allow-extract-colors="allowExtractColors"
|
||||
:palette="palette"
|
||||
:runtime-snapshot="runtimeSnapshot"
|
||||
@extract-colors-from-model="$emit('extractColorsFromModel')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
import { Live2DScene, useLive2d } from '@proj-airi/stage-ui-live2d'
|
||||
import { ThreeScene, useModelStore } from '@proj-airi/stage-ui-three'
|
||||
import { useMouse } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useSettings } from '../../../../stores/settings'
|
||||
import {
|
||||
createEmptyModelSettingsRuntimeSnapshot,
|
||||
resolveComponentStateToRuntimePhase,
|
||||
} from './runtime'
|
||||
|
||||
const props = defineProps<{
|
||||
live2dSceneClass?: string | string[]
|
||||
vrmSceneClass?: string | string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'runtimeSnapshotChanged', value: ModelSettingsRuntimeSnapshot): void
|
||||
}>()
|
||||
|
||||
const positionCursor = useMouse()
|
||||
const settingsStore = useSettings()
|
||||
const live2dStore = useLive2d()
|
||||
const modelStore = useModelStore()
|
||||
const live2dSceneRef = ref<{ canvasElement: () => HTMLCanvasElement | undefined }>()
|
||||
const vrmSceneRef = ref<{ canvasElement: () => HTMLCanvasElement | undefined }>()
|
||||
const live2dComponentState = ref<'pending' | 'loading' | 'mounted'>('pending')
|
||||
const vrmPreviewStageInstanceId = `model-settings-preview-stage:${Math.random().toString(36).slice(2, 10)}`
|
||||
|
||||
const {
|
||||
live2dDisableFocus,
|
||||
stageModelSelected,
|
||||
stageModelSelectedUrl,
|
||||
stageModelRenderer,
|
||||
themeColorsHue,
|
||||
themeColorsHueDynamic,
|
||||
live2dIdleAnimationEnabled,
|
||||
live2dAutoBlinkEnabled,
|
||||
live2dForceAutoBlinkEnabled,
|
||||
live2dShadowEnabled,
|
||||
live2dMaxFps,
|
||||
} = storeToRefs(settingsStore)
|
||||
const { scale: live2dScale } = storeToRefs(live2dStore)
|
||||
const { sceneMutationLocked, scenePhase } = storeToRefs(modelStore)
|
||||
|
||||
const live2dSceneClassList = computed(() => normalizeClassList(props.live2dSceneClass))
|
||||
const vrmSceneClassList = computed(() => normalizeClassList(props.vrmSceneClass))
|
||||
|
||||
function normalizeClassList(value?: string | string[]) {
|
||||
if (!value)
|
||||
return []
|
||||
|
||||
return typeof value === 'string' ? [value] : value
|
||||
}
|
||||
|
||||
function captureCanvasFrame(canvas?: HTMLCanvasElement) {
|
||||
return new Promise<Blob | undefined>((resolve) => {
|
||||
if (!canvas)
|
||||
return resolve(undefined)
|
||||
|
||||
canvas.toBlob(blob => resolve(blob ?? undefined))
|
||||
})
|
||||
}
|
||||
|
||||
async function capturePreviewFrame() {
|
||||
if (stageModelRenderer.value === 'live2d')
|
||||
return captureCanvasFrame(live2dSceneRef.value?.canvasElement())
|
||||
|
||||
if (stageModelRenderer.value === 'vrm')
|
||||
return captureCanvasFrame(vrmSceneRef.value?.canvasElement())
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const runtimeSnapshot = computed<ModelSettingsRuntimeSnapshot>(() => {
|
||||
const hasModel = !!stageModelSelectedUrl.value
|
||||
|
||||
if (stageModelRenderer.value === 'live2d') {
|
||||
const phase = resolveComponentStateToRuntimePhase(live2dComponentState.value, { hasModel })
|
||||
|
||||
return createEmptyModelSettingsRuntimeSnapshot({
|
||||
ownerInstanceId: vrmPreviewStageInstanceId,
|
||||
renderer: 'live2d',
|
||||
phase,
|
||||
controlsLocked: hasModel ? phase !== 'mounted' : false,
|
||||
previewAvailable: hasModel,
|
||||
canCapturePreview: !!live2dSceneRef.value?.canvasElement(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
if (stageModelRenderer.value === 'vrm') {
|
||||
return createEmptyModelSettingsRuntimeSnapshot({
|
||||
ownerInstanceId: vrmPreviewStageInstanceId,
|
||||
renderer: 'vrm',
|
||||
phase: hasModel ? scenePhase.value : 'no-model',
|
||||
controlsLocked: hasModel ? sceneMutationLocked.value : false,
|
||||
previewAvailable: hasModel,
|
||||
canCapturePreview: !!vrmSceneRef.value?.canvasElement(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
return createEmptyModelSettingsRuntimeSnapshot({
|
||||
ownerInstanceId: vrmPreviewStageInstanceId,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
})
|
||||
|
||||
watch(runtimeSnapshot, snapshot => emit('runtimeSnapshotChanged', snapshot), { immediate: true })
|
||||
|
||||
defineExpose({
|
||||
capturePreviewFrame,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="stageModelRenderer === 'live2d'">
|
||||
<div :class="live2dSceneClassList">
|
||||
<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"
|
||||
:scale="live2dScale"
|
||||
: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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="stageModelRenderer === 'vrm'">
|
||||
<div :class="vrmSceneClassList">
|
||||
<ThreeScene ref="vrmSceneRef" :model-src="stageModelSelectedUrl" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
export type ModelSettingsRuntimeRenderer = 'disabled' | 'live2d' | 'vrm'
|
||||
export type ModelSettingsRuntimePhase = 'pending' | 'loading' | 'binding' | 'mounted' | 'no-model' | 'error'
|
||||
|
||||
export interface ModelSettingsRuntimeSnapshot {
|
||||
ownerInstanceId: string
|
||||
renderer: ModelSettingsRuntimeRenderer
|
||||
phase: ModelSettingsRuntimePhase
|
||||
controlsLocked: boolean
|
||||
previewAvailable: boolean
|
||||
canCapturePreview: boolean
|
||||
lastError?: string
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export function createEmptyModelSettingsRuntimeSnapshot(
|
||||
overrides: Partial<ModelSettingsRuntimeSnapshot> = {},
|
||||
): ModelSettingsRuntimeSnapshot {
|
||||
return {
|
||||
ownerInstanceId: '',
|
||||
renderer: 'disabled',
|
||||
phase: 'pending',
|
||||
controlsLocked: false,
|
||||
previewAvailable: false,
|
||||
canCapturePreview: false,
|
||||
updatedAt: 0,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveComponentStateToRuntimePhase(
|
||||
componentState: 'pending' | 'loading' | 'mounted',
|
||||
options: {
|
||||
hasModel?: boolean
|
||||
} = {},
|
||||
): ModelSettingsRuntimePhase {
|
||||
if (options.hasModel === false)
|
||||
return 'no-model'
|
||||
|
||||
return componentState
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
import { useModelStore } from '@proj-airi/stage-ui-three'
|
||||
import { Button, Callout, SelectTab } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -8,9 +10,13 @@ import { useI18n } from 'vue-i18n'
|
||||
import { Container, PropertyColor, PropertyNumber, PropertyPoint } from '../../../data-pane'
|
||||
import { ColorPalette } from '../../../widgets'
|
||||
|
||||
defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
palette: string[]
|
||||
}>()
|
||||
allowExtractColors?: boolean
|
||||
runtimeSnapshot: ModelSettingsRuntimeSnapshot
|
||||
}>(), {
|
||||
allowExtractColors: true,
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(e: 'extractColorsFromModel'): void
|
||||
@@ -20,7 +26,6 @@ const { t } = useI18n()
|
||||
|
||||
const modelStore = useModelStore()
|
||||
const {
|
||||
sceneMutationLocked,
|
||||
modelSize,
|
||||
modelOffset,
|
||||
cameraFOV,
|
||||
@@ -42,6 +47,8 @@ const {
|
||||
envSelect,
|
||||
skyBoxIntensity,
|
||||
} = storeToRefs(modelStore)
|
||||
const controlsLocked = computed(() => props.runtimeSnapshot.controlsLocked)
|
||||
const canExtractColors = computed(() => props.runtimeSnapshot.canCapturePreview)
|
||||
const trackingOptions = computed<{
|
||||
value: 'camera' | 'mouse' | 'none'
|
||||
label: string
|
||||
@@ -54,7 +61,7 @@ const trackingOptions = computed<{
|
||||
|
||||
// switch between hemisphere light and sky box
|
||||
const settingsLockClass = computed(() => {
|
||||
return sceneMutationLocked.value ? ['pointer-events-none', 'opacity-60'] : []
|
||||
return controlsLocked.value ? ['pointer-events-none', 'opacity-60'] : []
|
||||
})
|
||||
|
||||
const envOptions = computed(() => [
|
||||
@@ -85,17 +92,19 @@ const envOptions = computed(() => [
|
||||
'backdrop-blur-lg',
|
||||
]"
|
||||
>
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" :disabled="sceneMutationLocked" @click="$emit('extractColorsFromModel')">
|
||||
{{ t('settings.vrm.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
<template v-if="allowExtractColors">
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" :disabled="controlsLocked || !canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
{{ t('settings.vrm.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<div grid="~ cols-5 gap-1" p-2 :class="settingsLockClass">
|
||||
<PropertyPoint
|
||||
v-model:x="modelOffset.x"
|
||||
v-model:y="modelOffset.y"
|
||||
v-model:z="modelOffset.z"
|
||||
:disabled="sceneMutationLocked"
|
||||
:disabled="controlsLocked"
|
||||
label="Model Position"
|
||||
:x-config="{ min: -modelSize.x * 2, max: modelSize.x * 2, step: modelSize.x / 10000, label: 'X', formatValue: val => val?.toFixed(4) }"
|
||||
:y-config="{ min: -modelSize.y * 2, max: modelSize.y * 2, step: modelSize.y / 10000, label: 'Y', formatValue: val => val?.toFixed(4) }"
|
||||
@@ -103,17 +112,17 @@ const envOptions = computed(() => [
|
||||
/>
|
||||
<PropertyNumber
|
||||
v-model="cameraFOV"
|
||||
:config="{ min: 1, max: 180, step: 1, label: t('settings.vrm.scale-and-position.fov'), disabled: sceneMutationLocked }"
|
||||
:config="{ min: 1, max: 180, step: 1, label: t('settings.vrm.scale-and-position.fov'), disabled: controlsLocked }"
|
||||
:label="t('settings.vrm.scale-and-position.fov')"
|
||||
/>
|
||||
<PropertyNumber
|
||||
v-model="cameraDistance"
|
||||
:config="{ min: modelSize.z, max: modelSize.z * 20, step: modelSize.z / 100, label: t('settings.vrm.scale-and-position.camera-distance'), formatValue: val => val?.toFixed(4), disabled: sceneMutationLocked }"
|
||||
:config="{ min: modelSize.z, max: modelSize.z * 20, step: modelSize.z / 100, label: t('settings.vrm.scale-and-position.camera-distance'), formatValue: val => val?.toFixed(4), disabled: controlsLocked }"
|
||||
:label="t('settings.vrm.scale-and-position.camera-distance')"
|
||||
/>
|
||||
<PropertyNumber
|
||||
v-model="modelRotationY"
|
||||
:config="{ min: -180, max: 180, step: 1, label: t('settings.vrm.scale-and-position.rotation-y'), disabled: sceneMutationLocked }"
|
||||
:config="{ min: -180, max: 180, step: 1, label: t('settings.vrm.scale-and-position.rotation-y'), disabled: controlsLocked }"
|
||||
:label="t('settings.vrm.scale-and-position.rotation-y')"
|
||||
/>
|
||||
|
||||
@@ -125,7 +134,7 @@ const envOptions = computed(() => [
|
||||
<template v-for="option in trackingOptions" :key="option.value">
|
||||
<Button
|
||||
:class="[option.class, 'w-auto']"
|
||||
:disabled="sceneMutationLocked"
|
||||
:disabled="controlsLocked"
|
||||
size="sm"
|
||||
:variant="trackingMode === option.value ? 'primary' : 'secondary'"
|
||||
:label="option.label"
|
||||
@@ -135,34 +144,34 @@ const envOptions = computed(() => [
|
||||
|
||||
<PropertyNumber
|
||||
v-model="directionalLightRotation.x"
|
||||
:config="{ min: -180, max: 180, step: 1, label: 'RotationXDeg', formatValue: val => val?.toFixed(0), disabled: sceneMutationLocked }"
|
||||
:config="{ min: -180, max: 180, step: 1, label: 'RotationXDeg', formatValue: val => val?.toFixed(0), disabled: controlsLocked }"
|
||||
label="Directional Light Rotation - X"
|
||||
/>
|
||||
<PropertyNumber
|
||||
v-model="directionalLightRotation.y"
|
||||
:config="{ min: -180, max: 180, step: 1, label: 'RotationYDeg', formatValue: val => val?.toFixed(0), disabled: sceneMutationLocked }"
|
||||
:config="{ min: -180, max: 180, step: 1, label: 'RotationYDeg', formatValue: val => val?.toFixed(0), disabled: controlsLocked }"
|
||||
label="Directional Light Rotation - Y"
|
||||
/>
|
||||
<PropertyColor
|
||||
v-model="directionalLightColor"
|
||||
:disabled="sceneMutationLocked"
|
||||
:disabled="controlsLocked"
|
||||
label="Directional Light Color"
|
||||
/>
|
||||
|
||||
<PropertyNumber
|
||||
v-model="directionalLightIntensity"
|
||||
:config="{ min: 0, max: 10, step: 0.01, label: 'Intensity', disabled: sceneMutationLocked }"
|
||||
:config="{ min: 0, max: 10, step: 0.01, label: 'Intensity', disabled: controlsLocked }"
|
||||
label="Directional Light Intensity"
|
||||
/>
|
||||
|
||||
<PropertyNumber
|
||||
v-model="ambientLightIntensity"
|
||||
:config="{ min: 0, max: 10, step: 0.01, label: 'Intensity', disabled: sceneMutationLocked }"
|
||||
:config="{ min: 0, max: 10, step: 0.01, label: 'Intensity', disabled: controlsLocked }"
|
||||
label="Ambient Light Intensity"
|
||||
/>
|
||||
<PropertyColor
|
||||
v-model="ambientLightColor"
|
||||
:disabled="sceneMutationLocked"
|
||||
:disabled="controlsLocked"
|
||||
label="Ambient Light Color"
|
||||
/>
|
||||
</div>
|
||||
@@ -179,24 +188,24 @@ const envOptions = computed(() => [
|
||||
Environment
|
||||
</div>
|
||||
<div :class="['p-2', ...settingsLockClass]">
|
||||
<SelectTab v-model="envSelect" :options="envOptions" :disabled="sceneMutationLocked" size="sm" />
|
||||
<SelectTab v-model="envSelect" :options="envOptions" :disabled="controlsLocked" size="sm" />
|
||||
</div>
|
||||
<div v-if="envSelect === 'hemisphere'">
|
||||
<!-- hemisphere settings -->
|
||||
<div grid="~ cols-5 gap-1" p-2 :class="settingsLockClass">
|
||||
<PropertyNumber
|
||||
v-model="hemisphereLightIntensity"
|
||||
:config="{ min: 0, max: 10, step: 0.01, label: 'Intensity', disabled: sceneMutationLocked }"
|
||||
:config="{ min: 0, max: 10, step: 0.01, label: 'Intensity', disabled: controlsLocked }"
|
||||
label="Hemisphere Light Intensity"
|
||||
/>
|
||||
<PropertyColor
|
||||
v-model="hemisphereSkyColor"
|
||||
:disabled="sceneMutationLocked"
|
||||
:disabled="controlsLocked"
|
||||
label="Hemisphere Sky Color"
|
||||
/>
|
||||
<PropertyColor
|
||||
v-model="hemisphereGroundColor"
|
||||
:disabled="sceneMutationLocked"
|
||||
:disabled="controlsLocked"
|
||||
label="Hemisphere Ground Color"
|
||||
/>
|
||||
</div>
|
||||
@@ -206,7 +215,7 @@ const envOptions = computed(() => [
|
||||
<div grid="~ cols-5 gap-1" p-2 :class="settingsLockClass">
|
||||
<PropertyNumber
|
||||
v-model="skyBoxIntensity"
|
||||
:config="{ min: 0, max: 1, step: 0.01, label: 'Intensity', disabled: sceneMutationLocked }"
|
||||
:config="{ min: 0, max: 1, step: 0.01, label: 'Intensity', disabled: controlsLocked }"
|
||||
:label="t('settings.vrm.skybox.skybox-intensity')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ catalogs:
|
||||
xsai:
|
||||
unspeech: ^0.1.11
|
||||
|
||||
enableGlobalVirtualStore: true
|
||||
# enableGlobalVirtualStore: true
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@prisma/client'
|
||||
|
||||
Reference in New Issue
Block a user