[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,71 +1,48 @@
<template>
<div
class="relative h-full w-full"
class="widget-expands relative h-full w-full"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
>
<Load3DScene
v-if="node"
ref="load3DSceneRef"
:node="node"
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:light-intensity="lightIntensity"
:fov="fov"
:camera-type="cameraType"
:show-preview="showPreview"
:background-image="backgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
:edge-threshold="edgeThreshold"
@material-mode-change="listenMaterialModeChange"
@background-color-change="listenBackgroundColorChange"
@light-intensity-change="listenLightIntensityChange"
@fov-change="listenFOVChange"
@camera-type-change="listenCameraTypeChange"
@show-grid-change="listenShowGridChange"
@show-preview-change="listenShowPreviewChange"
@background-image-change="listenBackgroundImageChange"
@up-direction-change="listenUpDirectionChange"
@edge-threshold-change="listenEdgeThresholdChange"
@recording-status-change="listenRecordingStatusChange"
/>
<Load3DControls
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:show-preview="showPreview"
:light-intensity="lightIntensity"
:show-light-intensity-button="showLightIntensityButton"
:fov="fov"
:show-f-o-v-button="showFOVButton"
:show-preview-button="showPreviewButton"
:camera-type="cameraType"
:has-background-image="hasBackgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
:edge-threshold="edgeThreshold"
@update-background-image="handleBackgroundImageUpdate"
@switch-camera="switchCamera"
@toggle-grid="toggleGrid"
@update-background-color="handleBackgroundColorChange"
@update-light-intensity="handleUpdateLightIntensity"
@toggle-preview="togglePreview"
@update-f-o-v="handleUpdateFOV"
@update-up-direction="handleUpdateUpDirection"
@update-material-mode="handleUpdateMaterialMode"
@update-edge-threshold="handleUpdateEdgeThreshold"
@export-model="handleExportModel"
: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 h-full w-full">
<Load3DControls
v-model:scene-config="sceneConfig"
v-model:model-config="modelConfig"
v-model:camera-config="cameraConfig"
v-model:light-config="lightConfig"
@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"
/>
</div>
<div
v-if="enable3DViewer"
v-if="enable3DViewer && node"
class="pointer-events-auto absolute top-12 right-2 z-20"
>
<ViewerControls :node="node" />
<ViewerControls :node="node as LGraphNode" />
</div>
<div
v-if="showRecordingControls"
v-if="!isPreview"
class="pointer-events-auto absolute right-2 z-20"
:class="{
'top-12': !enable3DViewer,
@@ -73,10 +50,9 @@
}"
>
<RecordingControls
:node="node"
:is-recording="isRecording"
:has-recording="hasRecording"
:recording-duration="recordingDuration"
v-model:is-recording="isRecording"
v-model:has-recording="hasRecording"
v-model:recording-duration="recordingDuration"
@start-recording="handleStartRecording"
@stop-recording="handleStopRecording"
@export-recording="handleExportRecording"
@@ -87,250 +63,79 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
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 Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
CameraType,
Load3DNodeType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { useLoad3d } from '@/composables/useLoad3d'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
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 { t } = useI18n()
const { widget } = defineProps<{
widget: ComponentWidget<string[]>
const props = defineProps<{
widget: ComponentWidget<string[]> | SimplifiedWidget
nodeId?: NodeId
}>()
const inputSpec = widget.inputSpec as CustomInputSpec
function isComponentWidget(
widget: ComponentWidget<string[]> | SimplifiedWidget
): widget is ComponentWidget<string[]> {
return 'node' in widget && widget.node !== undefined
}
const node = widget.node
const type = inputSpec.type as Load3DNodeType
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 backgroundColor = ref('#000000')
const showGrid = ref(true)
const showPreview = ref(false)
const lightIntensity = ref(5)
const showLightIntensityButton = ref(true)
const fov = ref(75)
const showFOVButton = ref(true)
const cameraType = ref<CameraType>('perspective')
const hasBackgroundImage = ref(false)
const backgroundImage = ref('')
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 {
// configs
sceneConfig,
modelConfig,
cameraConfig,
lightConfig,
// other state
isRecording,
isPreview,
hasRecording,
recordingDuration,
animations,
playing,
selectedSpeed,
selectedAnimation,
loading,
loadingMessage,
// Methods
initializeLoad3d,
handleMouseEnter,
handleMouseLeave,
handleStartRecording,
handleStopRecording,
handleExportRecording,
handleClearRecording,
handleBackgroundImageUpdate,
handleExportModel,
handleModelDrop,
cleanup
} = useLoad3d(node as Ref<LGraphNode | null>)
const enable3DViewer = computed(() =>
useSettingStore().get('Comfy.Load3D.3DViewerEnable')
)
const showPreviewButton = computed(() => {
return !type.includes('Preview')
})
const handleMouseEnter = () => {
if (load3DSceneRef.value?.load3d) {
load3DSceneRef.value.load3d.updateStatusMouseOnScene(true)
}
}
const handleMouseLeave = () => {
if (load3DSceneRef.value?.load3d) {
load3DSceneRef.value.load3d.updateStatusMouseOnScene(false)
}
}
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
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
hasRecording.value = recordingDuration.value > 0
}
}
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'
showFOVButton.value = cameraType.value === 'perspective'
node.properties['Camera Type'] = cameraType.value
}
const togglePreview = (value: boolean) => {
showPreview.value = value
node.properties['Show Preview'] = showPreview.value
}
const toggleGrid = (value: boolean) => {
showGrid.value = value
node.properties['Show Grid'] = showGrid.value
}
const handleUpdateLightIntensity = (value: number) => {
lightIntensity.value = value
node.properties['Light Intensity'] = lightIntensity.value
}
const handleBackgroundImageUpdate = async (file: File | null) => {
if (!file) {
hasBackgroundImage.value = false
backgroundImage.value = ''
node.properties['Background Image'] = ''
return
}
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
node.properties['Background Image'] = backgroundImage.value
}
const handleUpdateFOV = (value: number) => {
fov.value = value
node.properties['FOV'] = fov.value
}
const handleUpdateEdgeThreshold = (value: number) => {
edgeThreshold.value = value
node.properties['Edge Threshold'] = edgeThreshold.value
}
const handleBackgroundColorChange = (value: string) => {
backgroundColor.value = value
node.properties['Background Color'] = value
}
const handleUpdateUpDirection = (value: UpDirection) => {
upDirection.value = value
node.properties['Up Direction'] = value
}
const handleUpdateMaterialMode = (value: MaterialMode) => {
materialMode.value = value
node.properties['Material Mode'] = value
}
const handleExportModel = async (format: string) => {
if (!load3DSceneRef.value?.load3d) {
useToastStore().addAlert(t('toastMessages.no3dSceneToExport'))
return
}
try {
await load3DSceneRef.value.load3d.exportModel(format)
} catch (error) {
console.error('Error exporting model:', error)
useToastStore().addAlert(
t('toastMessages.failedToExportModel', {
format: format.toUpperCase()
})
)
}
}
const listenMaterialModeChange = (mode: MaterialMode) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenUpDirectionChange = (value: UpDirection) => {
upDirection.value = value
}
const listenEdgeThresholdChange = (value: number) => {
edgeThreshold.value = value
}
const listenRecordingStatusChange = (value: boolean) => {
isRecording.value = value
if (!value && load3DSceneRef.value?.load3d) {
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
hasRecording.value = recordingDuration.value > 0
}
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: CameraType) => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenBackgroundImageChange = (value: string) => {
backgroundImage.value = value
if (backgroundImage.value && backgroundImage.value !== '') {
hasBackgroundImage.value = true
}
}
</script>

