[refactor] refactor load3d (#5765)

Summary

Fully Refactored the Load3D module to improve architecture and
maintainability by consolidating functionality into a
centralized composable pattern and simplifying component structure. and
support VueNodes system

  Changes

- Architecture: Introduced new useLoad3d composable to centralize 3D
loading logic and state
  management
- Component Simplification: Removed redundant components
(Load3DAnimation.vue, Load3DAnimationScene.vue,
  PreviewManager.ts) 
- Support VueNodes
- improve config store
- remove lineart output due Animation doesnot support it, may add it
back later
- remove Preview screen and keep scene in fixed ratio in load3d (not
affect preview3d)
- improve record video feature which will already record video by same
ratio as scene
Need BE change https://github.com/comfyanonymous/ComfyUI/pull/10025


https://github.com/user-attachments/assets/9e038729-84a0-45ad-b0f2-11c57d7e0c9a



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5765-refactor-refactor-load3d-2796d73d365081728297cc486e2e9052)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2025-10-31 16:19:35 -04:00
committed by GitHub
parent 91b5a7de17
commit afa10f7a1e
51 changed files with 2784 additions and 4200 deletions

View File

@@ -1,6 +1,10 @@
<template>
<div
class="pointer-events-auto absolute top-12 left-2 z-20 flex flex-col rounded-lg bg-smoke-700/30"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
@wheel.stop
>
<div class="show-menu relative">
<Button class="p-button-rounded p-button-text" @click="toggleMenu">
@@ -20,7 +24,9 @@
@click="selectCategory(category)"
>
<i :class="getCategoryIcon(category)" />
<span class="text-white">{{ t(categoryLabels[category]) }}</span>
<span class="whitespace-nowrap text-white">{{
$t(categoryLabels[category])
}}</span>
</Button>
</div>
</div>
@@ -28,71 +34,47 @@
<div v-show="activeCategory" class="rounded-lg bg-smoke-700/30">
<SceneControls
v-if="activeCategory === 'scene'"
v-if="showSceneControls"
ref="sceneControlsRef"
:background-color="backgroundColor"
:show-grid="showGrid"
:has-background-image="hasBackgroundImage"
@toggle-grid="handleToggleGrid"
@update-background-color="handleBackgroundColorChange"
v-model:show-grid="sceneConfig!.showGrid"
v-model:background-color="sceneConfig!.backgroundColor"
v-model:background-image="sceneConfig!.backgroundImage"
@update-background-image="handleBackgroundImageUpdate"
/>
<ModelControls
v-if="activeCategory === 'model'"
v-if="showModelControls"
ref="modelControlsRef"
:input-spec="inputSpec"
:up-direction="upDirection"
:material-mode="materialMode"
:edge-threshold="edgeThreshold"
@update-up-direction="handleUpdateUpDirection"
@update-material-mode="handleUpdateMaterialMode"
@update-edge-threshold="handleUpdateEdgeThreshold"
v-model:material-mode="modelConfig!.materialMode"
v-model:up-direction="modelConfig!.upDirection"
/>
<CameraControls
v-if="activeCategory === 'camera'"
v-if="showCameraControls"
ref="cameraControlsRef"
:camera-type="cameraType"
:fov="fov"
:show-f-o-v-button="showFOVButton"
@switch-camera="switchCamera"
@update-f-o-v="handleUpdateFOV"
v-model:camera-type="cameraConfig!.cameraType"
v-model:fov="cameraConfig!.fov"
/>
<LightControls
v-if="activeCategory === 'light'"
v-if="showLightControls"
ref="lightControlsRef"
:light-intensity="lightIntensity"
:show-light-intensity-button="showLightIntensityButton"
@update-light-intensity="handleUpdateLightIntensity"
v-model:light-intensity="lightConfig!.intensity"
v-model:material-mode="modelConfig!.materialMode"
/>
<ExportControls
v-if="activeCategory === 'export'"
v-if="showExportControls"
ref="exportControlsRef"
@export-model="handleExportModel"
/>
</div>
<div v-if="showPreviewButton">
<Button class="p-button-rounded p-button-text" @click="togglePreview">
<i
v-tooltip.right="{ value: t('load3d.previewOutput'), showDelay: 300 }"
:class="[
'pi',
showPreview ? 'pi-eye' : 'pi-eye-slash',
'text-lg text-white'
]"
/>
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import CameraControls from '@/components/load3d/controls/CameraControls.vue'
import ExportControls from '@/components/load3d/controls/ExportControls.vue'
@@ -100,31 +82,16 @@ import LightControls from '@/components/load3d/controls/LightControls.vue'
import ModelControls from '@/components/load3d/controls/ModelControls.vue'
import SceneControls from '@/components/load3d/controls/SceneControls.vue'
import type {
CameraType,
MaterialMode,
UpDirection
CameraConfig,
LightConfig,
ModelConfig,
SceneConfig
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const vTooltip = Tooltip
const props = defineProps<{
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
showPreview: boolean
lightIntensity: number
showLightIntensityButton: boolean
fov: number
showFOVButton: boolean
showPreviewButton: boolean
cameraType: CameraType
hasBackgroundImage?: boolean
upDirection: UpDirection
materialMode: MaterialMode
edgeThreshold?: number
}>()
const sceneConfig = defineModel<SceneConfig>('sceneConfig')
const modelConfig = defineModel<ModelConfig>('modelConfig')
const cameraConfig = defineModel<CameraConfig>('cameraConfig')
const lightConfig = defineModel<LightConfig>('lightConfig')
const isMenuOpen = ref(false)
const activeCategory = ref<string>('scene')
@@ -137,15 +104,26 @@ const categoryLabels: Record<string, string> = {
}
const availableCategories = computed(() => {
const baseCategories = ['scene', 'model', 'camera', 'light']
if (!props.inputSpec.isAnimation) {
return [...baseCategories, 'export']
}
return baseCategories
return ['scene', 'model', 'camera', 'light', 'export']
})
const showSceneControls = computed(
() => activeCategory.value === 'scene' && !!sceneConfig.value
)
const showModelControls = computed(
() => activeCategory.value === 'model' && !!modelConfig.value
)
const showCameraControls = computed(
() => activeCategory.value === 'camera' && !!cameraConfig.value
)
const showLightControls = computed(
() =>
activeCategory.value === 'light' &&
!!lightConfig.value &&
!!modelConfig.value
)
const showExportControls = computed(() => activeCategory.value === 'export')
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value
}
@@ -168,73 +146,14 @@ const getCategoryIcon = (category: string) => {
}
const emit = defineEmits<{
(e: 'switchCamera'): void
(e: 'toggleGrid', value: boolean): void
(e: 'updateBackgroundColor', color: string): void
(e: 'updateLightIntensity', value: number): void
(e: 'updateFOV', value: number): void
(e: 'togglePreview', value: boolean): void
(e: 'updateBackgroundImage', file: File | null): void
(e: 'updateUpDirection', direction: UpDirection): void
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
(e: 'exportModel', format: string): void
}>()
const backgroundColor = ref(props.backgroundColor)
const showGrid = ref(props.showGrid)
const showPreview = ref(props.showPreview)
const lightIntensity = ref(props.lightIntensity)
const upDirection = ref(props.upDirection || 'original')
const materialMode = ref(props.materialMode || 'original')
const showLightIntensityButton = ref(props.showLightIntensityButton)
const fov = ref(props.fov)
const showFOVButton = ref(props.showFOVButton)
const showPreviewButton = ref(props.showPreviewButton)
const hasBackgroundImage = ref(props.hasBackgroundImage)
const edgeThreshold = ref(props.edgeThreshold)
const switchCamera = () => {
emit('switchCamera')
}
const togglePreview = () => {
showPreview.value = !showPreview.value
emit('togglePreview', showPreview.value)
}
const handleToggleGrid = (value: boolean) => {
emit('toggleGrid', value)
}
const handleBackgroundColorChange = (value: string) => {
emit('updateBackgroundColor', value)
}
const handleBackgroundImageUpdate = (file: File | null) => {
emit('updateBackgroundImage', file)
}
const handleUpdateUpDirection = (direction: UpDirection) => {
emit('updateUpDirection', direction)
}
const handleUpdateMaterialMode = (mode: MaterialMode) => {
emit('updateMaterialMode', mode)
}
const handleUpdateEdgeThreshold = (value: number) => {
emit('updateEdgeThreshold', value)
}
const handleUpdateLightIntensity = (value: number) => {
emit('updateLightIntensity', value)
}
const handleUpdateFOV = (value: number) => {
emit('updateFOV', value)
}
const handleExportModel = (format: string) => {
emit('exportModel', format)
}
@@ -247,101 +166,6 @@ const closeSlider = (e: MouseEvent) => {
}
}
watch(
() => props.upDirection,
(newValue) => {
if (newValue) {
upDirection.value = newValue
}
}
)
watch(
() => props.backgroundColor,
(newValue) => {
backgroundColor.value = newValue
}
)
watch(
() => props.fov,
(newValue) => {
fov.value = newValue
}
)
watch(
() => props.lightIntensity,
(newValue) => {
lightIntensity.value = newValue
}
)
watch(
() => props.showFOVButton,
(newValue) => {
showFOVButton.value = newValue
}
)
watch(
() => props.showLightIntensityButton,
(newValue) => {
showLightIntensityButton.value = newValue
}
)
watch(
() => props.upDirection,
(newValue) => {
upDirection.value = newValue
}
)
watch(
() => props.materialMode,
(newValue) => {
materialMode.value = newValue
}
)
watch(
() => props.showPreviewButton,
(newValue) => {
showPreviewButton.value = newValue
}
)
watch(
() => props.showPreview,
(newValue) => {
showPreview.value = newValue
}
)
watch(
() => props.hasBackgroundImage,
(newValue) => {
hasBackgroundImage.value = newValue
}
)
watch(
() => props.materialMode,
(newValue) => {
if (newValue) {
materialMode.value = newValue
}
}
)
watch(
() => props.edgeThreshold,
(newValue) => {
edgeThreshold.value = newValue
}
)
onMounted(() => {
document.addEventListener('click', closeSlider)
})