mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
## Summary For better support animation 3d model custon node, such as https://github.com/jtydhr88/ComfyUI-HY-Motion1, add ability to show/hide skeleton bones in Load3D nodes for models with skeletal animation. Uses THREE.SkeletonHelper with root bone detection to properly support both FBX and GLB model formats. ## Screenshots https://github.com/user-attachments/assets/df9de4a6-549e-4227-aa00-8859d71f43d1 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7857-feat-add-skeleton-visualization-toggle-for-3D-models-2e06d73d365081a39f49f81f72657a70) by [Unito](https://www.unito.io)
151 lines
4.2 KiB
Vue
151 lines
4.2 KiB
Vue
<template>
|
|
<div
|
|
class="relative size-full"
|
|
@mouseenter="handleMouseEnter"
|
|
@mouseleave="handleMouseLeave"
|
|
@pointerdown.stop
|
|
@pointermove.stop
|
|
@pointerup.stop
|
|
>
|
|
<Load3DScene
|
|
v-if="node"
|
|
:initialize-load3d="initializeLoad3d"
|
|
:cleanup="cleanup"
|
|
:loading="loading"
|
|
:loading-message="loadingMessage"
|
|
:on-model-drop="isPreview ? undefined : handleModelDrop"
|
|
:is-preview="isPreview"
|
|
/>
|
|
<div class="pointer-events-none absolute top-0 left-0 size-full">
|
|
<Load3DControls
|
|
v-model:scene-config="sceneConfig"
|
|
v-model:model-config="modelConfig"
|
|
v-model:camera-config="cameraConfig"
|
|
v-model:light-config="lightConfig"
|
|
:is-splat-model="isSplatModel"
|
|
:is-ply-model="isPlyModel"
|
|
:has-skeleton="hasSkeleton"
|
|
@update-background-image="handleBackgroundImageUpdate"
|
|
@export-model="handleExportModel"
|
|
/>
|
|
<AnimationControls
|
|
v-if="animations && animations.length > 0"
|
|
v-model:animations="animations"
|
|
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
|
|
v-if="enable3DViewer && node"
|
|
class="pointer-events-auto absolute top-12 right-2 z-20"
|
|
>
|
|
<ViewerControls :node="node as LGraphNode" />
|
|
</div>
|
|
|
|
<div
|
|
v-if="!isPreview"
|
|
class="pointer-events-auto absolute right-2 z-20"
|
|
:class="{
|
|
'top-12': !enable3DViewer,
|
|
'top-24': enable3DViewer
|
|
}"
|
|
>
|
|
<RecordingControls
|
|
v-model:is-recording="isRecording"
|
|
v-model:has-recording="hasRecording"
|
|
v-model:recording-duration="recordingDuration"
|
|
@start-recording="handleStartRecording"
|
|
@stop-recording="handleStopRecording"
|
|
@export-recording="handleExportRecording"
|
|
@clear-recording="handleClearRecording"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import type { Ref } from 'vue'
|
|
|
|
import Load3DControls from '@/components/load3d/Load3DControls.vue'
|
|
import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
|
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
|
|
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
|
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
|
|
import { useLoad3d } from '@/composables/useLoad3d'
|
|
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
|
import { app } from '@/scripts/app'
|
|
import type { ComponentWidget } from '@/scripts/domWidget'
|
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
|
|
|
const props = defineProps<{
|
|
widget: ComponentWidget<string[]> | SimplifiedWidget
|
|
nodeId?: NodeId
|
|
}>()
|
|
|
|
function isComponentWidget(
|
|
widget: ComponentWidget<string[]> | SimplifiedWidget
|
|
): widget is ComponentWidget<string[]> {
|
|
return 'node' in widget && widget.node !== undefined
|
|
}
|
|
|
|
const node = ref<LGraphNode | null>(null)
|
|
|
|
if (isComponentWidget(props.widget)) {
|
|
node.value = props.widget.node
|
|
} else if (props.nodeId) {
|
|
onMounted(() => {
|
|
node.value = app.rootGraph?.getNodeById(props.nodeId!) || null
|
|
})
|
|
}
|
|
|
|
const {
|
|
// configs
|
|
sceneConfig,
|
|
modelConfig,
|
|
cameraConfig,
|
|
lightConfig,
|
|
|
|
// other state
|
|
isRecording,
|
|
isPreview,
|
|
isSplatModel,
|
|
isPlyModel,
|
|
hasSkeleton,
|
|
hasRecording,
|
|
recordingDuration,
|
|
animations,
|
|
playing,
|
|
selectedSpeed,
|
|
selectedAnimation,
|
|
animationProgress,
|
|
animationDuration,
|
|
loading,
|
|
loadingMessage,
|
|
|
|
// Methods
|
|
initializeLoad3d,
|
|
handleMouseEnter,
|
|
handleMouseLeave,
|
|
handleStartRecording,
|
|
handleStopRecording,
|
|
handleExportRecording,
|
|
handleClearRecording,
|
|
handleSeek,
|
|
handleBackgroundImageUpdate,
|
|
handleExportModel,
|
|
handleModelDrop,
|
|
cleanup
|
|
} = useLoad3d(node as Ref<LGraphNode | null>)
|
|
|
|
const enable3DViewer = computed(() =>
|
|
useSettingStore().get('Comfy.Load3D.3DViewerEnable')
|
|
)
|
|
</script>
|