View File

@@ -1,336 +0,0 @@
<template>
<div
class="relative h-full w-full"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<Load3DAnimationScene
ref="load3DAnimationSceneRef"
:node="node"
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:light-intensity="lightIntensity"
:fov="fov"
:camera-type="cameraType"
:show-preview="showPreview"
:show-f-o-v-button="showFOVButton"
:show-light-intensity-button="showLightIntensityButton"
:playing="playing"
:selected-speed="selectedSpeed"
:selected-animation="selectedAnimation"
:background-image="backgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
@material-mode-change="listenMaterialModeChange"
@background-color-change="listenBackgroundColorChange"
@light-intensity-change="listenLightIntensityChange"
@fov-change="listenFOVChange"
@camera-type-change="listenCameraTypeChange"
@show-grid-change="listenShowGridChange"
@show-preview-change="listenShowPreviewChange"
@background-image-change="listenBackgroundImageChange"
@animation-list-change="animationListChange"
@up-direction-change="listenUpDirectionChange"
@recording-status-change="listenRecordingStatusChange"
/>
<div class="pointer-events-none absolute top-0 left-0 h-full w-full">
<Load3DControls
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:show-preview="showPreview"
:light-intensity="lightIntensity"
:show-light-intensity-button="showLightIntensityButton"
:fov="fov"
:show-f-o-v-button="showFOVButton"
:show-preview-button="showPreviewButton"
:camera-type="cameraType"
:has-background-image="hasBackgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
@update-background-image="handleBackgroundImageUpdate"
@switch-camera="switchCamera"
@toggle-grid="toggleGrid"
@update-background-color="handleBackgroundColorChange"
@update-light-intensity="handleUpdateLightIntensity"
@toggle-preview="togglePreview"
@update-f-o-v="handleUpdateFOV"
@update-up-direction="handleUpdateUpDirection"
@update-material-mode="handleUpdateMaterialMode"
/>
<Load3DAnimationControls
:animations="animations"
:playing="playing"
@toggle-play="togglePlay"
@speed-change="speedChange"
@animation-change="animationChange"
/>
</div>
<div
v-if="showRecordingControls"
class="pointer-events-auto absolute top-12 right-2 z-20"
>
<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>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
import Load3DAnimationScene from '@/components/load3d/Load3DAnimationScene.vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
AnimationItem,
CameraType,
Load3DAnimationNodeType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComponentWidget } from '@/scripts/domWidget'
const { widget } = defineProps<{
widget: ComponentWidget<string[]>
}>()
const inputSpec = widget.inputSpec as CustomInputSpec
const node = widget.node
const type = inputSpec.type as Load3DAnimationNodeType
const backgroundColor = ref('#000000')
const showGrid = ref(true)
const showPreview = ref(false)
const lightIntensity = ref(5)
const showLightIntensityButton = ref(true)
const fov = ref(75)
const showFOVButton = ref(true)
const cameraType = ref<'perspective' | 'orthographic'>('perspective')
const hasBackgroundImage = ref(false)
const animations = ref<AnimationItem[]>([])
const playing = ref(false)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const backgroundImage = ref('')
const isRecording = ref(false)
const hasRecording = ref(false)
const recordingDuration = ref(0)
const showRecordingControls = ref(!inputSpec.isPreview)
const showPreviewButton = computed(() => {
return !type.includes('Preview')
})
const load3DAnimationSceneRef = ref<InstanceType<
typeof Load3DAnimationScene
> | null>(null)
const handleMouseEnter = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.updateStatusMouseOnScene(true)
}
}
const handleMouseLeave = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.updateStatusMouseOnScene(false)
}
}
const handleStartRecording = async () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
await sceneRef.load3d.startRecording()
isRecording.value = true
}
}
const handleStopRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.stopRecording()
isRecording.value = false
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
hasRecording.value = recordingDuration.value > 0
}
}
const handleExportRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const filename = `${timestamp}-animation-recording.mp4`
sceneRef.load3d.exportRecording(filename)
}
}
const handleClearRecording = () => {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
sceneRef.load3d.clearRecording()
hasRecording.value = false
recordingDuration.value = 0
}
}
const listenRecordingStatusChange = (value: boolean) => {
isRecording.value = value
if (!value) {
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
if (sceneRef?.load3d) {
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
hasRecording.value = recordingDuration.value > 0
}
}
}
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
showFOVButton.value = cameraType.value === 'perspective'
node.properties['Camera Type'] = cameraType.value
}
const togglePreview = (value: boolean) => {
showPreview.value = value
node.properties['Show Preview'] = showPreview.value
}
const toggleGrid = (value: boolean) => {
showGrid.value = value
node.properties['Show Grid'] = showGrid.value
}
const handleUpdateLightIntensity = (value: number) => {
lightIntensity.value = value
node.properties['Light Intensity'] = lightIntensity.value
}
const handleBackgroundImageUpdate = async (file: File | null) => {
if (!file) {
hasBackgroundImage.value = false
backgroundImage.value = ''
node.properties['Background Image'] = ''
return
}
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
node.properties['Background Image'] = backgroundImage.value
}
const handleUpdateFOV = (value: number) => {
fov.value = value
node.properties['FOV'] = fov.value
}
const materialMode = ref<MaterialMode>('original')
const upDirection = ref<UpDirection>('original')
const handleUpdateUpDirection = (value: UpDirection) => {
upDirection.value = value
node.properties['Up Direction'] = value
}
const handleUpdateMaterialMode = (value: MaterialMode) => {
materialMode.value = value
node.properties['Material Mode'] = value
}
const handleBackgroundColorChange = (value: string) => {
backgroundColor.value = value
node.properties['Background Color'] = value
}
const togglePlay = (value: boolean) => {
playing.value = value
}
const speedChange = (value: number) => {
selectedSpeed.value = value
}
const animationChange = (value: number) => {
selectedAnimation.value = value
}
const animationListChange = (value: any) => {
animations.value = value
}
const listenMaterialModeChange = (mode: MaterialMode) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenUpDirectionChange = (value: UpDirection) => {
upDirection.value = value
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: CameraType) => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenBackgroundImageChange = (value: string) => {
backgroundImage.value = value
if (backgroundImage.value && backgroundImage.value !== '') {
hasBackgroundImage.value = true
}
}
</script>

