[3d] fully convert load 3d nodes into vue (#2590)

This commit is contained in:
Terry Jia
2025-02-16 20:15:49 -05:00
committed by GitHub
parent 9ebb5b2a0c
commit b2375a150c
11 changed files with 877 additions and 373 deletions

View File

@@ -0,0 +1,140 @@
<template>
<div class="relative w-full h-full">
<Load3DScene
:node="node"
:type="type"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
:fov="fov"
:cameraType="cameraType"
:showPreview="showPreview"
@materialModeChange="listenMaterialModeChange"
@backgroundColorChange="listenBackgroundColorChange"
@lightIntensityChange="listenLightIntensityChange"
@fovChange="listenFOVChange"
@cameraTypeChange="listenCameraTypeChange"
@showGridChange="listenShowGridChange"
@showPreviewChange="listenShowPreviewChange"
/>
<Load3DControls
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:showPreview="showPreview"
:lightIntensity="lightIntensity"
:showLightIntensityButton="showLightIntensityButton"
:fov="fov"
:showFOVButton="showFOVButton"
:showPreviewButton="showPreviewButton"
:cameraType="cameraType"
@switchCamera="switchCamera"
@toggleGrid="toggleGrid"
@updateBackgroundColor="handleBackgroundColorChange"
@updateLightIntensity="handleUpdateLightIntensity"
@togglePreview="togglePreview"
@updateFOV="handleUpdateFOV"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
const props = defineProps<{
node: any
type: 'Load3D' | 'Preview3D'
}>()
const node = ref(props.node)
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 showPreviewButton = computed(() => {
return !props.type.includes('Preview')
})
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
showFOVButton.value = cameraType.value === 'perspective'
node.value.properties['Camera Type'] = cameraType.value
}
const togglePreview = (value: boolean) => {
showPreview.value = value
node.value.properties['Show Preview'] = showPreview.value
}
const toggleGrid = (value: boolean) => {
showGrid.value = value
node.value.properties['Show Grid'] = showGrid.value
}
const handleUpdateLightIntensity = (value: number) => {
lightIntensity.value = value
node.value.properties['Light Intensity'] = lightIntensity.value
}
const handleUpdateFOV = (value: number) => {
fov.value = value
node.value.properties['FOV'] = fov.value
}
const materialMode = ref<'original' | 'normal' | 'wireframe' | 'depth'>(
'original'
)
const handleBackgroundColorChange = (value: string) => {
backgroundColor.value = value
node.value.properties['Background Color'] = value
}
const listenMaterialModeChange = (
mode: 'original' | 'normal' | 'wireframe' | 'depth'
) => {
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: 'perspective' | 'orthographic') => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
</script>

View File

@@ -0,0 +1,180 @@
<template>
<div class="relative w-full h-full">
<Load3DAnimationScene
:node="node"
:type="type"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
:fov="fov"
:cameraType="cameraType"
:showPreview="showPreview"
:materialMode="materialMode"
:showFOVButton="showFOVButton"
:showLightIntensityButton="showLightIntensityButton"
:playing="playing"
:selectedSpeed="selectedSpeed"
:selectedAnimation="selectedAnimation"
@materialModeChange="listenMaterialModeChange"
@backgroundColorChange="listenBackgroundColorChange"
@lightIntensityChange="listenLightIntensityChange"
@fovChange="listenFOVChange"
@cameraTypeChange="listenCameraTypeChange"
@showGridChange="listenShowGridChange"
@showPreviewChange="listenShowPreviewChange"
@animationListChange="animationListChange"
/>
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
<Load3DControls
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:showPreview="showPreview"
:lightIntensity="lightIntensity"
:showLightIntensityButton="showLightIntensityButton"
:fov="fov"
:showFOVButton="showFOVButton"
:showPreviewButton="showPreviewButton"
:cameraType="cameraType"
@switchCamera="switchCamera"
@toggleGrid="toggleGrid"
@updateBackgroundColor="handleBackgroundColorChange"
@updateLightIntensity="handleUpdateLightIntensity"
@togglePreview="togglePreview"
@updateFOV="handleUpdateFOV"
/>
<Load3DAnimationControls
:animations="animations"
:playing="playing"
@togglePlay="togglePlay"
@speedChange="speedChange"
@animationChange="animationChange"
/>
</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 type { AnimationItem } from '@/extensions/core/load3d/Load3dAnimation'
const props = defineProps<{
node: any
type: 'Load3DAnimation' | 'Preview3DAnimation'
}>()
const node = ref(props.node)
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 animations = ref<AnimationItem[]>([])
const playing = ref(false)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const showPreviewButton = computed(() => {
return !props.type.includes('Preview')
})
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
showFOVButton.value = cameraType.value === 'perspective'
node.value.properties['Camera Type'] = cameraType.value
}
const togglePreview = (value: boolean) => {
showPreview.value = value
node.value.properties['Show Preview'] = showPreview.value
}
const toggleGrid = (value: boolean) => {
showGrid.value = value
node.value.properties['Show Grid'] = showGrid.value
}
const handleUpdateLightIntensity = (value: number) => {
lightIntensity.value = value
node.value.properties['Light Intensity'] = lightIntensity.value
}
const handleUpdateFOV = (value: number) => {
fov.value = value
node.value.properties['FOV'] = fov.value
}
const materialMode = ref<'original' | 'normal' | 'wireframe' | 'depth'>(
'original'
)
const handleBackgroundColorChange = (value: string) => {
backgroundColor.value = value
node.value.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: 'original' | 'normal' | 'wireframe' | 'depth'
) => {
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: 'perspective' | 'orthographic') => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
</script>

