Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/load3d/Load3D.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
v-model:playing="playing"
v-model:selected-speed="selectedSpeed"
v-model:selected-animation="selectedAnimation"
v-model:animation-progress="animationProgress"
v-model:animation-duration="animationDuration"
@seek="handleSeek"
/>
</div>
<div
Expand Down Expand Up @@ -119,6 +122,8 @@ const {
playing,
selectedSpeed,
selectedAnimation,
animationProgress,
animationDuration,
loading,
loadingMessage,

Expand All @@ -130,6 +135,7 @@ const {
handleStopRecording,
handleExportRecording,
handleClearRecording,
handleSeek,
handleBackgroundImageUpdate,
handleExportModel,
handleModelDrop,
Expand Down
11 changes: 11 additions & 0 deletions src/components/load3d/Load3dViewerContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
@dragleave.stop="handleDragLeave"
@drop.prevent.stop="handleDrop"
/>
<AnimationControls
v-if="viewer.animations.value && viewer.animations.value.length > 0"
v-model:animations="viewer.animations.value"
v-model:playing="viewer.playing.value"
v-model:selected-speed="viewer.selectedSpeed.value"
v-model:selected-animation="viewer.selectedAnimation.value"
v-model:animation-progress="viewer.animationProgress.value"
v-model:animation-duration="viewer.animationDuration.value"
@seek="viewer.handleSeek"
/>
<div
v-if="isDragging"
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
Expand Down Expand Up @@ -85,6 +95,7 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, toRaw } from 'vue'

import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
import CameraControls from '@/components/load3d/controls/viewer/ViewerCameraControls.vue'
import ExportControls from '@/components/load3d/controls/viewer/ViewerExportControls.vue'
import LightControls from '@/components/load3d/controls/viewer/ViewerLightControls.vue'
Expand Down
106 changes: 78 additions & 28 deletions src/components/load3d/controls/AnimationControls.vue
Original file line number Diff line number Diff line change
@@ -1,49 +1,81 @@
<template>
<div
v-if="animations && animations.length > 0"
class="pointer-events-auto absolute top-0 left-0 z-10 flex w-full items-center justify-center gap-2 pt-2"
class="pointer-events-auto absolute top-0 left-0 z-10 flex w-full flex-col items-center gap-2 pt-2"
>
<Button
size="icon"
variant="textonly"
class="rounded-full"
:aria-label="$t('g.playPause')"
@click="togglePlay"
>
<i
:class="['pi', playing ? 'pi-pause' : 'pi-play', 'text-lg text-white']"
<div class="flex items-center justify-center gap-2">
<Button
size="icon"
variant="textonly"
class="rounded-full"
:aria-label="$t('g.playPause')"
@click="togglePlay"
>
<i
:class="[
'pi',
playing ? 'pi-pause' : 'pi-play',
'text-lg text-white'
]"
/>
</Button>

<Select
v-model="selectedSpeed"
:options="speedOptions"
option-label="name"
option-value="value"
class="w-24"
/>

<Select
v-model="selectedAnimation"
:options="animations"
option-label="name"
option-value="index"
class="w-32"
/>
</Button>

<Select
v-model="selectedSpeed"
:options="speedOptions"
option-label="name"
option-value="value"
class="w-24"
/>

<Select
v-model="selectedAnimation"
:options="animations"
option-label="name"
option-value="index"
class="w-32"
/>
</div>

<div class="flex w-full max-w-xs items-center gap-2 px-4">
<Slider
:model-value="[animationProgress]"
:min="0"
:max="100"
:step="0.1"
class="flex-1"
@update:model-value="handleSliderChange"
/>
<span class="min-w-16 text-xs text-white">
{{ formatTime(currentTime) }} / {{ formatTime(animationDuration) }}
</span>
</div>
</div>
</template>

<script setup lang="ts">
import Select from 'primevue/select'
import { computed } from 'vue'

import Button from '@/components/ui/button/Button.vue'
import Slider from '@/components/ui/slider/Slider.vue'

type Animation = { name: string; index: number }

const animations = defineModel<Animation[]>('animations')
const playing = defineModel<boolean>('playing')
const selectedSpeed = defineModel<number>('selectedSpeed')
const selectedAnimation = defineModel<number>('selectedAnimation')
const animationProgress = defineModel<number>('animationProgress', {
default: 0
})
const animationDuration = defineModel<number>('animationDuration', {
default: 0
})

const emit = defineEmits<{
seek: [progress: number]
}>()