View File

@@ -1,208 +0,0 @@
<template>
<Load3DScene
ref="load3DSceneRef"
:node="node"
:input-spec="inputSpec"
:background-color="backgroundColor"
:show-grid="showGrid"
:light-intensity="lightIntensity"
:fov="fov"
:camera-type="cameraType"
:show-preview="showPreview"
:extra-listeners="animationListeners"
:background-image="backgroundImage"
:up-direction="upDirection"
:material-mode="materialMode"
@material-mode-change="listenMaterialModeChange"
@background-color-change="listenBackgroundColorChange"
@light-intensity-change="listenLightIntensityChange"
@fov-change="listenFOVChange"
@camera-type-change="listenCameraTypeChange"
@show-grid-change="listenShowGridChange"
@show-preview-change="listenShowPreviewChange"
@recording-status-change="listenRecordingStatusChange"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
import type Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import type {
CameraType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const props = defineProps<{
node: any
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
lightIntensity: number
fov: number
cameraType: CameraType
showPreview: boolean
materialMode: MaterialMode
upDirection: UpDirection
showFOVButton: boolean
showLightIntensityButton: boolean
playing: boolean
selectedSpeed: number
selectedAnimation: number
backgroundImage: string
}>()
const node = ref(props.node)
const backgroundColor = ref(props.backgroundColor)
const showPreview = ref(props.showPreview)
const fov = ref(props.fov)
const lightIntensity = ref(props.lightIntensity)
const cameraType = ref(props.cameraType)
const showGrid = ref(props.showGrid)
const upDirection = ref(props.upDirection)
const materialMode = ref(props.materialMode)
const showFOVButton = ref(props.showFOVButton)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
watch(
() => props.cameraType,
(newValue) => {
cameraType.value = newValue
}
)
watch(
() => props.showGrid,
(newValue) => {
showGrid.value = newValue
}
)
watch(
() => props.backgroundColor,
(newValue) => {
backgroundColor.value = newValue
}
)
watch(
() => props.lightIntensity,
(newValue) => {
lightIntensity.value = newValue
}
)
watch(
() => props.fov,
(newValue) => {
fov.value = newValue
}
)
watch(
() => props.upDirection,
(newValue) => {
upDirection.value = newValue
}
)
watch(
() => props.materialMode,
(newValue) => {
materialMode.value = newValue
}
)
watch(
() => props.showPreview,
(newValue) => {
showPreview.value = newValue
}
)
watch(
() => props.playing,
(newValue) => {
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
load3d?.toggleAnimation(newValue)
}
)
watch(
() => props.selectedSpeed,
(newValue) => {
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
load3d?.setAnimationSpeed(newValue)
}
)
watch(
() => props.selectedAnimation,
(newValue) => {
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
load3d?.updateSelectedAnimation(newValue)
}
)
const emit = defineEmits<{
(e: 'animationListChange', animationList: string): void
(e: 'materialModeChange', materialMode: MaterialMode): void
(e: 'backgroundColorChange', color: string): void
(e: 'lightIntensityChange', lightIntensity: number): void
(e: 'fovChange', fov: number): void
(e: 'cameraTypeChange', cameraType: CameraType): void
(e: 'showGridChange', showGrid: boolean): void
(e: 'showPreviewChange', showPreview: boolean): void
(e: 'upDirectionChange', direction: UpDirection): void
(e: 'recording-status-change', status: boolean): void
}>()
const listenMaterialModeChange = (mode: MaterialMode) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: CameraType) => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenRecordingStatusChange = (value: boolean) => {
emit('recording-status-change', value)
}
const animationListeners = {
animationListChange: (newValue: any) => {
emit('animationListChange', newValue)
}
}
defineExpose({
load3DSceneRef
})
</script>

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)
})

