[3d] add recording video support (#3749)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Terry Jia
2025-05-03 23:00:07 -04:00
committed by GitHub
parent 8ae36e2c8d
commit 77ac4a415c
15 changed files with 542 additions and 9 deletions

View File

@@ -57,6 +57,21 @@
@upload-texture="handleUploadTexture"
@export-model="handleExportModel"
/>
<div
v-if="showRecordingControls"
class="absolute top-12 right-2 z-20 pointer-events-auto"
>
<RecordingControls
:node="node"
:is-recording="isRecording"
:has-recording="hasRecording"
:recording-duration="recordingDuration"
@start-recording="handleStartRecording"
@stop-recording="handleStopRecording"
@export-recording="handleExportRecording"
@clear-recording="handleClearRecording"
/>
</div>
</div>
</template>
@@ -66,6 +81,7 @@ import { useI18n } from 'vue-i18n'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import {
CameraType,
@@ -101,6 +117,10 @@ const upDirection = ref<UpDirection>('original')
const materialMode = ref<MaterialMode>('original')
const edgeThreshold = ref(85)
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
const isRecording = ref(false)
const hasRecording = ref(false)
const recordingDuration = ref(0)
const showRecordingControls = ref(!inputSpec.isPreview)
const showPreviewButton = computed(() => {
return !type.includes('Preview')
@@ -118,6 +138,38 @@ const handleMouseLeave = () => {
}
}
const handleStartRecording = async () => {
if (load3DSceneRef.value?.load3d) {
await load3DSceneRef.value.load3d.startRecording()
isRecording.value = true
}
}
const handleStopRecording = () => {
if (load3DSceneRef.value?.load3d) {
load3DSceneRef.value.load3d.stopRecording()
isRecording.value = false
hasRecording.value = true
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
}
}
const handleExportRecording = () => {
if (load3DSceneRef.value?.load3d) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const filename = `${timestamp}-scene-recording.mp4`
load3DSceneRef.value.load3d.exportRecording(filename)
}
}
const handleClearRecording = () => {
if (load3DSceneRef.value?.load3d) {
load3DSceneRef.value.load3d.clearRecording()
hasRecording.value = false
recordingDuration.value = 0
}
}
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'

View File

@@ -0,0 +1,176 @@
<template>
<div class="relative bg-gray-700 bg-opacity-30 rounded-lg">
<div class="flex flex-col gap-2">
<Button
class="p-button-rounded p-button-text"
@click="resizeNodeMatchOutput"
>
<i
v-tooltip.right="{
value: t('load3d.resizeNodeMatchOutput'),
showDelay: 300
}"
class="pi pi-window-maximize text-white text-lg"
/>
</Button>
<Button
class="p-button-rounded p-button-text"
:class="{ 'p-button-danger': isRecording }"
@click="toggleRecording"
>
<i
v-tooltip.right="{
value: isRecording
? t('load3d.stopRecording')
: t('load3d.startRecording'),
showDelay: 300
}"
:class="[
'pi',
isRecording ? 'pi-circle-fill' : 'pi-video',
'text-white text-lg'
]"
/>
</Button>
<Button
v-if="hasRecording && !isRecording"
class="p-button-rounded p-button-text"
@click="exportRecording"
>
<i
v-tooltip.right="{
value: t('load3d.exportRecording'),
showDelay: 300
}"
class="pi pi-download text-white text-lg"
/>
</Button>
<Button
v-if="hasRecording && !isRecording"
class="p-button-rounded p-button-text"
@click="clearRecording"
>
<i
v-tooltip.right="{
value: t('load3d.clearRecording'),
showDelay: 300
}"
class="pi pi-trash text-white text-lg"
/>
</Button>
<div
v-if="recordingDuration > 0 && !isRecording"
class="text-xs text-white text-center mt-1"
>
{{ formatDuration(recordingDuration) }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { IWidget, LGraphNode } from '@comfyorg/litegraph'
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { ref, watch } from 'vue'
import { t } from '@/i18n'
const vTooltip = Tooltip
const props = defineProps<{
node: LGraphNode
isRecording: boolean
hasRecording: boolean
recordingDuration: number
}>()
const emit = defineEmits<{
(e: 'startRecording'): void
(e: 'stopRecording'): void
(e: 'exportRecording'): void
(e: 'clearRecording'): void
}>()
const node = ref(props.node)
const isRecording = ref(props.isRecording)
const hasRecording = ref(props.hasRecording)
const recordingDuration = ref(props.recordingDuration)
watch(
() => props.isRecording,
(newValue) => {
isRecording.value = newValue
}
)
watch(
() => props.hasRecording,
(newValue) => {
hasRecording.value = newValue
}
)
watch(
() => props.recordingDuration,
(newValue) => {
recordingDuration.value = newValue
}
)
const resizeNodeMatchOutput = () => {
console.log('resizeNodeMatchOutput')
const outputWidth = node.value.widgets?.find(
(w: IWidget) => w.name === 'width'
)
const outputHeight = node.value.widgets?.find(
(w: IWidget) => w.name === 'height'
)
if (outputWidth && outputHeight && outputHeight.value && outputWidth.value) {
const [oldWidth, oldHeight] = node.value.size
const scene = node.value.widgets?.find((w: IWidget) => w.name === 'image')
const sceneHeight = scene?.computedHeight
if (sceneHeight) {
const sceneWidth = oldWidth - 20
const outputRatio = Number(outputHeight.value) / Number(outputWidth.value)
const expectSceneHeight = sceneWidth * outputRatio
node.value.setSize([
oldWidth,
oldHeight + (expectSceneHeight - sceneHeight)
])
}
}
}
const toggleRecording = () => {
if (isRecording.value) {
emit('stopRecording')
} else {
emit('startRecording')
}
}
const exportRecording = () => {
emit('exportRecording')
}
const clearRecording = () => {
emit('clearRecording')
}
const formatDuration = (seconds: number): string => {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.floor(seconds % 60)
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
}
</script>