View File

@@ -1,55 +1,31 @@
<template>
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
<Load3DControls
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:showPreview="showPreview"
:lightIntensity="lightIntensity"
:showLightIntensityButton="showLightIntensityButton"
:fov="fov"
:showFOVButton="showFOVButton"
:showPreviewButton="showPreviewButton"
@toggleCamera="onToggleCamera"
@toggleGrid="onToggleGrid"
@togglePreview="onTogglePreview"
@updateBackgroundColor="onUpdateBackgroundColor"
@updateLightIntensity="onUpdateLightIntensity"
@updateFOV="onUpdateFOV"
ref="load3dControlsRef"
<div
v-if="animations && animations.length > 0"
class="absolute top-0 left-0 w-full flex justify-center pt-2 gap-2 items-center pointer-events-auto z-10"
>
<Button class="p-button-rounded p-button-text" @click="togglePlay">
<i
:class="['pi', playing ? 'pi-pause' : 'pi-play', 'text-white text-lg']"
></i>
</Button>
<Select
v-model="selectedSpeed"
:options="speedOptions"
optionLabel="name"
optionValue="value"
@change="speedChange"
class="w-24"
/>
<div
v-if="animations && animations.length > 0"
class="absolute top-0 left-0 w-full flex justify-center pt-2 gap-2 items-center pointer-events-auto z-10"
>
<Button class="p-button-rounded p-button-text" @click="togglePlay">
<i
:class="[
'pi',
playing ? 'pi-pause' : 'pi-play',
'text-white text-lg'
]"
></i>
</Button>
<Select
v-model="selectedSpeed"
:options="speedOptions"
optionLabel="name"
optionValue="value"
@change="speedChange"
class="w-24"
/>
<Select
v-model="selectedAnimation"
:options="animations"
optionLabel="name"
optionValue="index"
@change="animationChange"
class="w-32"
/>
</div>
<Select
v-model="selectedAnimation"
:options="animations"
optionLabel="name"
optionValue="index"
@change="animationChange"
class="w-32"
/>
</div>
</template>
@@ -58,46 +34,21 @@ import Button from 'primevue/button'
import Select from 'primevue/select'
import { ref, watch } from 'vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
const props = defineProps<{
animations: Array<{ name: string; index: number }>
playing: boolean
backgroundColor: string
showGrid: boolean
showPreview: boolean
lightIntensity: number
showLightIntensityButton: boolean
fov: number
showFOVButton: boolean
showPreviewButton: boolean
}>()
const emit = defineEmits<{
(e: 'toggleCamera'): void
(e: 'toggleGrid', value: boolean): void
(e: 'togglePreview', value: boolean): void
(e: 'updateBackgroundColor', color: string): void
(e: 'togglePlay', value: boolean): void
(e: 'speedChange', value: number): void
(e: 'animationChange', value: number): void
(e: 'updateLightIntensity', value: number): void
(e: 'updateFOV', value: number): void
}>()
const animations = ref(props.animations)
const playing = ref(props.playing)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const backgroundColor = ref(props.backgroundColor)
const showGrid = ref(props.showGrid)
const showPreview = ref(props.showPreview)
const lightIntensity = ref(props.lightIntensity)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const fov = ref(props.fov)
const showFOVButton = ref(props.showFOVButton)
const showPreviewButton = ref(props.showPreviewButton)
const load3dControlsRef = ref(null)
const speedOptions = [
{ name: '0.1x', value: 0.1 },
@@ -107,42 +58,16 @@ const speedOptions = [
{ name: '2x', value: 2 }
]
watch(backgroundColor, (newValue) => {
load3dControlsRef.value.backgroundColor = newValue
})
watch(showLightIntensityButton, (newValue) => {
load3dControlsRef.value.showLightIntensityButton = newValue
})
watch(showFOVButton, (newValue) => {
load3dControlsRef.value.showFOVButton = newValue
})
watch(showPreviewButton, (newValue) => {
load3dControlsRef.value.showPreviewButton = newValue
})
const onToggleCamera = () => {
emit('toggleCamera')
}
const onToggleGrid = (value: boolean) => emit('toggleGrid', value)
const onTogglePreview = (value: boolean) => {
emit('togglePreview', value)
}
const onUpdateBackgroundColor = (color: string) =>
emit('updateBackgroundColor', color)
const onUpdateLightIntensity = (lightIntensity: number) => {
emit('updateLightIntensity', lightIntensity)
}
const onUpdateFOV = (fov: number) => {
emit('updateFOV', fov)
}
watch(
() => props.animations,
(newVal) => {
animations.value = newVal
}
)
const togglePlay = () => {
playing.value = !playing.value
emit('togglePlay', playing.value)
}
@@ -153,16 +78,4 @@ const speedChange = () => {
const animationChange = () => {
emit('animationChange', selectedAnimation.value)
}
defineExpose({
animations,
selectedAnimation,
playing,
backgroundColor,
showGrid,
lightIntensity,
showLightIntensityButton,
fov,
showFOVButton
})
</script>

View File

@@ -0,0 +1,162 @@
<template>
<Load3DScene
:node="node"
:type="type"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
:fov="fov"
:cameraType="cameraType"
:showPreview="showPreview"
:extraListeners="animationListeners"
@materialModeChange="listenMaterialModeChange"
@backgroundColorChange="listenBackgroundColorChange"
@lightIntensityChange="listenLightIntensityChange"
@fovChange="listenFOVChange"
@cameraTypeChange="listenCameraTypeChange"
@showGridChange="listenShowGridChange"
@showPreviewChange="listenShowPreviewChange"
ref="load3DSceneRef"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
const props = defineProps<{
node: any
type: 'Load3DAnimation' | 'Preview3DAnimation'
backgroundColor: string
showGrid: boolean
lightIntensity: number
fov: number
cameraType: 'perspective' | 'orthographic'
showPreview: boolean
materialMode: 'original' | 'normal' | 'wireframe' | 'depth'
showFOVButton: boolean
showLightIntensityButton: boolean
playing: boolean
selectedSpeed: number
selectedAnimation: number
}>()
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 materialMode = ref(props.materialMode)
const showFOVButton = ref(props.showFOVButton)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const load3DSceneRef = ref(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.showPreview,
(newValue) => {
showPreview.value = newValue
}
)
watch(
() => props.playing,
(newValue) => {
load3DSceneRef.value.load3d.toggleAnimation(newValue)
}
)
watch(
() => props.selectedSpeed,
(newValue) => {
load3DSceneRef.value.load3d.setAnimationSpeed(newValue)
}
)
watch(
() => props.selectedAnimation,
(newValue) => {
load3DSceneRef.value.load3d.updateSelectedAnimation(newValue)
}
)
const emit = defineEmits<{
(e: 'animationListChange', animationList: string): void
}>()
const listenMaterialModeChange = (
mode: 'original' | 'normal' | 'wireframe' | 'depth'
) => {
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: 'perspective' | 'orthographic') => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const animationListeners = {
animationListChange: (newValue: any) => {
emit('animationListChange', newValue)
}
}
</script>

View File

@@ -2,9 +2,9 @@
<div
class="absolute top-2 left-2 flex flex-col gap-2 pointer-events-auto z-20"
>
<Button class="p-button-rounded p-button-text" @click="toggleCamera">
<Button class="p-button-rounded p-button-text" @click="switchCamera">
<i
class="pi pi-camera text-white text-lg"
:class="['pi', getCameraIcon, 'text-white text-lg']"
v-tooltip.right="{ value: t('load3d.switchCamera'), showDelay: 300 }"
></i>
</Button>
@@ -13,9 +13,11 @@
class="p-button-rounded p-button-text"
:class="{ 'p-button-outlined': showGrid }"
@click="toggleGrid"
v-tooltip.right="{ value: t('load3d.showGrid'), showDelay: 300 }"
>
<i class="pi pi-table text-white text-lg"></i>
<i
class="pi pi-table text-white text-lg"
v-tooltip.right="{ value: t('load3d.showGrid'), showDelay: 300 }"
></i>
</Button>
<Button class="p-button-rounded p-button-text" @click="openColorPicker">
@@ -34,7 +36,7 @@
/>
</Button>
<div class="relative" v-if="showLightIntensityButton">
<div class="relative show-light-intensity" v-if="showLightIntensityButton">
<Button
class="p-button-rounded p-button-text"
@click="toggleLightIntensity"
@@ -63,7 +65,7 @@
</div>
</div>
<div class="relative" v-if="showFOVButton">
<div class="relative show-fov" v-if="showFOVButton">
<Button class="p-button-rounded p-button-text" @click="toggleFOV">
<i
class="pi pi-expand text-white text-lg"
@@ -102,12 +104,15 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import Slider from 'primevue/slider'
import { onMounted, onUnmounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { t } from '@/i18n'
const vTooltip = Tooltip
const props = defineProps<{
backgroundColor: string
showGrid: boolean
@@ -117,10 +122,11 @@ const props = defineProps<{
fov: number
showFOVButton: boolean
showPreviewButton: boolean
cameraType: 'perspective' | 'orthographic'
}>()
const emit = defineEmits<{
(e: 'toggleCamera'): void
(e: 'switchCamera'): void
(e: 'toggleGrid', value: boolean): void
(e: 'updateBackgroundColor', color: string): void
(e: 'updateLightIntensity', value: number): void
@@ -140,8 +146,8 @@ const showFOV = ref(false)
const showFOVButton = ref(props.showFOVButton)
const showPreviewButton = ref(props.showPreviewButton)
const toggleCamera = () => {
emit('toggleCamera')
const switchCamera = () => {
emit('switchCamera')
}
const toggleGrid = () => {
@@ -181,12 +187,64 @@ const updateFOV = () => {
const closeSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('.relative')) {
showLightIntensity.value = false
if (!target.closest('.show-fov')) {
showFOV.value = false
}
if (!target.closest('.show-light-intensity')) {
showLightIntensity.value = false
}
}
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.showPreviewButton,
(newValue) => {
showPreviewButton.value = newValue
}
)
watch(
() => props.showPreview,
(newValue) => {
showPreview.value = newValue
}
)
onMounted(() => {
document.addEventListener('click', closeSlider)
})
@@ -195,13 +253,7 @@ onUnmounted(() => {
document.removeEventListener('click', closeSlider)
})
defineExpose({
backgroundColor,
showGrid,
lightIntensity,
showLightIntensityButton,
fov,
showFOVButton,
showPreviewButton
const getCameraIcon = computed(() => {
return props.cameraType === 'perspective' ? 'pi-camera' : 'pi-th-large'
})
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div ref="container" class="w-full h-full"></div>
</template>
<script setup lang="ts">
import { LGraphNode } from '@comfyorg/litegraph'
import { onMounted, onUnmounted, ref, toRaw, watchEffect } from 'vue'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import { useLoad3dService } from '@/services/load3dService'
const props = defineProps<{
node: any
type: 'Load3D' | 'Load3DAnimation' | 'Preview3D' | 'Preview3DAnimation'
backgroundColor: string
showGrid: boolean
lightIntensity: number
fov: number
cameraType: 'perspective' | 'orthographic'
showPreview: boolean
extraListeners?: Record<string, (value: any) => void>
}>()
const container = ref<HTMLElement | null>(null)
const node = ref(props.node)
const load3d = ref<Load3d | Load3dAnimation | null>(null)
const eventConfig = {
materialModeChange: (value: string) => emit('materialModeChange', value),
backgroundColorChange: (value: string) =>
emit('backgroundColorChange', value),
lightIntensityChange: (value: number) => emit('lightIntensityChange', value),
fovChange: (value: number) => emit('fovChange', value),
cameraTypeChange: (value: string) => emit('cameraTypeChange', value),
showGridChange: (value: boolean) => emit('showGridChange', value),
showPreviewChange: (value: boolean) => emit('showPreviewChange', value)
} as const
watchEffect(() => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value)
rawLoad3d.setBackgroundColor(props.backgroundColor)
rawLoad3d.toggleGrid(props.showGrid)
rawLoad3d.setLightIntensity(props.lightIntensity)
rawLoad3d.setFOV(props.fov)
rawLoad3d.toggleCamera(props.cameraType)
rawLoad3d.togglePreview(props.showPreview)
}
})
const emit = defineEmits<{
(e: 'materialModeChange', materialMode: string): void
(e: 'backgroundColorChange', color: string): void
(e: 'lightIntensityChange', lightIntensity: number): void
(e: 'fovChange', fov: number): void
(e: 'cameraTypeChange', cameraType: string): void
(e: 'showGridChange', showGrid: boolean): void
(e: 'showPreviewChange', showPreview: 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)
})
if (props.extraListeners) {
Object.entries(props.extraListeners).forEach(([event, handler]) => {
const method = `${action}EventListener` as const
load3d.value?.[method](event, handler)
})
}
}
onMounted(() => {
load3d.value = useLoad3dService().registerLoad3d(
node.value as LGraphNode,
container.value,
props.type
)
handleEvents('add')
})
onUnmounted(() => {
handleEvents('remove')
useLoad3dService().removeLoad3d(node.value as LGraphNode)
})
defineExpose({
load3d
})
</script>

View File

@@ -1,8 +1,11 @@
// @ts-strict-ignore
import { IWidget } from '@comfyorg/litegraph'
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { nextTick } from 'vue'
import { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import PrimeVue from 'primevue/config'
import { createApp, h, nextTick, render } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import Load3DAnimation from '@/components/load3d/Load3DAnimation.vue'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
@@ -21,51 +24,54 @@ app.registerExtension({
const container = document.createElement('div')
container.classList.add('comfy-load-3d')
const load3d = useLoad3dService().registerLoad3d(
node,
container,
'Load3D'
)
/* Hold off for now
const mountComponent = () => {
const vnode = h(Load3D, {
node: node,
type: 'Load3D'
})
node.onMouseEnter = function () {
if (load3d) {
load3d.refreshViewport()
}
render(vnode, container)
}
*/
node.onResize = function () {
if (load3d) {
load3d.handleResize()
}
}
let controlsApp = createApp(Load3D, {
node: node,
type: 'Load3D'
})
controlsApp.mount(container)
const origOnRemoved = node.onRemoved
node.onRemoved = function () {
if (load3d) {
load3d.remove()
/*
render(null, container)
container.remove()
*/
if (controlsApp) {
controlsApp.unmount()
controlsApp = null
}
useLoad3dService().removeLoad3d(node)
origOnRemoved?.apply(this, [])
}
node.onDrawBackground = function () {
load3d.renderer.domElement.hidden = this.flags.collapsed ?? false
}
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = '.gltf,.glb,.obj,.mtl,.fbx,.stl'
fileInput.style.display = 'none'
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
) as IStringWidget
const uploadPath = await Load3dUtils.uploadFile(
load3d,
useLoad3dService().getLoad3d(node),
fileInput.files[0],
fileInput
).catch((error) => {
@@ -88,7 +94,8 @@ app.registerExtension({
})
node.addWidget('button', 'clear', 'clear', () => {
load3d.clearModel()
useLoad3dService().getLoad3d(node).clearModel()
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
@@ -97,6 +104,8 @@ app.registerExtension({
}
})
//mountComponent()
return {
widget: node.addDOMWidget(inputName, 'LOAD_3D', container)
}
@@ -177,40 +186,43 @@ app.registerExtension({
container.classList.add('comfy-load-3d-animation')
const load3d = useLoad3dService().registerLoad3d(
node,
container,
'Load3DAnimation'
)
/*
const mountComponent = () => {
const vnode = h(Load3DAnimation, {
node: node,
type: 'Load3DAnimation'
})
node.onMouseEnter = function () {
if (load3d) {
load3d.refreshViewport()
}
render(vnode, container)
}
*/
node.onResize = function () {
if (load3d) {
load3d.handleResize()
}
}
let controlsApp = createApp(Load3DAnimation, {
node: node,
type: 'Load3DAnimation'
})
controlsApp.use(PrimeVue)
controlsApp.mount(container)
const origOnRemoved = node.onRemoved
node.onRemoved = function () {
if (load3d) {
load3d.remove()
/*
render(null, container)
container.remove()
*/
if (controlsApp) {
controlsApp.unmount()
controlsApp = null
}
useLoad3dService().removeLoad3d(node)
origOnRemoved?.apply(this, [])
}
node.onDrawBackground = function () {
load3d.renderer.domElement.hidden = this.flags.collapsed ?? false
}
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = '.fbx,glb,gltf'
@@ -221,7 +233,7 @@ app.registerExtension({
(w: IWidget) => w.name === 'model_file'
) as IStringWidget
const uploadPath = await Load3dUtils.uploadFile(
load3d,
useLoad3dService().getLoad3d(node),
fileInput.files[0],
fileInput
).catch((error) => {
@@ -244,7 +256,7 @@ app.registerExtension({
})
node.addWidget('button', 'clear', 'clear', () => {
load3d.clearModel()
useLoad3dService().getLoad3d(node).clearModel()
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
@@ -253,6 +265,8 @@ app.registerExtension({
}
})
//mountComponent()
return {
widget: node.addDOMWidget(inputName, 'LOAD_3D_ANIMATION', container)
}
@@ -342,39 +356,42 @@ app.registerExtension({
container.classList.add('comfy-preview-3d')
const load3d = useLoad3dService().registerLoad3d(
node,
container,
'Preview3D'
)
/*
const mountComponent = () => {
const vnode = h(Load3D, {
node: node,
type: 'Preview3D'
})
node.onMouseEnter = function () {
if (load3d) {
load3d.refreshViewport()
}
render(vnode, container)
}
*/
node.onResize = function () {
if (load3d) {
load3d.handleResize()
}
}
let controlsApp = createApp(Load3D, {
node: node,
type: 'Preview3D'
})
controlsApp.mount(container)
const origOnRemoved = node.onRemoved
node.onRemoved = function () {
if (load3d) {
load3d.remove()
}
/*
render(null, container)
useLoad3dService().removeLoad3d(node)
container.remove()
*/
if (controlsApp) {
controlsApp.unmount()
controlsApp = null
}
origOnRemoved?.apply(this, [])
}
node.onDrawBackground = function () {
load3d.renderer.domElement.hidden = this.flags.collapsed ?? false
}
//mountComponent()
return {
widget: node.addDOMWidget(inputName, 'PREVIEW_3D', container)
@@ -447,40 +464,32 @@ app.registerExtension({
container.classList.add('comfy-preview-3d-animation')
const load3d = useLoad3dService().registerLoad3d(
node,
container,
'Preview3DAnimation'
)
let controlsApp = createApp(Load3DAnimation, {
node: node,
type: 'Preview3DAnimation'
})
node.onMouseEnter = function () {
if (load3d) {
load3d.refreshViewport()
}
}
controlsApp.use(PrimeVue)
node.onResize = function () {
if (load3d) {
load3d.handleResize()
}
}
controlsApp.mount(container)
const origOnRemoved = node.onRemoved
node.onRemoved = function () {
if (load3d) {
load3d.remove()
/*
render(null, container)
container.remove()
*/
if (controlsApp) {
controlsApp.unmount()
controlsApp = null
}
useLoad3dService().removeLoad3d(node)
origOnRemoved?.apply(this, [])
}
node.onDrawBackground = function () {
load3d.renderer.domElement.hidden = this.flags.collapsed ?? false
}
return {
widget: node.addDOMWidget(
inputName,

View File

@@ -88,17 +88,22 @@ class Load3DConfiguration {
this.load3d.toggleCamera(cameraType)
const showGrid = this.load3d.loadNodeProperty('Show Grid', true)
this.load3d.toggleGrid(showGrid)
const showPreview = this.load3d.loadNodeProperty('Show Preview', true)
this.load3d.togglePreview(showPreview)
const bgColor = this.load3d.loadNodeProperty('Background Color', '#282828')
this.load3d.setBackgroundColor(bgColor)
const lightIntensity = this.load3d.loadNodeProperty('Light Intensity', '5')
const lightIntensity = this.load3d.loadNodeProperty('Light Intensity', 5)
this.load3d.setLightIntensity(lightIntensity)
const fov = this.load3d.loadNodeProperty('FOV', '75')
const fov = this.load3d.loadNodeProperty('FOV', 75)
this.load3d.setFOV(fov)
}

View File

@@ -8,11 +8,14 @@ import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { App, createApp } from 'vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import { useToastStore } from '@/stores/toastStore'
interface Load3DOptions {
createPreview?: boolean
node?: LGraphNode
}
class Load3d {
scene: THREE.Scene
perspectiveCamera: THREE.PerspectiveCamera
@@ -51,14 +54,15 @@ class Load3d {
targetHeight: number = 1024
showPreview: boolean = true
node: LGraphNode = {} as LGraphNode
protected controlsApp: App | null = null
protected controlsContainer: HTMLDivElement
private listeners: { [key: string]: Function[] } = {}
constructor(
container: Element | HTMLElement,
options: { createPreview?: boolean } = {}
options: Load3DOptions = {
node: {} as LGraphNode
}
) {
this.node = options.node || ({} as LGraphNode)
this.scene = new THREE.Scene()
this.perspectiveCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
@@ -140,53 +144,30 @@ class Load3d {
this.createCapturePreview(container)
}
this.controlsContainer = document.createElement('div')
this.controlsContainer.style.position = 'absolute'
this.controlsContainer.style.top = '0'
this.controlsContainer.style.left = '0'
this.controlsContainer.style.width = '100%'
this.controlsContainer.style.height = '100%'
this.controlsContainer.style.pointerEvents = 'none'
this.controlsContainer.style.zIndex = '1'
container.appendChild(this.controlsContainer)
this.mountControls(options)
this.handleResize()
this.startAnimation()
}
protected mountControls(options: { createPreview?: boolean } = {}) {
const controlsMount = document.createElement('div')
controlsMount.style.pointerEvents = 'auto'
this.controlsContainer.appendChild(controlsMount)
this.controlsApp = createApp(Load3DControls, {
backgroundColor: '#282828',
showGrid: true,
showPreview: options.createPreview,
lightIntensity: 5,
showLightIntensityButton: true,
fov: 75,
showFOVButton: true,
showPreviewButton: options.createPreview,
onToggleCamera: () => this.toggleCamera(),
onToggleGrid: (show: boolean) => this.toggleGrid(show),
onTogglePreview: (show: boolean) => this.togglePreview(show),
onUpdateBackgroundColor: (color: string) =>
this.setBackgroundColor(color),
onUpdateLightIntensity: (lightIntensity: number) =>
this.setLightIntensity(lightIntensity),
onUpdateFOV: (fov: number) => this.setFOV(fov)
})
this.controlsApp.directive('tooltip', Tooltip)
this.controlsApp.mount(controlsMount)
addEventListener(event: string, callback: Function) {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event].push(callback)
}
setNode(node: LGraphNode) {
this.node = node
removeEventListener(event: string, callback: Function) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter(
(cb) => cb !== callback
)
}
}
emitEvent(event: string, data?: any) {
if (this.listeners[event]) {
this.listeners[event].forEach((callback) => callback(data))
}
}
storeNodeProperty(name: string, value: any) {
@@ -339,8 +320,6 @@ class Load3d {
this.perspectiveCamera.fov = fov
this.perspectiveCamera.updateProjectionMatrix()
this.renderer.render(this.scene, this.activeCamera)
this.storeNodeProperty('FOV', fov)
}
if (
@@ -351,6 +330,8 @@ class Load3d {
this.previewCamera.updateProjectionMatrix()
this.previewRenderer.render(this.scene, this.previewCamera)
}
this.emitEvent('fovChange', fov)
}
getCameraState() {
@@ -430,11 +411,6 @@ class Load3d {
setMaterialMode(mode: 'original' | 'normal' | 'wireframe' | 'depth') {
this.materialMode = mode
if (this.controlsApp?._instance?.exposed) {
this.controlsApp._instance.exposed.showLightIntensityButton.value =
mode == 'original'
}
if (this.currentModel) {
if (mode === 'depth') {
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
@@ -528,6 +504,8 @@ class Load3d {
})
this.renderer.render(this.scene, this.activeCamera)
this.emitEvent('materialModeChange', mode)
}
}
@@ -608,14 +586,10 @@ class Load3d {
)
this.viewHelper.center = this.controls.target
if (this.controlsApp?._instance?.exposed) {
this.controlsApp._instance.exposed.showFOVButton.value =
this.getCurrentCameraType() == 'perspective'
}
this.storeNodeProperty('Camera Type', this.getCurrentCameraType())
this.handleResize()
this.updatePreviewRender()
this.emitEvent('cameraTypeChange', cameraType)
}
getCurrentCameraType(): 'perspective' | 'orthographic' {
@@ -627,9 +601,9 @@ class Load3d {
toggleGrid(showGrid: boolean) {
if (this.gridHelper) {
this.gridHelper.visible = showGrid
this.storeNodeProperty('Show Grid', showGrid)
}
this.emitEvent('showGridChange', showGrid)
}
togglePreview(showPreview: boolean) {
@@ -637,9 +611,9 @@ class Load3d {
this.showPreview = showPreview
this.previewContainer.style.display = this.showPreview ? 'block' : 'none'
this.storeNodeProperty('Show Preview', showPreview)
}
this.emitEvent('showPreviewChange', showPreview)
}
setLightIntensity(intensity: number) {
@@ -659,7 +633,7 @@ class Load3d {
}
})
this.storeNodeProperty('Light Intensity', intensity)
this.emitEvent('lightIntensityChange', intensity)
}
startAnimation() {
@@ -771,10 +745,6 @@ class Load3d {
this.controls.dispose()
this.viewHelper.dispose()
this.renderer.dispose()
if (this.controlsApp) {
this.controlsApp.unmount()
this.controlsApp = null
}
this.renderer.domElement.remove()
this.scene.clear()
}
@@ -1044,11 +1014,7 @@ class Load3d {
this.renderer.setClearColor(new THREE.Color(color))
this.renderer.render(this.scene, this.activeCamera)
if (this.controlsApp?._instance?.exposed) {
this.controlsApp._instance.exposed.backgroundColor.value = color
}
this.storeNodeProperty('Background Color', color)
this.emitEvent('backgroundColorChange', color)
}
}

View File

@@ -6,6 +6,11 @@ import { createApp } from 'vue'
import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
import Load3d from '@/extensions/core/load3d/Load3d'
interface AnimationItem {
name: string
index: number
}
class Load3dAnimation extends Load3d {
currentAnimation: THREE.AnimationMixer | null = null
animationActions: THREE.AnimationAction[] = []
@@ -22,53 +27,17 @@ class Load3dAnimation extends Load3d {
super(container, options)
}
protected mountControls(options: { createPreview?: boolean } = {}) {
const controlsMount = document.createElement('div')
controlsMount.style.pointerEvents = 'auto'
this.controlsContainer.appendChild(controlsMount)
this.controlsApp = createApp(Load3DAnimationControls, {
backgroundColor: '#282828',
showGrid: true,
showPreview: options.createPreview,
animations: [],
playing: false,
lightIntensity: 5,
showLightIntensityButton: true,
fov: 75,
showFOVButton: true,
showPreviewButton: options.createPreview,
onToggleCamera: () => this.toggleCamera(),
onToggleGrid: (show: boolean) => this.toggleGrid(show),
onTogglePreview: (show: boolean) => this.togglePreview(show),
onUpdateBackgroundColor: (color: string) =>
this.setBackgroundColor(color),
onTogglePlay: (play: boolean) => this.toggleAnimation(play),
onSpeedChange: (speed: number) => this.setAnimationSpeed(speed),
onAnimationChange: (selectedAnimation: number) =>
this.updateSelectedAnimation(selectedAnimation),
onUpdateLightIntensity: (lightIntensity: number) =>
this.setLightIntensity(lightIntensity),
onUpdateFOV: (fov: number) => this.setFOV(fov)
})
this.controlsApp.use(PrimeVue)
this.controlsApp.directive('tooltip', Tooltip)
this.controlsApp.mount(controlsMount)
}
updateAnimationList() {
if (this.controlsApp?._instance?.exposed) {
if (this.animationClips.length > 0) {
this.controlsApp._instance.exposed.animations.value =
this.animationClips.map((clip, index) => ({
name: clip.name || `Animation ${index + 1}`,
index
}))
} else {
this.controlsApp._instance.exposed.animations.value = []
}
let updatedAnimationList: AnimationItem[] = []
if (this.animationClips.length > 0) {
updatedAnimationList = this.animationClips.map((clip, index) => ({
name: clip.name || `Animation ${index + 1}`,
index
}))
}
this.emitEvent('animationListChange', updatedAnimationList)
}
protected async setupModel(model: THREE.Object3D) {
@@ -146,10 +115,6 @@ class Load3dAnimation extends Load3d {
}
this.animationActions = [action]
if (this.controlsApp?._instance?.exposed) {
this.controlsApp._instance.exposed.selectedAnimation.value = index
}
}
clearModel() {
@@ -165,10 +130,7 @@ class Load3dAnimation extends Load3d {
this.isAnimationPlaying = false
this.animationSpeed = 1.0
if (this.controlsApp?._instance?.exposed) {
this.controlsApp._instance.exposed.animations.value = []
this.controlsApp._instance.exposed.selectedAnimation.value = 0
}
this.emitEvent('animationListChange', [])
super.clearModel()
}
@@ -181,10 +143,6 @@ class Load3dAnimation extends Load3d {
this.isAnimationPlaying = play ?? !this.isAnimationPlaying
if (this.controlsApp?._instance?.exposed) {
this.controlsApp._instance.exposed.playing.value = this.isAnimationPlaying
}
this.animationActions.forEach((action) => {
if (this.isAnimationPlaying) {
action.paused = false
@@ -231,4 +189,5 @@ class Load3dAnimation extends Load3d {
}
}
export { AnimationItem }
export default Load3dAnimation

View File

@@ -1,4 +1,5 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import { toRaw } from 'vue'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
@@ -21,8 +22,10 @@ export class Load3dService {
container: HTMLElement,
type: 'Load3D' | 'Load3DAnimation' | 'Preview3D' | 'Preview3DAnimation'
) {
if (this.nodeToLoad3dMap.has(node)) {
this.removeLoad3d(node)
const rawNode = toRaw(node)
if (this.nodeToLoad3dMap.has(rawNode)) {
this.removeLoad3d(rawNode)
}
const isAnimation = type.includes('Animation')
@@ -31,17 +34,32 @@ export class Load3dService {
const isPreview = type.includes('Preview')
const instance = new Load3dClass(container, { createPreview: !isPreview })
const instance = new Load3dClass(container, {
createPreview: !isPreview,
node: rawNode
})
instance.setNode(node)
rawNode.onMouseEnter = function () {
instance.refreshViewport()
}
this.nodeToLoad3dMap.set(node, instance)
rawNode.onResize = function () {
instance.handleResize()
}
rawNode.onDrawBackground = function () {
instance.renderer.domElement.hidden = this.flags.collapsed ?? false
}
this.nodeToLoad3dMap.set(rawNode, instance)
return instance
}
getLoad3d(node: LGraphNode): Load3d | Load3dAnimation | null {
return this.nodeToLoad3dMap.get(node) || null
const rawNode = toRaw(node)
return this.nodeToLoad3dMap.get(rawNode) || null
}
getNodeByLoad3d(load3d: Load3d | Load3dAnimation): LGraphNode | null {
@@ -54,10 +72,14 @@ export class Load3dService {
}
removeLoad3d(node: LGraphNode) {
const instance = this.nodeToLoad3dMap.get(node)
const rawNode = toRaw(node)
const instance = this.nodeToLoad3dMap.get(rawNode)
if (instance) {
instance.remove()
this.nodeToLoad3dMap.delete(node)
this.nodeToLoad3dMap.delete(rawNode)
}
}