View File

@@ -1,238 +1,72 @@
<template>
<div ref="container" class="relative h-full w-full">
<LoadingOverlay ref="loadingOverlayRef" />
<div
ref="container"
class="relative h-full w-full"
data-capture-wheel="true"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
@mousedown.stop
@mousemove.stop
@mouseup.stop
@contextmenu.stop.prevent
@dragover.prevent.stop="handleDragOver"
@dragleave.stop="handleDragLeave"
@drop.prevent.stop="handleDrop"
>
<LoadingOverlay
ref="loadingOverlayRef"
:loading="loading"
:loading-message="loadingMessage"
/>
<div
v-if="!isPreview && isDragging"
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
>
<div
class="rounded-lg border-2 border-dashed border-blue-400 bg-blue-500/20 px-6 py-4 text-lg font-medium text-blue-100"
>
{{ dragMessage }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, toRaw, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
import type Load3d from '@/extensions/core/load3d/Load3d'
import type Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import type {
CameraType,
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { useLoad3dService } from '@/services/load3dService'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
const props = defineProps<{
node: LGraphNode
inputSpec: CustomInputSpec
backgroundColor: string
showGrid: boolean
lightIntensity: number
fov: number
cameraType: CameraType
showPreview: boolean
backgroundImage: string
upDirection: UpDirection
materialMode: MaterialMode
edgeThreshold?: number
extraListeners?: Record<string, (value: any) => void>
initializeLoad3d: (containerRef: HTMLElement) => Promise<void>
cleanup: () => void
loading: boolean
loadingMessage: string
onModelDrop?: (file: File) => void | Promise<void>
isPreview: boolean
}>()
const container = ref<HTMLElement | null>(null)
const node = ref(props.node)
const load3d = ref<Load3d | Load3dAnimation | null>(null)
const loadingOverlayRef = ref<InstanceType<typeof LoadingOverlay> | null>(null)
const eventConfig = {
materialModeChange: (value: string) =>
emit('materialModeChange', value as MaterialMode),
backgroundColorChange: (value: string) =>
emit('backgroundColorChange', value),
lightIntensityChange: (value: number) => emit('lightIntensityChange', value),
fovChange: (value: number) => emit('fovChange', value),
cameraTypeChange: (value: string) =>
emit('cameraTypeChange', value as CameraType),
showGridChange: (value: boolean) => emit('showGridChange', value),
showPreviewChange: (value: boolean) => emit('showPreviewChange', value),
backgroundImageChange: (value: string) =>
emit('backgroundImageChange', value),
backgroundImageLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.loadingBackgroundImage')),
backgroundImageLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
upDirectionChange: (value: string) =>
emit('upDirectionChange', value as UpDirection),
edgeThresholdChange: (value: number) => emit('edgeThresholdChange', value),
modelLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.loadingModel')),
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
materialLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.switchingMaterialMode')),
materialLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
exportLoadingStart: (message: string) => {
loadingOverlayRef.value?.startLoading(message || t('load3d.exportingModel'))
},
exportLoadingEnd: () => {
loadingOverlayRef.value?.endLoading()
},
recordingStatusChange: (value: boolean) =>
emit('recordingStatusChange', value)
} as const
watch(
() => props.showPreview,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.togglePreview(newValue)
}
}
)
watch(
() => props.cameraType,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.toggleCamera(newValue)
}
}
)
watch(
() => props.fov,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setFOV(newValue)
}
}
)
watch(
() => props.lightIntensity,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setLightIntensity(newValue)
}
}
)
watch(
() => props.showGrid,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.toggleGrid(newValue)
}
}
)
watch(
() => props.backgroundColor,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setBackgroundColor(newValue)
}
}
)
watch(
() => props.backgroundImage,
async (newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
await rawLoad3d.setBackgroundImage(newValue)
}
}
)
watch(
() => props.upDirection,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setUpDirection(newValue)
}
}
)
watch(
() => props.materialMode,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setMaterialMode(newValue)
}
}
)
watch(
() => props.edgeThreshold,
(newValue) => {
if (load3d.value && newValue) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setEdgeThreshold(newValue)
}
}
)
const emit = defineEmits<{
(e: 'materialModeChange', materialMode: MaterialMode): void
(e: 'backgroundColorChange', color: string): void
(e: 'lightIntensityChange', lightIntensity: number): void
(e: 'fovChange', fov: number): void
(e: 'cameraTypeChange', cameraType: CameraType): void
(e: 'showGridChange', showGrid: boolean): void
(e: 'showPreviewChange', showPreview: boolean): void
(e: 'backgroundImageChange', backgroundImage: string): void
(e: 'upDirectionChange', upDirection: UpDirection): void
(e: 'edgeThresholdChange', threshold: number): void
(e: 'recordingStatusChange', status: boolean): void
}>()
const handleEvents = (action: 'add' | 'remove') => {
if (!load3d.value) return
Object.entries(eventConfig).forEach(([event, handler]) => {
const method = `${action}EventListener` as const
load3d.value?.[method](event, handler)
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
useLoad3dDrag({
onModelDrop: async (file) => {
if (props.onModelDrop) {
await props.onModelDrop(file)
}
},
disabled: computed(() => props.isPreview)
})
if (props.extraListeners) {
Object.entries(props.extraListeners).forEach(([event, handler]) => {
const method = `${action}EventListener` as const
load3d.value?.[method](event, handler)
})
}
}
onMounted(() => {
if (container.value) {
load3d.value = useLoad3dService().registerLoad3d(
node.value as LGraphNode,
container.value,
props.inputSpec
)
void props.initializeLoad3d(container.value)
}
handleEvents('add')
})
onUnmounted(() => {
handleEvents('remove')
useLoad3dService().removeLoad3d(node.value as LGraphNode)
})
defineExpose({
load3d
props.cleanup()
})
</script>

View File

@@ -11,7 +11,20 @@
ref="containerRef"
class="absolute h-full w-full"
@resize="viewer.handleResize"
@dragover.prevent.stop="handleDragOver"
@dragleave.stop="handleDragLeave"
@drop.prevent.stop="handleDrop"
/>
<div
v-if="isDragging"
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
>
<div
class="rounded-lg border-2 border-dashed border-blue-400 bg-blue-500/20 px-6 py-4 text-lg font-medium text-blue-100"
>
{{ dragMessage }}
</div>
</div>
</div>
<div class="flex w-72 flex-col">
@@ -75,6 +88,7 @@ import ExportControls from '@/components/load3d/controls/viewer/ViewerExportCont
import LightControls from '@/components/load3d/controls/viewer/ViewerLightControls.vue'
import ModelControls from '@/components/load3d/controls/viewer/ViewerModelControls.vue'
import SceneControls from '@/components/load3d/controls/viewer/ViewerSceneControls.vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useLoad3dService } from '@/services/load3dService'
@@ -92,6 +106,14 @@ const mutationObserver = ref<MutationObserver | null>(null)
const viewer = useLoad3dService().getOrCreateViewer(toRaw(props.node))
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
useLoad3dDrag({
onModelDrop: async (file) => {
await viewer.handleModelDrop(file)
},
disabled: viewer.isPreview
})
onMounted(async () => {
const source = useLoad3dService().getLoad3d(props.node)
if (source && containerRef.value) {

View File

@@ -1,8 +1,8 @@
<template>
<Transition name="fade">
<div
v-if="modelLoading"
class="absolute inset-0 z-50 flex items-center justify-center bg-black/50"
v-if="loading"
class="bg-opacity-50 absolute inset-0 z-50 flex items-center justify-center bg-black"
>
<div class="flex flex-col items-center">
<div class="spinner" />
@@ -15,29 +15,10 @@
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { t } from '@/i18n'
const modelLoading = ref(false)
const loadingMessage = ref('')
const startLoading = async (message?: string) => {
loadingMessage.value = message || t('load3d.loadingModel')
modelLoading.value = true
await nextTick()
}
const endLoading = async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
modelLoading.value = false
}
defineExpose({
startLoading,
endLoading
})
defineProps<{
loading: boolean
loadingMessage: string
}>()
</script>
<style scoped>

View File

@@ -15,7 +15,6 @@
option-label="name"
option-value="value"
class="w-24"
@change="speedChange"
/>
<Select
@@ -24,7 +23,6 @@
option-label="name"
option-value="index"
class="w-32"
@change="animationChange"
/>
</div>
</template>
@@ -32,23 +30,13 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Select from 'primevue/select'
import { ref, watch } from 'vue'
const props = defineProps<{
animations: Array<{ name: string; index: number }>
playing: boolean
}>()
type Animation = { name: string; index: number }
const emit = defineEmits<{
(e: 'togglePlay', value: boolean): void
(e: 'speedChange', value: number): void
(e: 'animationChange', value: number): void
}>()
const animations = ref(props.animations)
const playing = ref(props.playing)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const animations = defineModel<Animation[]>('animations')
const playing = defineModel<boolean>('playing')
const selectedSpeed = defineModel<number>('selectedSpeed')
const selectedAnimation = defineModel<number>('selectedAnimation')
const speedOptions = [
{ name: '0.1x', value: 0.1 },
@@ -58,24 +46,7 @@ const speedOptions = [
{ name: '2x', value: 2 }
]
watch(
() => props.animations,
(newVal) => {
animations.value = newVal
}
)
const togglePlay = () => {
playing.value = !playing.value
emit('togglePlay', playing.value)
}
const speedChange = () => {
emit('speedChange', selectedSpeed.value)
}
const animationChange = () => {
emit('animationChange', selectedAnimation.value)
}
</script>

View File

@@ -3,7 +3,7 @@
<Button class="p-button-rounded p-button-text" @click="switchCamera">
<i
v-tooltip.right="{
value: t('load3d.switchCamera'),
value: $t('load3d.switchCamera'),
showDelay: 300
}"
:class="['pi', getCameraIcon, 'text-lg text-white']"
@@ -12,7 +12,7 @@
<div v-if="showFOVButton" class="show-fov relative">
<Button class="p-button-rounded p-button-text" @click="toggleFOV">
<i
v-tooltip.right="{ value: t('load3d.fov'), showDelay: 300 }"
v-tooltip.right="{ value: $t('load3d.fov'), showDelay: 300 }"
class="pi pi-expand text-lg text-white"
/>
</Button>
@@ -21,83 +21,37 @@
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
style="width: 150px"
>
<Slider
v-model="fov"
class="w-full"
:min="10"
:max="150"
:step="1"
@change="updateFOV"
/>
<Slider v-model="fov" class="w-full" :min="10" :max="150" :step="1" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import Slider from 'primevue/slider'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import type { CameraType } from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
const vTooltip = Tooltip
const props = defineProps<{
cameraType: CameraType
fov: number
showFOVButton: boolean
}>()
const emit = defineEmits<{
(e: 'switchCamera'): void
(e: 'updateFOV', value: number): void
}>()
const cameraType = ref(props.cameraType)
const fov = ref(props.fov)
const showFOVButton = ref(props.showFOVButton)
const showFOV = ref(false)
watch(
() => props.fov,
(newValue) => {
fov.value = newValue
}
)
watch(
() => props.showFOVButton,
(newValue) => {
showFOVButton.value = newValue
}
)
watch(
() => props.cameraType,
(newValue) => {
cameraType.value = newValue
}
)
const switchCamera = () => {
emit('switchCamera')
}
const cameraType = defineModel<CameraType>('cameraType')
const fov = defineModel<number>('fov')
const showFOVButton = computed(() => cameraType.value === 'perspective')
const getCameraIcon = computed(() => {
return cameraType.value === 'perspective' ? 'pi-camera' : 'pi-camera'
})
const toggleFOV = () => {
showFOV.value = !showFOV.value
}
const updateFOV = () => {
emit('updateFOV', fov.value)
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
}
const getCameraIcon = computed(() => {
return props.cameraType === 'perspective' ? 'pi-camera' : 'pi-camera'
})
const closeCameraSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement

View File

@@ -7,7 +7,7 @@
>
<i
v-tooltip.right="{
value: t('load3d.exportModel'),
value: $t('load3d.exportModel'),
showDelay: 300
}"
class="pi pi-download text-lg text-white"
@@ -33,14 +33,9 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { onMounted, onUnmounted, ref } from 'vue'
import { t } from '@/i18n'
const vTooltip = Tooltip
const emit = defineEmits<{
(e: 'exportModel', format: string): void
}>()

View File

@@ -7,7 +7,7 @@
>
<i
v-tooltip.right="{
value: t('load3d.lightIntensity'),
value: $t('load3d.lightIntensity'),
showDelay: 300
}"
class="pi pi-sun text-lg text-white"
@@ -24,7 +24,6 @@
:min="lightIntensityMinimum"
:max="lightIntensityMaximum"
:step="lightAdjustmentIncrement"
@change="updateLightIntensity"
/>
</div>
</div>
@@ -32,27 +31,19 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import Slider from 'primevue/slider'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { t } from '@/i18n'
import type { MaterialMode } from '@/extensions/core/load3d/interfaces'
import { useSettingStore } from '@/platform/settings/settingStore'
const vTooltip = Tooltip
const lightIntensity = defineModel<number>('lightIntensity')
const materialMode = defineModel<MaterialMode>('materialMode')
const props = defineProps<{
lightIntensity: number
showLightIntensityButton: boolean
}>()
const emit = defineEmits<{
(e: 'updateLightIntensity', value: number): void
}>()
const lightIntensity = ref(props.lightIntensity)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const showLightIntensityButton = computed(
() => materialMode.value === 'original'
)
const showLightIntensity = ref(false)
const lightIntensityMaximum = useSettingStore().get(
@@ -65,28 +56,10 @@ const lightAdjustmentIncrement = useSettingStore().get(
'Comfy.Load3D.LightAdjustmentIncrement'
)
watch(
() => props.lightIntensity,
(newValue) => {
lightIntensity.value = newValue
}
)
watch(
() => props.showLightIntensityButton,
(newValue) => {
showLightIntensityButton.value = newValue
}
)
const toggleLightIntensity = () => {
showLightIntensity.value = !showLightIntensity.value
}
const updateLightIntensity = () => {
emit('updateLightIntensity', lightIntensity.value)
}
const closeLightSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement

View File

@@ -22,7 +22,7 @@
:class="{ 'bg-blue-500': upDirection === direction }"
@click="selectUpDirection(direction)"
>
{{ formatOption(direction) }}
{{ direction.toUpperCase() }}
</Button>
</div>
</div>
@@ -49,7 +49,7 @@
<Button
v-for="mode in materialModes"
:key="mode"
class="p-button-text text-white"
class="p-button-text whitespace-nowrap text-white"
:class="{ 'bg-blue-500': materialMode === mode }"
@click="selectMaterialMode(mode)"
>
@@ -58,75 +58,24 @@
</div>
</div>
</div>
<div v-if="materialMode === 'lineart'" class="show-edge-threshold relative">
<Button
class="p-button-rounded p-button-text"
@click="toggleEdgeThreshold"
>
<i
v-tooltip.right="{
value: t('load3d.edgeThreshold'),
showDelay: 300
}"
class="pi pi-sliders-h text-lg text-white"
/>
</Button>
<div
v-show="showEdgeThreshold"
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
style="width: 150px"
>
<label class="mb-1 block text-xs text-white"
>{{ t('load3d.edgeThreshold') }}: {{ edgeThreshold }}°</label
>
<Slider
v-model="edgeThreshold"
class="w-full"
:min="0"
:max="120"
:step="1"
@change="updateEdgeThreshold"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import Slider from 'primevue/slider'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import type {
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
const vTooltip = Tooltip
const materialMode = defineModel<MaterialMode>('materialMode')
const upDirection = defineModel<UpDirection>('upDirection')
const props = defineProps<{
inputSpec: CustomInputSpec
upDirection: UpDirection
materialMode: MaterialMode
edgeThreshold?: number
}>()
const emit = defineEmits<{
(e: 'updateUpDirection', direction: UpDirection): void
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
}>()
const upDirection = ref(props.upDirection || 'original')
const materialMode = ref(props.materialMode || 'original')
const edgeThreshold = ref(props.edgeThreshold || 85)
const showUpDirection = ref(false)
const showMaterialMode = ref(false)
const showEdgeThreshold = ref(false)
const upDirections: UpDirection[] = [
'original',
@@ -146,65 +95,26 @@ const materialModes = computed(() => {
//'depth' disable for now
]
if (!props.inputSpec.isAnimation && !props.inputSpec.isPreview) {
modes.push('lineart')
}
return modes
})
watch(
() => props.upDirection,
(newValue) => {
if (newValue) {
upDirection.value = newValue
}
}
)
watch(
() => props.materialMode,
(newValue) => {
if (newValue) {
materialMode.value = newValue
}
}
)
watch(
() => props.edgeThreshold,
(newValue) => {
// @ts-expect-error fixme ts strict error
edgeThreshold.value = newValue
}
)
const toggleUpDirection = () => {
showUpDirection.value = !showUpDirection.value
showMaterialMode.value = false
showEdgeThreshold.value = false
}
const selectUpDirection = (direction: UpDirection) => {
upDirection.value = direction
emit('updateUpDirection', direction)
showUpDirection.value = false
}
const formatOption = (option: string) => {
if (option === 'original') return 'Original'
return option.toUpperCase()
}
const toggleMaterialMode = () => {
showMaterialMode.value = !showMaterialMode.value
showUpDirection.value = false
showEdgeThreshold.value = false
}
const selectMaterialMode = (mode: MaterialMode) => {
materialMode.value = mode
emit('updateMaterialMode', mode)
showMaterialMode.value = false
}
@@ -212,16 +122,6 @@ const formatMaterialMode = (mode: MaterialMode) => {
return t(`load3d.materialModes.${mode}`)
}
const toggleEdgeThreshold = () => {
showEdgeThreshold.value = !showEdgeThreshold.value
showUpDirection.value = false
showMaterialMode.value = false
}
const updateEdgeThreshold = () => {
emit('updateEdgeThreshold', edgeThreshold.value)
}
const closeSceneSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement
@@ -232,10 +132,6 @@ const closeSceneSlider = (e: MouseEvent) => {
if (!target.closest('.show-material-mode')) {
showMaterialMode.value = false
}
if (!target.closest('.show-edge-threshold')) {
showEdgeThreshold.value = false
}
}
onMounted(() => {

View File

@@ -1,18 +1,6 @@
<template>
<div class="relative rounded-lg bg-smoke-700/30">
<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-lg text-white"
/>
</Button>
<Button
class="p-button-rounded p-button-text"
:class="{
@@ -24,8 +12,8 @@
<i
v-tooltip.right="{
value: isRecording
? t('load3d.stopRecording')
: t('load3d.startRecording'),
? $t('load3d.stopRecording')
: $t('load3d.startRecording'),
showDelay: 300
}"
:class="[
@@ -39,11 +27,11 @@
<Button
v-if="hasRecording && !isRecording"
class="p-button-rounded p-button-text"
@click="exportRecording"
@click="handleExportRecording"
>
<i
v-tooltip.right="{
value: t('load3d.exportRecording'),
value: $t('load3d.exportRecording'),
showDelay: 300
}"
class="pi pi-download text-lg text-white"
@@ -53,11 +41,11 @@
<Button
v-if="hasRecording && !isRecording"
class="p-button-rounded p-button-text"
@click="clearRecording"
@click="handleClearRecording"
>
<i
v-tooltip.right="{
value: t('load3d.clearRecording'),
value: $t('load3d.clearRecording'),
showDelay: 300
}"
class="pi pi-trash text-lg text-white"
@@ -65,7 +53,7 @@
</Button>
<div
v-if="recordingDuration > 0 && !isRecording"
v-if="recordingDuration && recordingDuration > 0 && !isRecording"
class="mt-1 text-center text-xs text-white"
>
{{ formatDuration(recordingDuration) }}
@@ -75,21 +63,11 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useLoad3dService } from '@/services/load3dService'
const vTooltip = Tooltip
const { hasRecording, isRecording, node, recordingDuration } = defineProps<{
hasRecording: boolean
isRecording: boolean
node: LGraphNode
recordingDuration: number
}>()
const hasRecording = defineModel<boolean>('hasRecording')
const isRecording = defineModel<boolean>('isRecording')
const recordingDuration = defineModel<number>('recordingDuration')
const emit = defineEmits<{
(e: 'startRecording'): void
@@ -98,49 +76,19 @@ const emit = defineEmits<{
(e: 'clearRecording'): void
}>()
const resizeNodeMatchOutput = () => {
const outputWidth = node.widgets?.find((w) => w.name === 'width')
const outputHeight = node.widgets?.find((w) => w.name === 'height')
if (outputWidth && outputHeight && outputHeight.value && outputWidth.value) {
const [oldWidth, oldHeight] = node.size
const scene = node.widgets?.find((w) => 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.setSize([oldWidth, oldHeight + (expectSceneHeight - sceneHeight)])
node.graph?.setDirtyCanvas(true, true)
const load3d = useLoad3dService().getLoad3d(node as LGraphNode)
if (load3d) {
load3d.refreshViewport()
}
}
}
}
const toggleRecording = () => {
if (isRecording) {
if (isRecording.value) {
emit('stopRecording')
} else {
emit('startRecording')
}
}
const exportRecording = () => {
const handleExportRecording = () => {
emit('exportRecording')
}
const clearRecording = () => {
const handleClearRecording = () => {
emit('clearRecording')
}

View File

@@ -6,7 +6,7 @@
@click="toggleGrid"
>
<i
v-tooltip.right="{ value: t('load3d.showGrid'), showDelay: 300 }"
v-tooltip.right="{ value: $t('load3d.showGrid'), showDelay: 300 }"
class="pi pi-table text-lg text-white"
/>
</Button>
@@ -15,7 +15,7 @@
<Button class="p-button-rounded p-button-text" @click="openColorPicker">
<i
v-tooltip.right="{
value: t('load3d.backgroundColor'),
value: $t('load3d.backgroundColor'),
showDelay: 300
}"
class="pi pi-palette text-lg text-white"
@@ -36,7 +36,7 @@
<Button class="p-button-rounded p-button-text" @click="openImagePicker">
<i
v-tooltip.right="{
value: t('load3d.uploadBackgroundImage'),
value: $t('load3d.uploadBackgroundImage'),
showDelay: 300
}"
class="pi pi-image text-lg text-white"
@@ -58,7 +58,7 @@
>
<i
v-tooltip.right="{
value: t('load3d.removeBackgroundImage'),
value: $t('load3d.removeBackgroundImage'),
showDelay: 300
}"
class="pi pi-times text-lg text-white"
@@ -69,60 +69,29 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import { ref, watch } from 'vue'
import { t } from '@/i18n'
const vTooltip = Tooltip
const props = defineProps<{
backgroundColor: string
showGrid: boolean
hasBackgroundImage?: boolean
}>()
import { computed, ref } from 'vue'
const emit = defineEmits<{
(e: 'toggleGrid', value: boolean): void
(e: 'updateBackgroundColor', color: string): void
(e: 'updateBackgroundImage', file: File | null): void
}>()
const backgroundColor = ref(props.backgroundColor)
const showGrid = ref(props.showGrid)
const hasBackgroundImage = ref(props.hasBackgroundImage)
const showGrid = defineModel<boolean>('showGrid')
const backgroundColor = defineModel<string>('backgroundColor')
const backgroundImage = defineModel<string>('backgroundImage')
const hasBackgroundImage = computed(
() => backgroundImage.value && backgroundImage.value !== ''
)
const colorPickerRef = ref<HTMLInputElement | null>(null)
const imagePickerRef = ref<HTMLInputElement | null>(null)
watch(
() => props.backgroundColor,
(newValue) => {
backgroundColor.value = newValue
}
)
watch(
() => props.showGrid,
(newValue) => {
showGrid.value = newValue
}
)
watch(
() => props.hasBackgroundImage,
(newValue) => {
hasBackgroundImage.value = newValue
}
)
const toggleGrid = () => {
showGrid.value = !showGrid.value
emit('toggleGrid', showGrid.value)
}
const updateBackgroundColor = (color: string) => {
emit('updateBackgroundColor', color)
backgroundColor.value = color
}
const openColorPicker = () => {

View File

@@ -15,7 +15,6 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
@@ -24,8 +23,6 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useLoad3dService } from '@/services/load3dService'
import { useDialogStore } from '@/stores/dialogStore'
const vTooltip = Tooltip
const { node } = defineProps<{
node: LGraphNode
}>()

View File

@@ -8,7 +8,7 @@
</Select>
<Button severity="secondary" text rounded @click="exportModel(exportFormat)">
{{ t('load3d.export') }}
{{ $t('load3d.export') }}
</Button>
</template>
@@ -17,8 +17,6 @@ import Button from 'primevue/button'
import Select from 'primevue/select'
import { ref } from 'vue'
import { t } from '@/i18n'
const emit = defineEmits<{
(e: 'exportModel', format: string): void
}>()

View File

@@ -1,5 +1,5 @@
<template>
<label>{{ t('load3d.lightIntensity') }}</label>
<label>{{ $t('load3d.lightIntensity') }}</label>
<Slider
v-model="lightIntensity"
@@ -13,7 +13,6 @@
<script setup lang="ts">
import Slider from 'primevue/slider'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
const lightIntensity = defineModel<number>('lightIntensity')

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-4">
<div>
<label>{{ t('load3d.upDirection') }}</label>
<label>{{ $t('load3d.upDirection') }}</label>
<Select
v-model="upDirection"
:options="upDirectionOptions"
@@ -11,7 +11,7 @@
</div>
<div>
<label>{{ t('load3d.materialMode') }}</label>
<label>{{ $t('load3d.materialMode') }}</label>
<Select
v-model="materialMode"
:options="materialModeOptions"

View File

@@ -2,7 +2,7 @@
<div class="space-y-4">
<div v-if="!hasBackgroundImage">
<label>
{{ t('load3d.backgroundColor') }}
{{ $t('load3d.backgroundColor') }}
</label>
<input v-model="backgroundColor" type="color" class="w-full" />
</div>
@@ -10,14 +10,14 @@
<div>
<Checkbox v-model="showGrid" input-id="showGrid" binary name="showGrid" />
<label for="showGrid" class="pl-2">
{{ t('load3d.showGrid') }}
{{ $t('load3d.showGrid') }}
</label>
</div>
<div v-if="!hasBackgroundImage">
<Button
severity="secondary"
:label="t('load3d.uploadBackgroundImage')"
:label="$t('load3d.uploadBackgroundImage')"
icon="pi pi-image"
class="w-full"
@click="openImagePicker"
@@ -34,7 +34,7 @@
<div v-if="hasBackgroundImage" class="space-y-2">
<Button
severity="secondary"
:label="t('load3d.removeBackgroundImage')"
:label="$t('load3d.removeBackgroundImage')"
icon="pi pi-times"
class="w-full"
@click="removeBackgroundImage"
@@ -48,8 +48,6 @@ import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import { ref } from 'vue'
import { t } from '@/i18n'
const backgroundColor = defineModel<string>('backgroundColor')
const showGrid = defineModel<boolean>('showGrid')