const speedOptions = [
{ name: '0.1x', value: 0.1 },
Expand All @@ -53,7 +85,25 @@ const speedOptions = [
{ name: '2x', value: 2 }
]

const togglePlay = () => {
const currentTime = computed(() => {
if (!animationDuration.value) return 0
return (animationProgress.value / 100) * animationDuration.value
})

function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = (seconds % 60).toFixed(1)
return mins > 0 ? `${mins}:${secs.padStart(4, '0')}` : `${secs}s`
}

function togglePlay() {
playing.value = !playing.value
}

function handleSliderChange(value: number[] | undefined) {
if (!value) return
const progress = value[0]
animationProgress.value = progress
emit('seek', progress)
}
</script>
20 changes: 20 additions & 0 deletions src/composables/useLoad3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
const playing = ref(false)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const animationProgress = ref(0)
const animationDuration = ref(0)
const loading = ref(false)
const loadingMessage = ref('')
const isPreview = ref(false)
Expand Down Expand Up @@ -357,6 +359,13 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
}
}

const handleSeek = (progress: number) => {
if (load3d && animationDuration.value > 0) {
const time = (progress / 100) * animationDuration.value
load3d.setAnimationTime(time)
}
}

const handleBackgroundImageUpdate = async (file: File | null) => {
if (!file) {
sceneConfig.value.backgroundImage = ''
Expand Down Expand Up @@ -514,6 +523,14 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
animationListChange: (newValue: AnimationItem[]) => {
animations.value = newValue
},
animationProgressChange: (data: {
progress: number
currentTime: number
duration: number
}) => {
animationProgress.value = data.progress
animationDuration.value = data.duration
},
cameraChanged: (cameraState: CameraState) => {
const rawNode = toRaw(nodeRef.value)
if (rawNode) {
Expand Down Expand Up @@ -573,6 +590,8 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
playing,
selectedSpeed,
selectedAnimation,
animationProgress,
animationDuration,
loading,
loadingMessage,

Expand All @@ -585,6 +604,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
handleStopRecording,
handleExportRecording,
handleClearRecording,
handleSeek,
handleBackgroundImageUpdate,
handleExportModel,
handleModelDrop,
Expand Down
77 changes: 77 additions & 0 deletions src/composables/useLoad3dViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ref, toRaw, watch } from 'vue'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
AnimationItem,
BackgroundRenderModeType,
CameraState,
CameraType,
Expand Down Expand Up @@ -49,6 +50,14 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
const isSplatModel = ref(false)
const isPlyModel = ref(false)

// Animation state
const animations = ref<AnimationItem[]>([])
const playing = ref(false)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const animationProgress = ref(0)
const animationDuration = ref(0)

let load3d: Load3d | null = null
let sourceLoad3d: Load3d | null = null

Expand Down Expand Up @@ -174,6 +183,61 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
}
})

// Animation watches
watch(playing, (newValue) => {
if (load3d) {
load3d.toggleAnimation(newValue)
}
})

watch(selectedSpeed, (newValue) => {
if (load3d && newValue) {
load3d.setAnimationSpeed(newValue)
}
})

watch(selectedAnimation, (newValue) => {
if (load3d && newValue !== undefined) {
load3d.updateSelectedAnimation(newValue)
}
})

const handleSeek = (progress: number) => {
if (load3d && animationDuration.value > 0) {
const time = (progress / 100) * animationDuration.value
load3d.setAnimationTime(time)
}
}

const setupAnimationEvents = () => {
if (!load3d) return

load3d.addEventListener(
'animationListChange',
(newValue: AnimationItem[]) => {
animations.value = newValue
}
)

load3d.addEventListener(
'animationProgressChange',
(data: { progress: number; currentTime: number; duration: number }) => {
animationProgress.value = data.progress
animationDuration.value = data.duration
}
)

// Initialize animation list if animations already exist
if (load3d.hasAnimations()) {
const clips = load3d.animationManager.animationClips
animations.value = clips.map((clip, index) => ({
name: clip.name || `Animation ${index + 1}`,
index
}))
animationDuration.value = load3d.getAnimationDuration()
}
}

/**
* Initialize viewer in node mode (with source Load3d)
*/
Expand Down Expand Up @@ -270,6 +334,8 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
upDirection: upDirection.value,
materialMode: materialMode.value
}

setupAnimationEvents()
} catch (error) {
console.error('Error initializing Load3d viewer:', error)
useToastStore().addAlert(
Expand Down Expand Up @@ -310,6 +376,8 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
isPlyModel.value = load3d.isPlyModel()

isPreview.value = true

setupAnimationEvents()
} catch (error) {
console.error('Error initializing standalone 3D viewer:', error)
useToastStore().addAlert('Failed to load 3D model')
Expand Down Expand Up @@ -527,6 +595,14 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
isSplatModel,
isPlyModel,

// Animation state
animations,
playing,
selectedSpeed,
selectedAnimation,
animationProgress,
animationDuration,

// Methods
initializeViewer,
initializeStandaloneViewer,
Expand All @@ -539,6 +615,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
refreshViewport,
handleBackgroundImageUpdate,
handleModelDrop,
handleSeek,
cleanup
}
}
Loading