mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
support panoramic image in 3d node (#6638)
## Summary Adds panoramic image support to the 3D node viewer, allowing users to display equirectangular panoramic images as immersive backgrounds alongside the existing tiled image mode. ## Changes - Toggle between tiled and panorama rendering modes for background images - Field of view (FOV) control for panorama mode - Refactored FOV slider into reusable PopupSlider component ## Screenshots https://github.com/user-attachments/assets/8955d74b-b0e6-4b26-83ca-ccf902b43aa6 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6638-support-panoramic-image-in-3d-node-2a56d73d365081b98647f988130e312e) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -39,6 +39,8 @@
|
||||
v-model:show-grid="sceneConfig!.showGrid"
|
||||
v-model:background-color="sceneConfig!.backgroundColor"
|
||||
v-model:background-image="sceneConfig!.backgroundImage"
|
||||
v-model:background-render-mode="sceneConfig!.backgroundRenderMode"
|
||||
v-model:fov="cameraConfig!.fov"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
/>
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
<SceneControls
|
||||
v-model:background-color="viewer.backgroundColor.value"
|
||||
v-model:show-grid="viewer.showGrid.value"
|
||||
v-model:background-render-mode="viewer.backgroundRenderMode.value"
|
||||
v-model:fov="viewer.fov.value"
|
||||
:has-background-image="viewer.hasBackgroundImage.value"
|
||||
@update-background-image="viewer.handleBackgroundImageUpdate"
|
||||
/>
|
||||
|
||||
@@ -6,65 +6,30 @@
|
||||
value: $t('load3d.switchCamera'),
|
||||
showDelay: 300
|
||||
}"
|
||||
:class="['pi', getCameraIcon, 'text-lg text-white']"
|
||||
:class="['pi', 'pi-camera', 'text-lg text-white']"
|
||||
/>
|
||||
</Button>
|
||||
<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 }"
|
||||
class="pi pi-expand text-lg text-white"
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
v-show="showFOV"
|
||||
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" />
|
||||
</div>
|
||||
</div>
|
||||
<PopupSlider
|
||||
v-if="showFOVButton"
|
||||
v-model="fov"
|
||||
:tooltip-text="$t('load3d.fov')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PopupSlider from '@/components/load3d/controls/PopupSlider.vue'
|
||||
import type { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const showFOV = ref(false)
|
||||
|
||||
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 switchCamera = () => {
|
||||
cameraType.value =
|
||||
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
|
||||
}
|
||||
|
||||
const closeCameraSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
if (!target.closest('.show-fov')) {
|
||||
showFOV.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeCameraSlider)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeCameraSlider)
|
||||
})
|
||||
</script>
|
||||
|
||||
64
src/components/load3d/controls/PopupSlider.vue
Normal file
64
src/components/load3d/controls/PopupSlider.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="relative show-slider">
|
||||
<Button class="p-button-rounded p-button-text" @click="toggleSlider">
|
||||
<i
|
||||
v-tooltip.right="{ value: tooltipText, showDelay: 300 }"
|
||||
:class="['pi', icon, 'text-lg text-white']"
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
v-show="showSlider"
|
||||
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg w-[150px]"
|
||||
>
|
||||
<Slider
|
||||
v-model="value"
|
||||
class="w-full"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Slider from 'primevue/slider'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const {
|
||||
icon = 'pi-expand',
|
||||
min = 10,
|
||||
max = 150,
|
||||
step = 1
|
||||
} = defineProps<{
|
||||
icon?: string
|
||||
tooltipText: string
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
}>()
|
||||
|
||||
const value = defineModel<number>()
|
||||
const showSlider = ref(false)
|
||||
|
||||
const toggleSlider = () => {
|
||||
showSlider.value = !showSlider.value
|
||||
}
|
||||
|
||||
const closeSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
if (!target.closest('.show-slider')) {
|
||||
showSlider.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeSlider)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeSlider)
|
||||
})
|
||||
</script>
|
||||
@@ -51,6 +51,28 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="hasBackgroundImage">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
:class="{ 'p-button-outlined': backgroundRenderMode === 'panorama' }"
|
||||
@click="toggleBackgroundRenderMode"
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.panoramaMode'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-globe text-lg text-white"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PopupSlider
|
||||
v-if="hasBackgroundImage && backgroundRenderMode === 'panorama'"
|
||||
v-model="fov"
|
||||
:tooltip-text="$t('load3d.fov')"
|
||||
/>
|
||||
|
||||
<div v-if="hasBackgroundImage">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
@@ -72,6 +94,9 @@
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import PopupSlider from '@/components/load3d/controls/PopupSlider.vue'
|
||||
import type { BackgroundRenderModeType } from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateBackgroundImage', file: File | null): void
|
||||
}>()
|
||||
@@ -79,6 +104,11 @@ const emit = defineEmits<{
|
||||
const showGrid = defineModel<boolean>('showGrid')
|
||||
const backgroundColor = defineModel<string>('backgroundColor')
|
||||
const backgroundImage = defineModel<string>('backgroundImage')
|
||||
const backgroundRenderMode = defineModel<BackgroundRenderModeType>(
|
||||
'backgroundRenderMode',
|
||||
{ default: 'tiled' }
|
||||
)
|
||||
const fov = defineModel<number>('fov')
|
||||
const hasBackgroundImage = computed(
|
||||
() => backgroundImage.value && backgroundImage.value !== ''
|
||||
)
|
||||
@@ -113,4 +143,9 @@ const uploadBackgroundImage = (event: Event) => {
|
||||
const removeBackgroundImage = () => {
|
||||
emit('updateBackgroundImage', null)
|
||||
}
|
||||
|
||||
const toggleBackgroundRenderMode = () => {
|
||||
backgroundRenderMode.value =
|
||||
backgroundRenderMode.value === 'panorama' ? 'tiled' : 'panorama'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -32,6 +32,24 @@
|
||||
</div>
|
||||
|
||||
<div v-if="hasBackgroundImage" class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
:severity="backgroundRenderMode === 'tiled' ? 'primary' : 'secondary'"
|
||||
:label="$t('load3d.tiledMode')"
|
||||
icon="pi pi-th-large"
|
||||
class="flex-1"
|
||||
@click="setBackgroundRenderMode('tiled')"
|
||||
/>
|
||||
<Button
|
||||
:severity="
|
||||
backgroundRenderMode === 'panorama' ? 'primary' : 'secondary'
|
||||
"
|
||||
:label="$t('load3d.panoramaMode')"
|
||||
icon="pi pi-globe"
|
||||
class="flex-1"
|
||||
@click="setBackgroundRenderMode('panorama')"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="$t('load3d.removeBackgroundImage')"
|
||||
@@ -50,6 +68,9 @@ import { ref } from 'vue'
|
||||
|
||||
const backgroundColor = defineModel<string>('backgroundColor')
|
||||
const showGrid = defineModel<boolean>('showGrid')
|
||||
const backgroundRenderMode = defineModel<'tiled' | 'panorama'>(
|
||||
'backgroundRenderMode'
|
||||
)
|
||||
|
||||
defineProps<{
|
||||
hasBackgroundImage?: boolean
|
||||
@@ -77,4 +98,8 @@ const handleImageUpload = (event: Event) => {
|
||||
const removeBackgroundImage = () => {
|
||||
emit('updateBackgroundImage', null)
|
||||
}
|
||||
|
||||
const setBackgroundRenderMode = (mode: 'tiled' | 'panorama') => {
|
||||
backgroundRenderMode.value = mode
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -31,7 +31,8 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
const sceneConfig = ref<SceneConfig>({
|
||||
showGrid: true,
|
||||
backgroundColor: '#000000',
|
||||
backgroundImage: ''
|
||||
backgroundImage: '',
|
||||
backgroundRenderMode: 'tiled'
|
||||
})
|
||||
|
||||
const modelConfig = ref<ModelConfig>({
|
||||
@@ -131,7 +132,11 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
// Restore configs - watchers will handle applying them to the Three.js scene
|
||||
const savedSceneConfig = node.properties['Scene Config'] as SceneConfig
|
||||
if (savedSceneConfig) {
|
||||
sceneConfig.value = savedSceneConfig
|
||||
sceneConfig.value = {
|
||||
...sceneConfig.value,
|
||||
...savedSceneConfig,
|
||||
backgroundRenderMode: savedSceneConfig.backgroundRenderMode || 'tiled'
|
||||
}
|
||||
}
|
||||
|
||||
const savedModelConfig = node.properties['Model Config'] as ModelConfig
|
||||
@@ -221,12 +226,17 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
|
||||
watch(
|
||||
sceneConfig,
|
||||
(newValue) => {
|
||||
async (newValue) => {
|
||||
if (load3d && nodeRef.value) {
|
||||
nodeRef.value.properties['Scene Config'] = newValue
|
||||
load3d.toggleGrid(newValue.showGrid)
|
||||
load3d.setBackgroundColor(newValue.backgroundColor)
|
||||
void load3d.setBackgroundImage(newValue.backgroundImage || '')
|
||||
|
||||
await load3d.setBackgroundImage(newValue.backgroundImage || '')
|
||||
|
||||
if (newValue.backgroundRenderMode) {
|
||||
load3d.setBackgroundRenderMode(newValue.backgroundRenderMode)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
@@ -424,6 +434,9 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
backgroundColorChange: (value: string) => {
|
||||
sceneConfig.value.backgroundColor = value
|
||||
},
|
||||
backgroundRenderModeChange: (value: string) => {
|
||||
sceneConfig.value.backgroundRenderMode = value as 'tiled' | 'panorama'
|
||||
},
|
||||
lightIntensityChange: (value: number) => {
|
||||
lightConfig.value.intensity = value
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, toRaw, watch } from 'vue'
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type {
|
||||
BackgroundRenderModeType,
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
@@ -21,6 +22,7 @@ interface Load3dViewerState {
|
||||
lightIntensity: number
|
||||
cameraState: any
|
||||
backgroundImage: string
|
||||
backgroundRenderMode: BackgroundRenderModeType
|
||||
upDirection: UpDirection
|
||||
materialMode: MaterialMode
|
||||
}
|
||||
@@ -33,6 +35,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
const lightIntensity = ref(1)
|
||||
const backgroundImage = ref('')
|
||||
const hasBackgroundImage = ref(false)
|
||||
const backgroundRenderMode = ref<BackgroundRenderModeType>('tiled')
|
||||
const upDirection = ref<UpDirection>('original')
|
||||
const materialMode = ref<MaterialMode>('original')
|
||||
const needApplyChanges = ref(true)
|
||||
@@ -49,6 +52,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
lightIntensity: 1,
|
||||
cameraState: null,
|
||||
backgroundImage: '',
|
||||
backgroundRenderMode: 'tiled',
|
||||
upDirection: 'original',
|
||||
materialMode: 'original'
|
||||
})
|
||||
@@ -124,6 +128,20 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(backgroundRenderMode, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setBackgroundRenderMode(newValue)
|
||||
} catch (error) {
|
||||
console.error('Error updating background render mode:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateBackgroundRenderMode', {
|
||||
mode: newValue
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(upDirection, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
@@ -180,6 +198,10 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
source.sceneManager.currentBackgroundColor
|
||||
showGrid.value =
|
||||
sceneConfig.showGrid ?? source.sceneManager.gridHelper.visible
|
||||
backgroundRenderMode.value =
|
||||
sceneConfig.backgroundRenderMode ||
|
||||
source.sceneManager.backgroundRenderMode ||
|
||||
'tiled'
|
||||
|
||||
const backgroundInfo = source.sceneManager.getCurrentBackgroundInfo()
|
||||
if (backgroundInfo.type === 'image' && sceneConfig.backgroundImage) {
|
||||
@@ -219,6 +241,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
lightIntensity: lightIntensity.value,
|
||||
cameraState: sourceCameraState,
|
||||
backgroundImage: backgroundImage.value,
|
||||
backgroundRenderMode: backgroundRenderMode.value,
|
||||
upDirection: upDirection.value,
|
||||
materialMode: materialMode.value
|
||||
}
|
||||
@@ -274,7 +297,8 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
nodeValue.properties['Scene Config'] = {
|
||||
showGrid: initialState.value.showGrid,
|
||||
backgroundColor: initialState.value.backgroundColor,
|
||||
backgroundImage: initialState.value.backgroundImage
|
||||
backgroundImage: initialState.value.backgroundImage,
|
||||
backgroundRenderMode: initialState.value.backgroundRenderMode
|
||||
}
|
||||
|
||||
nodeValue.properties['Camera Config'] = {
|
||||
@@ -309,7 +333,8 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
nodeValue.properties['Scene Config'] = {
|
||||
showGrid: showGrid.value,
|
||||
backgroundColor: backgroundColor.value,
|
||||
backgroundImage: backgroundImage.value
|
||||
backgroundImage: backgroundImage.value,
|
||||
backgroundRenderMode: backgroundRenderMode.value
|
||||
}
|
||||
|
||||
nodeValue.properties['Camera Config'] = {
|
||||
@@ -331,6 +356,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
await useLoad3dService().copyLoad3dState(load3d, sourceLoad3d)
|
||||
|
||||
await sourceLoad3d.setBackgroundImage(backgroundImage.value)
|
||||
sourceLoad3d.setBackgroundRenderMode(backgroundRenderMode.value)
|
||||
|
||||
sourceLoad3d.forceRender()
|
||||
|
||||
@@ -429,6 +455,7 @@ export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
lightIntensity,
|
||||
backgroundImage,
|
||||
hasBackgroundImage,
|
||||
backgroundRenderMode,
|
||||
upDirection,
|
||||
materialMode,
|
||||
needApplyChanges,
|
||||
|
||||
@@ -162,7 +162,11 @@ class Load3DConfiguration {
|
||||
return
|
||||
}
|
||||
|
||||
this.load3d.setBackgroundImage(config.backgroundImage)
|
||||
void this.load3d.setBackgroundImage(config.backgroundImage)
|
||||
|
||||
if (config.backgroundRenderMode) {
|
||||
this.load3d.setBackgroundRenderMode(config.backgroundRenderMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -510,6 +510,11 @@ class Load3d {
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
setBackgroundRenderMode(mode: 'tiled' | 'panorama'): void {
|
||||
this.sceneManager.setBackgroundRenderMode(mode)
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
toggleCamera(cameraType?: 'perspective' | 'orthographic'): void {
|
||||
this.cameraManager.toggleCamera(cameraType)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import Load3dUtils from './Load3dUtils'
|
||||
import {
|
||||
type BackgroundRenderModeType,
|
||||
type EventManagerInterface,
|
||||
type SceneManagerInterface
|
||||
} from './interfaces'
|
||||
@@ -16,6 +17,8 @@ export class SceneManager implements SceneManagerInterface {
|
||||
backgroundMesh: THREE.Mesh | null = null
|
||||
backgroundTexture: THREE.Texture | null = null
|
||||
|
||||
backgroundRenderMode: 'tiled' | 'panorama' = 'tiled'
|
||||
|
||||
backgroundColorMaterial: THREE.MeshBasicMaterial | null = null
|
||||
currentBackgroundType: 'color' | 'image' = 'color'
|
||||
currentBackgroundColor: string = '#282828'
|
||||
@@ -89,6 +92,10 @@ export class SceneManager implements SceneManagerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.scene.background) {
|
||||
this.scene.background = null
|
||||
}
|
||||
|
||||
this.scene.clear()
|
||||
}
|
||||
|
||||
@@ -104,6 +111,15 @@ export class SceneManager implements SceneManagerInterface {
|
||||
this.currentBackgroundColor = color
|
||||
this.currentBackgroundType = 'color'
|
||||
|
||||
if (this.scene.background instanceof THREE.Texture) {
|
||||
this.scene.background = null
|
||||
}
|
||||
|
||||
if (this.backgroundRenderMode === 'panorama') {
|
||||
this.backgroundRenderMode = 'tiled'
|
||||
this.eventManager.emitEvent('backgroundRenderModeChange', 'tiled')
|
||||
}
|
||||
|
||||
if (!this.backgroundMesh || !this.backgroundColorMaterial) {
|
||||
this.initBackgroundScene()
|
||||
}
|
||||
@@ -168,36 +184,41 @@ export class SceneManager implements SceneManagerInterface {
|
||||
this.backgroundTexture = texture
|
||||
this.currentBackgroundType = 'image'
|
||||
|
||||
if (!this.backgroundMesh) {
|
||||
this.initBackgroundScene()
|
||||
}
|
||||
|
||||
const imageMaterial = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
if (this.backgroundMesh) {
|
||||
if (
|
||||
this.backgroundMesh.material !== this.backgroundColorMaterial &&
|
||||
this.backgroundMesh.material instanceof THREE.Material
|
||||
) {
|
||||
this.backgroundMesh.material.dispose()
|
||||
if (this.backgroundRenderMode === 'panorama') {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping
|
||||
this.scene.background = texture
|
||||
} else {
|
||||
if (!this.backgroundMesh) {
|
||||
this.initBackgroundScene()
|
||||
}
|
||||
|
||||
this.backgroundMesh.material = imageMaterial
|
||||
this.backgroundMesh.position.set(0, 0, 0)
|
||||
}
|
||||
const imageMaterial = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
this.updateBackgroundSize(
|
||||
this.backgroundTexture,
|
||||
this.backgroundMesh,
|
||||
this.renderer.domElement.clientWidth,
|
||||
this.renderer.domElement.clientHeight
|
||||
)
|
||||
if (this.backgroundMesh) {
|
||||
if (
|
||||
this.backgroundMesh.material !== this.backgroundColorMaterial &&
|
||||
this.backgroundMesh.material instanceof THREE.Material
|
||||
) {
|
||||
this.backgroundMesh.material.dispose()
|
||||
}
|
||||
|
||||
this.backgroundMesh.material = imageMaterial
|
||||
this.backgroundMesh.position.set(0, 0, 0)
|
||||
}
|
||||
|
||||
this.updateBackgroundSize(
|
||||
this.backgroundTexture,
|
||||
this.backgroundMesh,
|
||||
this.renderer.domElement.clientWidth,
|
||||
this.renderer.domElement.clientHeight
|
||||
)
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('backgroundImageChange', uploadPath)
|
||||
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
|
||||
@@ -213,6 +234,35 @@ export class SceneManager implements SceneManagerInterface {
|
||||
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
|
||||
}
|
||||
|
||||
setBackgroundRenderMode(mode: BackgroundRenderModeType): void {
|
||||
if (this.backgroundRenderMode === mode) return
|
||||
|
||||
this.backgroundRenderMode = mode
|
||||
|
||||
if (this.currentBackgroundType === 'image' && this.backgroundTexture) {
|
||||
try {
|
||||
if (mode === 'panorama') {
|
||||
this.backgroundTexture.mapping =
|
||||
THREE.EquirectangularReflectionMapping
|
||||
this.scene.background = this.backgroundTexture
|
||||
} else {
|
||||
this.scene.background = null
|
||||
if (
|
||||
this.backgroundMesh &&
|
||||
this.backgroundMesh.material instanceof THREE.MeshBasicMaterial
|
||||
) {
|
||||
this.backgroundMesh.material.map = this.backgroundTexture
|
||||
this.backgroundMesh.material.needsUpdate = true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error set background render mode:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('backgroundRenderModeChange', mode)
|
||||
}
|
||||
|
||||
updateBackgroundSize(
|
||||
backgroundTexture: THREE.Texture | null,
|
||||
backgroundMesh: THREE.Mesh | null,
|
||||
@@ -254,7 +304,11 @@ export class SceneManager implements SceneManagerInterface {
|
||||
}
|
||||
|
||||
renderBackground(): void {
|
||||
if (this.backgroundMesh) {
|
||||
if (
|
||||
(this.backgroundRenderMode === 'tiled' ||
|
||||
this.currentBackgroundType === 'color') &&
|
||||
this.backgroundMesh
|
||||
) {
|
||||
const currentToneMapping = this.renderer.toneMapping
|
||||
const currentExposure = this.renderer.toneMappingExposure
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
export type MaterialMode = 'original' | 'normal' | 'wireframe' | 'depth'
|
||||
export type UpDirection = 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
export type CameraType = 'perspective' | 'orthographic'
|
||||
export type BackgroundRenderModeType = 'tiled' | 'panorama'
|
||||
|
||||
export interface CameraState {
|
||||
position: THREE.Vector3
|
||||
@@ -25,6 +26,7 @@ export interface SceneConfig {
|
||||
showGrid: boolean
|
||||
backgroundColor: string
|
||||
backgroundImage?: string
|
||||
backgroundRenderMode?: BackgroundRenderModeType
|
||||
}
|
||||
|
||||
export interface ModelConfig {
|
||||
@@ -77,6 +79,7 @@ export interface SceneManagerInterface extends BaseManager {
|
||||
setBackgroundColor(color: string): void
|
||||
setBackgroundImage(uploadPath: string): Promise<void>
|
||||
removeBackgroundImage(): void
|
||||
setBackgroundRenderMode(mode: BackgroundRenderModeType): void
|
||||
handleResize(width: number, height: number): void
|
||||
captureScene(width: number, height: number): Promise<CaptureResult>
|
||||
}
|
||||
|
||||
@@ -1737,6 +1737,8 @@
|
||||
"previewOutput": "Preview Output",
|
||||
"uploadBackgroundImage": "Upload Background Image",
|
||||
"removeBackgroundImage": "Remove Background Image",
|
||||
"tiledMode": "Tiled",
|
||||
"panoramaMode": "Panorama",
|
||||
"loadingModel": "Loading 3D Model...",
|
||||
"upDirection": "Up Direction",
|
||||
"materialMode": "Material Mode",
|
||||
@@ -1833,7 +1835,8 @@
|
||||
"failedToInitializeLoad3dViewer": "Failed to initialize 3D Viewer",
|
||||
"failedToLoadBackgroundImage": "Failed to load background image",
|
||||
"failedToLoadModel": "Failed to load 3D model",
|
||||
"modelLoadedSuccessfully": "3D model loaded successfully"
|
||||
"modelLoadedSuccessfully": "3D model loaded successfully",
|
||||
"failedToUpdateBackgroundRenderMode": "Failed to update background render mode to {mode}"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
|
||||
@@ -47,7 +47,8 @@ describe('useLoad3d', () => {
|
||||
'Scene Config': {
|
||||
showGrid: true,
|
||||
backgroundColor: '#000000',
|
||||
backgroundImage: ''
|
||||
backgroundImage: '',
|
||||
backgroundRenderMode: 'tiled'
|
||||
},
|
||||
'Model Config': {
|
||||
upDirection: 'original',
|
||||
@@ -81,6 +82,7 @@ describe('useLoad3d', () => {
|
||||
toggleGrid: vi.fn(),
|
||||
setBackgroundColor: vi.fn(),
|
||||
setBackgroundImage: vi.fn().mockResolvedValue(undefined),
|
||||
setBackgroundRenderMode: vi.fn(),
|
||||
setUpDirection: vi.fn(),
|
||||
setMaterialMode: vi.fn(),
|
||||
toggleCamera: vi.fn(),
|
||||
@@ -130,7 +132,8 @@ describe('useLoad3d', () => {
|
||||
expect(composable.sceneConfig.value).toEqual({
|
||||
showGrid: true,
|
||||
backgroundColor: '#000000',
|
||||
backgroundImage: ''
|
||||
backgroundImage: '',
|
||||
backgroundRenderMode: 'tiled'
|
||||
})
|
||||
expect(composable.modelConfig.value).toEqual({
|
||||
upDirection: 'original',
|
||||
@@ -165,9 +168,11 @@ describe('useLoad3d', () => {
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await composable.initializeLoad3d(containerRef)
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.toggleGrid).toHaveBeenCalledWith(true)
|
||||
expect(mockLoad3d.setBackgroundColor).toHaveBeenCalledWith('#000000')
|
||||
expect(mockLoad3d.setBackgroundRenderMode).toHaveBeenCalledWith('tiled')
|
||||
expect(mockLoad3d.setUpDirection).toHaveBeenCalledWith('original')
|
||||
expect(mockLoad3d.setMaterialMode).toHaveBeenCalledWith('original')
|
||||
expect(mockLoad3d.toggleCamera).toHaveBeenCalledWith('perspective')
|
||||
@@ -356,17 +361,22 @@ describe('useLoad3d', () => {
|
||||
composable.sceneConfig.value = {
|
||||
showGrid: false,
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundImage: 'test.jpg'
|
||||
backgroundImage: 'test.jpg',
|
||||
backgroundRenderMode: 'panorama'
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.toggleGrid).toHaveBeenCalledWith(false)
|
||||
expect(mockLoad3d.setBackgroundColor).toHaveBeenCalledWith('#ffffff')
|
||||
expect(mockLoad3d.setBackgroundImage).toHaveBeenCalledWith('test.jpg')
|
||||
expect(mockLoad3d.setBackgroundRenderMode).toHaveBeenCalledWith(
|
||||
'panorama'
|
||||
)
|
||||
expect(mockNode.properties['Scene Config']).toEqual({
|
||||
showGrid: false,
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundImage: 'test.jpg'
|
||||
backgroundImage: 'test.jpg',
|
||||
backgroundRenderMode: 'panorama'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -375,6 +385,10 @@ describe('useLoad3d', () => {
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await composable.initializeLoad3d(containerRef)
|
||||
await nextTick()
|
||||
|
||||
mockLoad3d.setUpDirection.mockClear()
|
||||
mockLoad3d.setMaterialMode.mockClear()
|
||||
|
||||
composable.modelConfig.value.upDirection = '+y'
|
||||
composable.modelConfig.value.materialMode = 'wireframe'
|
||||
@@ -393,6 +407,10 @@ describe('useLoad3d', () => {
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await composable.initializeLoad3d(containerRef)
|
||||
await nextTick()
|
||||
|
||||
mockLoad3d.toggleCamera.mockClear()
|
||||
mockLoad3d.setFOV.mockClear()
|
||||
|
||||
composable.cameraConfig.value.cameraType = 'orthographic'
|
||||
composable.cameraConfig.value.fov = 90
|
||||
@@ -412,6 +430,9 @@ describe('useLoad3d', () => {
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await composable.initializeLoad3d(containerRef)
|
||||
await nextTick()
|
||||
|
||||
mockLoad3d.setLightIntensity.mockClear()
|
||||
|
||||
composable.lightConfig.value.intensity = 10
|
||||
await nextTick()
|
||||
@@ -652,6 +673,7 @@ describe('useLoad3d', () => {
|
||||
const expectedEvents = [
|
||||
'materialModeChange',
|
||||
'backgroundColorChange',
|
||||
'backgroundRenderModeChange',
|
||||
'lightIntensityChange',
|
||||
'fovChange',
|
||||
'cameraTypeChange',
|
||||
|
||||
@@ -44,7 +44,8 @@ describe('useLoad3dViewer', () => {
|
||||
'Scene Config': {
|
||||
backgroundColor: '#282828',
|
||||
showGrid: true,
|
||||
backgroundImage: ''
|
||||
backgroundImage: '',
|
||||
backgroundRenderMode: 'tiled'
|
||||
},
|
||||
'Camera Config': {
|
||||
cameraType: 'perspective',
|
||||
@@ -115,6 +116,7 @@ describe('useLoad3dViewer', () => {
|
||||
materialMode: 'original'
|
||||
},
|
||||
setBackgroundImage: vi.fn().mockResolvedValue(undefined),
|
||||
setBackgroundRenderMode: vi.fn(),
|
||||
forceRender: vi.fn()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user