mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
[3d] refactor load3d nodes (#2683)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -63,8 +63,8 @@ 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'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { AnimationItem } from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const props = defineProps<{
|
||||
node: any
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<div ref="container" class="w-full h-full"></div>
|
||||
<div ref="container" class="w-full h-full relative">
|
||||
<LoadingOverlay ref="loadingOverlayRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { onMounted, onUnmounted, ref, toRaw, watchEffect } from 'vue'
|
||||
|
||||
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
@@ -26,6 +29,7 @@ const props = defineProps<{
|
||||
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),
|
||||
@@ -36,7 +40,10 @@ const eventConfig = {
|
||||
cameraTypeChange: (value: string) => emit('cameraTypeChange', value),
|
||||
showGridChange: (value: boolean) => emit('showGridChange', value),
|
||||
showPreviewChange: (value: boolean) => emit('showPreviewChange', value),
|
||||
backgroundImageChange: (value: string) => emit('backgroundImageChange', value)
|
||||
backgroundImageChange: (value: string) =>
|
||||
emit('backgroundImageChange', value),
|
||||
modelLoadingStart: () => loadingOverlayRef.value?.startLoading(),
|
||||
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading()
|
||||
} as const
|
||||
|
||||
watchEffect(() => {
|
||||
|
||||
56
src/components/load3d/LoadingOverlay.vue
Normal file
56
src/components/load3d/LoadingOverlay.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="modelLoading"
|
||||
class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="spinner"></div>
|
||||
<div class="text-white mt-4 text-lg">
|
||||
{{ t('load3d.loadingModel') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const modelLoading = ref(false)
|
||||
|
||||
const startLoading = () => {
|
||||
modelLoading.value = true
|
||||
}
|
||||
|
||||
const endLoading = () => {
|
||||
modelLoading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
startLoading,
|
||||
endLoading
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -174,6 +174,8 @@ app.registerExtension({
|
||||
Load3dUtils.uploadTempImage(maskData, 'scene_mask')
|
||||
])
|
||||
|
||||
load3d.handleResize()
|
||||
|
||||
return {
|
||||
image: `threed/${data.name} [temp]`,
|
||||
mask: `threed/${dataMask.name} [temp]`
|
||||
@@ -345,6 +347,8 @@ app.registerExtension({
|
||||
Load3dUtils.uploadTempImage(maskData, 'scene_mask')
|
||||
])
|
||||
|
||||
load3d.handleResize()
|
||||
|
||||
return {
|
||||
image: `threed/${data.name} [temp]`,
|
||||
mask: `threed/${dataMask.name} [temp]`
|
||||
|
||||
161
src/extensions/core/load3d/AnimationManager.ts
Normal file
161
src/extensions/core/load3d/AnimationManager.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import {
|
||||
AnimationItem,
|
||||
AnimationManagerInterface,
|
||||
EventManagerInterface
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
export class AnimationManager implements AnimationManagerInterface {
|
||||
currentAnimation: THREE.AnimationMixer | null = null
|
||||
animationActions: THREE.AnimationAction[] = []
|
||||
animationClips: THREE.AnimationClip[] = []
|
||||
selectedAnimationIndex: number = 0
|
||||
isAnimationPlaying: boolean = false
|
||||
animationSpeed: number = 1.0
|
||||
|
||||
private eventManager: EventManagerInterface
|
||||
private getCurrentModel: () => THREE.Object3D | null
|
||||
|
||||
constructor(
|
||||
eventManager: EventManagerInterface,
|
||||
getCurrentModel: () => THREE.Object3D | null
|
||||
) {
|
||||
this.eventManager = eventManager
|
||||
this.getCurrentModel = getCurrentModel
|
||||
}
|
||||
|
||||
init(): void {}
|
||||
|
||||
dispose(): void {
|
||||
if (this.currentAnimation) {
|
||||
this.animationActions.forEach((action) => {
|
||||
action.stop()
|
||||
})
|
||||
this.currentAnimation = null
|
||||
}
|
||||
this.animationActions = []
|
||||
this.animationClips = []
|
||||
this.selectedAnimationIndex = 0
|
||||
this.isAnimationPlaying = false
|
||||
this.animationSpeed = 1.0
|
||||
|
||||
this.eventManager.emitEvent('animationListChange', [])
|
||||
}
|
||||
|
||||
setupModelAnimations(model: THREE.Object3D, originalModel: any): void {
|
||||
if (this.currentAnimation) {
|
||||
this.currentAnimation.stopAllAction()
|
||||
this.animationActions = []
|
||||
}
|
||||
|
||||
let animations: THREE.AnimationClip[] = []
|
||||
if (model.animations?.length > 0) {
|
||||
animations = model.animations
|
||||
} else if (originalModel && 'animations' in originalModel) {
|
||||
animations = originalModel.animations
|
||||
}
|
||||
|
||||
if (animations.length > 0) {
|
||||
this.animationClips = animations
|
||||
if (model.type === 'Scene') {
|
||||
this.currentAnimation = new THREE.AnimationMixer(model)
|
||||
} else {
|
||||
this.currentAnimation = new THREE.AnimationMixer(
|
||||
this.getCurrentModel()!
|
||||
)
|
||||
}
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.updateSelectedAnimation(0)
|
||||
}
|
||||
}
|
||||
|
||||
this.updateAnimationList()
|
||||
}
|
||||
|
||||
updateAnimationList(): void {
|
||||
let updatedAnimationList: AnimationItem[] = []
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
updatedAnimationList = this.animationClips.map((clip, index) => ({
|
||||
name: clip.name || `Animation ${index + 1}`,
|
||||
index
|
||||
}))
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('animationListChange', updatedAnimationList)
|
||||
}
|
||||
|
||||
setAnimationSpeed(speed: number): void {
|
||||
this.animationSpeed = speed
|
||||
this.animationActions.forEach((action) => {
|
||||
action.setEffectiveTimeScale(speed)
|
||||
})
|
||||
}
|
||||
|
||||
updateSelectedAnimation(index: number): void {
|
||||
if (
|
||||
!this.currentAnimation ||
|
||||
!this.animationClips ||
|
||||
index >= this.animationClips.length
|
||||
) {
|
||||
console.warn('Invalid animation update request')
|
||||
return
|
||||
}
|
||||
|
||||
this.animationActions.forEach((action) => {
|
||||
action.stop()
|
||||
})
|
||||
this.currentAnimation.stopAllAction()
|
||||
this.animationActions = []
|
||||
|
||||
this.selectedAnimationIndex = index
|
||||
const clip = this.animationClips[index]
|
||||
|
||||
const action = this.currentAnimation.clipAction(clip)
|
||||
|
||||
action.setEffectiveTimeScale(this.animationSpeed)
|
||||
|
||||
action.reset()
|
||||
action.clampWhenFinished = false
|
||||
action.loop = THREE.LoopRepeat
|
||||
|
||||
if (this.isAnimationPlaying) {
|
||||
action.play()
|
||||
} else {
|
||||
action.play()
|
||||
action.paused = true
|
||||
}
|
||||
|
||||
this.animationActions = [action]
|
||||
}
|
||||
|
||||
toggleAnimation(play?: boolean): void {
|
||||
if (!this.currentAnimation || this.animationActions.length === 0) {
|
||||
console.warn('No animation to toggle')
|
||||
return
|
||||
}
|
||||
|
||||
this.isAnimationPlaying = play ?? !this.isAnimationPlaying
|
||||
|
||||
this.animationActions.forEach((action) => {
|
||||
if (this.isAnimationPlaying) {
|
||||
action.paused = false
|
||||
if (action.time === 0 || action.time === action.getClip().duration) {
|
||||
action.reset()
|
||||
}
|
||||
} else {
|
||||
action.paused = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
update(delta: number): void {
|
||||
if (this.currentAnimation && this.isAnimationPlaying) {
|
||||
this.currentAnimation.update(delta)
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {}
|
||||
}
|
||||
234
src/extensions/core/load3d/CameraManager.ts
Normal file
234
src/extensions/core/load3d/CameraManager.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import {
|
||||
CameraManagerInterface,
|
||||
CameraState,
|
||||
CameraType,
|
||||
EventManagerInterface,
|
||||
NodeStorageInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class CameraManager implements CameraManagerInterface {
|
||||
perspectiveCamera: THREE.PerspectiveCamera
|
||||
orthographicCamera: THREE.OrthographicCamera
|
||||
activeCamera: THREE.Camera
|
||||
|
||||
private renderer: THREE.WebGLRenderer
|
||||
private eventManager: EventManagerInterface
|
||||
private nodeStorage: NodeStorageInterface
|
||||
|
||||
private controls: OrbitControls | null = null
|
||||
|
||||
DEFAULT_DISTANCE = 10
|
||||
DEFAULT_LOOK_AT = 0
|
||||
|
||||
DEFAULT_CAMERA = {
|
||||
near: 0.01,
|
||||
far: 10000
|
||||
}
|
||||
|
||||
DEFAULT_PERSPECTIVE_CAMERA = {
|
||||
fov: 75,
|
||||
aspect: 1
|
||||
}
|
||||
|
||||
DEFAULT_FRUSTUM_SIZE = 10
|
||||
|
||||
DEFAULT_ORTHOGRAPHIC_CAMERA = {
|
||||
left: -this.DEFAULT_FRUSTUM_SIZE / 2,
|
||||
right: this.DEFAULT_FRUSTUM_SIZE / 2,
|
||||
top: this.DEFAULT_FRUSTUM_SIZE / 2,
|
||||
bottom: -this.DEFAULT_FRUSTUM_SIZE / 2
|
||||
}
|
||||
|
||||
constructor(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
eventManager: EventManagerInterface,
|
||||
nodeStorage: NodeStorageInterface
|
||||
) {
|
||||
this.renderer = renderer
|
||||
this.eventManager = eventManager
|
||||
this.nodeStorage = nodeStorage
|
||||
|
||||
this.perspectiveCamera = new THREE.PerspectiveCamera(
|
||||
this.DEFAULT_PERSPECTIVE_CAMERA.fov,
|
||||
this.DEFAULT_PERSPECTIVE_CAMERA.aspect,
|
||||
this.DEFAULT_CAMERA.near,
|
||||
this.DEFAULT_CAMERA.far
|
||||
)
|
||||
|
||||
this.orthographicCamera = new THREE.OrthographicCamera(
|
||||
this.DEFAULT_ORTHOGRAPHIC_CAMERA.left,
|
||||
this.DEFAULT_ORTHOGRAPHIC_CAMERA.right,
|
||||
this.DEFAULT_ORTHOGRAPHIC_CAMERA.top,
|
||||
this.DEFAULT_ORTHOGRAPHIC_CAMERA.bottom,
|
||||
this.DEFAULT_CAMERA.near,
|
||||
this.DEFAULT_CAMERA.far
|
||||
)
|
||||
|
||||
this.reset()
|
||||
|
||||
this.activeCamera = this.perspectiveCamera
|
||||
}
|
||||
|
||||
init(): void {}
|
||||
|
||||
dispose(): void {}
|
||||
|
||||
setControls(controls: OrbitControls): void {
|
||||
this.controls = controls
|
||||
|
||||
if (this.controls) {
|
||||
this.controls.addEventListener('end', () => {
|
||||
this.nodeStorage.storeNodeProperty('Camera Info', this.getCameraState())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentCameraType(): CameraType {
|
||||
return this.activeCamera === this.perspectiveCamera
|
||||
? 'perspective'
|
||||
: 'orthographic'
|
||||
}
|
||||
|
||||
toggleCamera(cameraType?: CameraType): void {
|
||||
const oldCamera = this.activeCamera
|
||||
|
||||
const position = oldCamera.position.clone()
|
||||
const rotation = oldCamera.rotation.clone()
|
||||
const target = this.controls?.target.clone() || new THREE.Vector3()
|
||||
|
||||
if (!cameraType) {
|
||||
this.activeCamera =
|
||||
oldCamera === this.perspectiveCamera
|
||||
? this.orthographicCamera
|
||||
: this.perspectiveCamera
|
||||
} else {
|
||||
this.activeCamera =
|
||||
cameraType === 'perspective'
|
||||
? this.perspectiveCamera
|
||||
: this.orthographicCamera
|
||||
|
||||
if (oldCamera === this.activeCamera) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.activeCamera.position.copy(position)
|
||||
this.activeCamera.rotation.copy(rotation)
|
||||
|
||||
if (this.controls) {
|
||||
this.controls.object = this.activeCamera
|
||||
this.controls.target.copy(target)
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('cameraTypeChange', cameraType)
|
||||
}
|
||||
|
||||
setFOV(fov: number): void {
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.fov = fov
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('fovChange', fov)
|
||||
}
|
||||
|
||||
getCameraState(): CameraState {
|
||||
return {
|
||||
position: this.activeCamera.position.clone(),
|
||||
target: this.controls?.target.clone() || new THREE.Vector3(),
|
||||
zoom:
|
||||
this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? this.activeCamera.zoom
|
||||
: (this.activeCamera as THREE.PerspectiveCamera).zoom,
|
||||
cameraType: this.getCurrentCameraType()
|
||||
}
|
||||
}
|
||||
|
||||
setCameraState(state: CameraState): void {
|
||||
this.activeCamera.position.copy(state.position)
|
||||
|
||||
this.controls?.target.copy(state.target)
|
||||
|
||||
if (this.activeCamera instanceof THREE.OrthographicCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.controls?.update()
|
||||
}
|
||||
|
||||
handleResize(width: number, height: number): void {
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.aspect = width / height
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = 10
|
||||
const aspect = width / height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
}
|
||||
|
||||
setupForModel(size: THREE.Vector3): void {
|
||||
const distance = Math.max(size.x, size.z) * 2
|
||||
const height = size.y * 2
|
||||
|
||||
this.perspectiveCamera.position.set(distance, height, distance)
|
||||
this.orthographicCamera.position.set(distance, height, distance)
|
||||
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.lookAt(0, size.y / 2, 0)
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = Math.max(size.x, size.y, size.z) * 2
|
||||
const aspect = this.perspectiveCamera.aspect
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
this.orthographicCamera.lookAt(0, size.y / 2, 0)
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.controls?.target.set(0, size.y / 2, 0)
|
||||
this.controls?.update()
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.perspectiveCamera.position.set(
|
||||
this.DEFAULT_DISTANCE,
|
||||
this.DEFAULT_DISTANCE,
|
||||
this.DEFAULT_DISTANCE
|
||||
)
|
||||
|
||||
this.orthographicCamera.position.set(
|
||||
this.DEFAULT_DISTANCE,
|
||||
this.DEFAULT_DISTANCE,
|
||||
this.DEFAULT_DISTANCE
|
||||
)
|
||||
|
||||
this.perspectiveCamera.lookAt(
|
||||
this.DEFAULT_LOOK_AT,
|
||||
this.DEFAULT_LOOK_AT,
|
||||
this.DEFAULT_LOOK_AT
|
||||
)
|
||||
this.orthographicCamera.lookAt(
|
||||
this.DEFAULT_LOOK_AT,
|
||||
this.DEFAULT_LOOK_AT,
|
||||
this.DEFAULT_LOOK_AT
|
||||
)
|
||||
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
}
|
||||
72
src/extensions/core/load3d/ControlsManager.ts
Normal file
72
src/extensions/core/load3d/ControlsManager.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import {
|
||||
ControlsManagerInterface,
|
||||
EventManagerInterface,
|
||||
NodeStorageInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class ControlsManager implements ControlsManagerInterface {
|
||||
controls: OrbitControls
|
||||
|
||||
private eventManager: EventManagerInterface
|
||||
private nodeStorage: NodeStorageInterface
|
||||
private camera: THREE.Camera
|
||||
|
||||
constructor(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
camera: THREE.Camera,
|
||||
eventManager: EventManagerInterface,
|
||||
nodeStorage: NodeStorageInterface
|
||||
) {
|
||||
this.eventManager = eventManager
|
||||
this.nodeStorage = nodeStorage
|
||||
this.camera = camera
|
||||
|
||||
this.controls = new OrbitControls(camera, renderer.domElement)
|
||||
this.controls.enableDamping = true
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.controls.addEventListener('end', () => {
|
||||
this.nodeStorage.storeNodeProperty('Camera Info', {
|
||||
position: this.camera.position.clone(),
|
||||
target: this.controls.target.clone(),
|
||||
zoom:
|
||||
this.camera instanceof THREE.OrthographicCamera
|
||||
? (this.camera as THREE.OrthographicCamera).zoom
|
||||
: (this.camera as THREE.PerspectiveCamera).zoom,
|
||||
cameraType:
|
||||
this.camera instanceof THREE.PerspectiveCamera
|
||||
? 'perspective'
|
||||
: 'orthographic'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.controls.dispose()
|
||||
}
|
||||
|
||||
handleResize(): void {}
|
||||
|
||||
update(): void {
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
updateCamera(camera: THREE.Camera): void {
|
||||
const position = this.controls.object.position.clone()
|
||||
const target = this.controls.target.clone()
|
||||
|
||||
this.camera = camera
|
||||
this.controls.object = camera
|
||||
this.controls.target = target
|
||||
camera.position.copy(position)
|
||||
this.controls.update()
|
||||
}
|
||||
reset(): void {
|
||||
this.controls.target.set(0, 0, 0)
|
||||
this.controls.update()
|
||||
}
|
||||
}
|
||||
26
src/extensions/core/load3d/EventManager.ts
Normal file
26
src/extensions/core/load3d/EventManager.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { EventCallback, EventManagerInterface } from './interfaces'
|
||||
|
||||
export class EventManager implements EventManagerInterface {
|
||||
private listeners: { [key: string]: EventCallback[] } = {}
|
||||
|
||||
addEventListener(event: string, callback: EventCallback): void {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = []
|
||||
}
|
||||
this.listeners[event].push(callback)
|
||||
}
|
||||
|
||||
removeEventListener(event: string, callback: EventCallback): void {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event] = this.listeners[event].filter(
|
||||
(cb) => cb !== callback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
emitEvent(event: string, data?: any): void {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach((callback) => callback(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/extensions/core/load3d/LightingManager.ts
Normal file
78
src/extensions/core/load3d/LightingManager.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { EventManagerInterface, LightingManagerInterface } from './interfaces'
|
||||
|
||||
export class LightingManager implements LightingManagerInterface {
|
||||
lights: THREE.Light[] = []
|
||||
private scene: THREE.Scene
|
||||
private eventManager: EventManagerInterface
|
||||
|
||||
constructor(scene: THREE.Scene, eventManager: EventManagerInterface) {
|
||||
this.scene = scene
|
||||
this.eventManager = eventManager
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.setupLights()
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.lights.forEach((light) => {
|
||||
this.scene.remove(light)
|
||||
})
|
||||
this.lights = []
|
||||
}
|
||||
|
||||
setupLights(): void {
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
|
||||
this.scene.add(ambientLight)
|
||||
this.lights.push(ambientLight)
|
||||
|
||||
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
mainLight.position.set(0, 10, 10)
|
||||
this.scene.add(mainLight)
|
||||
this.lights.push(mainLight)
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.5)
|
||||
backLight.position.set(0, 10, -10)
|
||||
this.scene.add(backLight)
|
||||
this.lights.push(backLight)
|
||||
|
||||
const leftFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
|
||||
leftFillLight.position.set(-10, 0, 0)
|
||||
this.scene.add(leftFillLight)
|
||||
this.lights.push(leftFillLight)
|
||||
|
||||
const rightFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
|
||||
rightFillLight.position.set(10, 0, 0)
|
||||
this.scene.add(rightFillLight)
|
||||
this.lights.push(rightFillLight)
|
||||
|
||||
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.2)
|
||||
bottomLight.position.set(0, -10, 0)
|
||||
this.scene.add(bottomLight)
|
||||
this.lights.push(bottomLight)
|
||||
}
|
||||
|
||||
setLightIntensity(intensity: number): void {
|
||||
this.lights.forEach((light) => {
|
||||
if (light instanceof THREE.DirectionalLight) {
|
||||
if (light === this.lights[1]) {
|
||||
light.intensity = intensity * 0.8
|
||||
} else if (light === this.lights[2]) {
|
||||
light.intensity = intensity * 0.5
|
||||
} else if (light === this.lights[5]) {
|
||||
light.intensity = intensity * 0.2
|
||||
} else {
|
||||
light.intensity = intensity * 0.3
|
||||
}
|
||||
} else if (light instanceof THREE.AmbientLight) {
|
||||
light.intensity = intensity * 0.5
|
||||
}
|
||||
})
|
||||
|
||||
this.eventManager.emitEvent('lightIntensityChange', intensity)
|
||||
}
|
||||
|
||||
reset(): void {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,206 +1,130 @@
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
|
||||
interface AnimationItem {
|
||||
name: string
|
||||
index: number
|
||||
}
|
||||
import { AnimationManager } from './AnimationManager'
|
||||
import Load3d from './Load3d'
|
||||
import { Load3DOptions } from './interfaces'
|
||||
|
||||
class Load3dAnimation extends Load3d {
|
||||
currentAnimation: THREE.AnimationMixer | null = null
|
||||
animationActions: THREE.AnimationAction[] = []
|
||||
animationClips: THREE.AnimationClip[] = []
|
||||
selectedAnimationIndex: number = 0
|
||||
isAnimationPlaying: boolean = false
|
||||
|
||||
animationSpeed: number = 1.0
|
||||
private animationManager: AnimationManager
|
||||
|
||||
constructor(
|
||||
container: Element | HTMLElement,
|
||||
options: { createPreview?: boolean } = {}
|
||||
options: Load3DOptions = {
|
||||
node: {} as LGraphNode
|
||||
}
|
||||
) {
|
||||
super(container, options)
|
||||
|
||||
this.animationManager = new AnimationManager(
|
||||
this.eventManager,
|
||||
this.getCurrentModel.bind(this)
|
||||
)
|
||||
|
||||
this.animationManager.init()
|
||||
|
||||
this.overrideAnimationLoop()
|
||||
}
|
||||
|
||||
updateAnimationList() {
|
||||
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)
|
||||
private getCurrentModel(): THREE.Object3D | null {
|
||||
return this.modelManager.currentModel
|
||||
}
|
||||
|
||||
protected async setupModel(model: THREE.Object3D) {
|
||||
await super.setupModel(model)
|
||||
|
||||
if (this.currentAnimation) {
|
||||
this.currentAnimation.stopAllAction()
|
||||
this.animationActions = []
|
||||
private overrideAnimationLoop(): void {
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId)
|
||||
}
|
||||
|
||||
let animations: THREE.AnimationClip[] = []
|
||||
if (model.animations?.length > 0) {
|
||||
animations = model.animations
|
||||
} else if (this.originalModel && 'animations' in this.originalModel) {
|
||||
animations = (
|
||||
this.originalModel as unknown as { animations: THREE.AnimationClip[] }
|
||||
).animations
|
||||
}
|
||||
|
||||
if (animations.length > 0) {
|
||||
this.animationClips = animations
|
||||
if (model.type === 'Scene') {
|
||||
this.currentAnimation = new THREE.AnimationMixer(model)
|
||||
} else {
|
||||
this.currentAnimation = new THREE.AnimationMixer(this.currentModel!)
|
||||
}
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.updateSelectedAnimation(0)
|
||||
}
|
||||
}
|
||||
|
||||
this.updateAnimationList()
|
||||
}
|
||||
|
||||
setAnimationSpeed(speed: number) {
|
||||
this.animationSpeed = speed
|
||||
this.animationActions.forEach((action) => {
|
||||
action.setEffectiveTimeScale(speed)
|
||||
})
|
||||
}
|
||||
|
||||
updateSelectedAnimation(index: number) {
|
||||
if (
|
||||
!this.currentAnimation ||
|
||||
!this.animationClips ||
|
||||
index >= this.animationClips.length
|
||||
) {
|
||||
console.warn('Invalid animation update request')
|
||||
return
|
||||
}
|
||||
|
||||
this.animationActions.forEach((action) => {
|
||||
action.stop()
|
||||
})
|
||||
this.currentAnimation.stopAllAction()
|
||||
this.animationActions = []
|
||||
|
||||
this.selectedAnimationIndex = index
|
||||
const clip = this.animationClips[index]
|
||||
|
||||
const action = this.currentAnimation.clipAction(clip)
|
||||
|
||||
action.setEffectiveTimeScale(this.animationSpeed)
|
||||
|
||||
action.reset()
|
||||
action.clampWhenFinished = false
|
||||
action.loop = THREE.LoopRepeat
|
||||
|
||||
if (this.isAnimationPlaying) {
|
||||
action.play()
|
||||
} else {
|
||||
action.play()
|
||||
action.paused = true
|
||||
}
|
||||
|
||||
this.animationActions = [action]
|
||||
}
|
||||
|
||||
clearModel() {
|
||||
if (this.currentAnimation) {
|
||||
this.animationActions.forEach((action) => {
|
||||
action.stop()
|
||||
})
|
||||
this.currentAnimation = null
|
||||
}
|
||||
this.animationActions = []
|
||||
this.animationClips = []
|
||||
this.selectedAnimationIndex = 0
|
||||
this.isAnimationPlaying = false
|
||||
this.animationSpeed = 1.0
|
||||
|
||||
this.emitEvent('animationListChange', [])
|
||||
|
||||
super.clearModel()
|
||||
}
|
||||
|
||||
toggleAnimation(play?: boolean) {
|
||||
if (!this.currentAnimation || this.animationActions.length === 0) {
|
||||
console.warn('No animation to toggle')
|
||||
return
|
||||
}
|
||||
|
||||
this.isAnimationPlaying = play ?? !this.isAnimationPlaying
|
||||
|
||||
this.animationActions.forEach((action) => {
|
||||
if (this.isAnimationPlaying) {
|
||||
action.paused = false
|
||||
if (action.time === 0 || action.time === action.getClip().duration) {
|
||||
action.reset()
|
||||
}
|
||||
} else {
|
||||
action.paused = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
|
||||
if (this.showPreview) {
|
||||
this.updatePreviewRender()
|
||||
if (this.previewManager.showPreview) {
|
||||
this.previewManager.updatePreviewRender()
|
||||
}
|
||||
|
||||
const delta = this.clock.getDelta()
|
||||
|
||||
if (this.currentAnimation && this.isAnimationPlaying) {
|
||||
this.currentAnimation.update(delta)
|
||||
}
|
||||
this.animationManager.update(delta)
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
this.viewHelperManager.update(delta)
|
||||
|
||||
if (!this.viewHelper.animating) {
|
||||
this.storeNodeProperty('Camera Info', this.getCameraState())
|
||||
}
|
||||
}
|
||||
this.controlsManager.update()
|
||||
|
||||
this.renderer.clear()
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.render(
|
||||
this.sceneManager.scene,
|
||||
this.cameraManager.activeCamera
|
||||
)
|
||||
|
||||
if (this.backgroundMesh && this.backgroundTexture) {
|
||||
const material = this.backgroundMesh.material as THREE.MeshBasicMaterial
|
||||
if (material.map) {
|
||||
const currentToneMapping = this.renderer.toneMapping
|
||||
const currentExposure = this.renderer.toneMappingExposure
|
||||
|
||||
this.renderer.toneMapping = THREE.NoToneMapping
|
||||
|
||||
this.renderer.render(this.backgroundScene, this.backgroundCamera)
|
||||
|
||||
this.renderer.toneMapping = currentToneMapping
|
||||
this.renderer.toneMappingExposure = currentExposure
|
||||
}
|
||||
if (this.viewHelperManager.viewHelper.render) {
|
||||
this.viewHelperManager.viewHelper.render(this.renderer)
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
|
||||
animate()
|
||||
}
|
||||
|
||||
async loadModel(url: string, originalFileName?: string): Promise<void> {
|
||||
await super.loadModel(url, originalFileName)
|
||||
|
||||
if (this.modelManager.currentModel) {
|
||||
this.animationManager.setupModelAnimations(
|
||||
this.modelManager.currentModel,
|
||||
this.modelManager.originalModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
clearModel(): void {
|
||||
this.animationManager.dispose()
|
||||
super.clearModel()
|
||||
}
|
||||
|
||||
updateAnimationList(): void {
|
||||
this.animationManager.updateAnimationList()
|
||||
}
|
||||
|
||||
setAnimationSpeed(speed: number): void {
|
||||
this.animationManager.setAnimationSpeed(speed)
|
||||
}
|
||||
|
||||
updateSelectedAnimation(index: number): void {
|
||||
this.animationManager.updateSelectedAnimation(index)
|
||||
}
|
||||
|
||||
toggleAnimation(play?: boolean): void {
|
||||
this.animationManager.toggleAnimation(play)
|
||||
}
|
||||
|
||||
get isAnimationPlaying(): boolean {
|
||||
return this.animationManager.isAnimationPlaying
|
||||
}
|
||||
|
||||
get animationSpeed(): number {
|
||||
return this.animationManager.animationSpeed
|
||||
}
|
||||
|
||||
get selectedAnimationIndex(): number {
|
||||
return this.animationManager.selectedAnimationIndex
|
||||
}
|
||||
|
||||
get animationClips(): THREE.AnimationClip[] {
|
||||
return this.animationManager.animationClips
|
||||
}
|
||||
|
||||
get animationActions(): THREE.AnimationAction[] {
|
||||
return this.animationManager.animationActions
|
||||
}
|
||||
|
||||
get currentAnimation(): THREE.AnimationMixer | null {
|
||||
return this.animationManager.currentAnimation
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.animationManager.dispose()
|
||||
super.remove()
|
||||
}
|
||||
}
|
||||
|
||||
export { AnimationItem }
|
||||
export default Load3dAnimation
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
150
src/extensions/core/load3d/LoaderManager.ts
Normal file
150
src/extensions/core/load3d/LoaderManager.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import * as THREE from 'three'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
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 { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
import {
|
||||
EventManagerInterface,
|
||||
LoaderManagerInterface,
|
||||
ModelManagerInterface
|
||||
} from './interfaces'
|
||||
|
||||
export class LoaderManager implements LoaderManagerInterface {
|
||||
gltfLoader: GLTFLoader
|
||||
objLoader: OBJLoader
|
||||
mtlLoader: MTLLoader
|
||||
fbxLoader: FBXLoader
|
||||
stlLoader: STLLoader
|
||||
|
||||
private modelManager: ModelManagerInterface
|
||||
private eventManager: EventManagerInterface
|
||||
|
||||
constructor(
|
||||
modelManager: ModelManagerInterface,
|
||||
eventManager: EventManagerInterface
|
||||
) {
|
||||
this.modelManager = modelManager
|
||||
this.eventManager = eventManager
|
||||
|
||||
this.gltfLoader = new GLTFLoader()
|
||||
this.objLoader = new OBJLoader()
|
||||
this.mtlLoader = new MTLLoader()
|
||||
this.fbxLoader = new FBXLoader()
|
||||
this.stlLoader = new STLLoader()
|
||||
}
|
||||
|
||||
init(): void {}
|
||||
|
||||
dispose(): void {}
|
||||
|
||||
async loadModel(url: string, originalFileName?: string): Promise<void> {
|
||||
try {
|
||||
this.eventManager.emitEvent('modelLoadingStart', null)
|
||||
|
||||
this.modelManager.clearModel()
|
||||
|
||||
let fileExtension: string | undefined
|
||||
if (originalFileName) {
|
||||
fileExtension = originalFileName.split('.').pop()?.toLowerCase()
|
||||
} else {
|
||||
const filename = new URLSearchParams(url.split('?')[1]).get('filename')
|
||||
fileExtension = filename?.split('.').pop()?.toLowerCase()
|
||||
}
|
||||
|
||||
if (!fileExtension) {
|
||||
useToastStore().addAlert('Could not determine file type')
|
||||
return
|
||||
}
|
||||
|
||||
let model = await this.loadModelInternal(url, fileExtension)
|
||||
|
||||
if (model) {
|
||||
await this.modelManager.setupModel(model)
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('modelLoadingEnd', null)
|
||||
} catch (error) {
|
||||
this.eventManager.emitEvent('modelLoadingEnd', null)
|
||||
console.error('Error loading model:', error)
|
||||
useToastStore().addAlert('Error loading model')
|
||||
}
|
||||
}
|
||||
|
||||
private async loadModelInternal(
|
||||
url: string,
|
||||
fileExtension: string
|
||||
): Promise<THREE.Object3D | null> {
|
||||
let model: THREE.Object3D | null = null
|
||||
|
||||
switch (fileExtension) {
|
||||
case 'stl':
|
||||
const geometry = await this.stlLoader.loadAsync(url)
|
||||
this.modelManager.setOriginalModel(geometry)
|
||||
geometry.computeVertexNormals()
|
||||
|
||||
const mesh = new THREE.Mesh(
|
||||
geometry,
|
||||
this.modelManager.standardMaterial
|
||||
)
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.add(mesh)
|
||||
model = group
|
||||
break
|
||||
|
||||
case 'fbx':
|
||||
const fbxModel = await this.fbxLoader.loadAsync(url)
|
||||
this.modelManager.setOriginalModel(fbxModel)
|
||||
model = fbxModel
|
||||
|
||||
fbxModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.modelManager.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'obj':
|
||||
if (this.modelManager.materialMode === 'original') {
|
||||
const mtlUrl = url.replace(/\.obj([^.]*$)/, '.mtl$1')
|
||||
try {
|
||||
const materials = await this.mtlLoader.loadAsync(mtlUrl)
|
||||
materials.preload()
|
||||
this.objLoader.setMaterials(materials)
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'No MTL file found or error loading it, continuing without materials'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
model = await this.objLoader.loadAsync(url)
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.modelManager.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'gltf':
|
||||
case 'glb':
|
||||
const gltf = await this.gltfLoader.loadAsync(url)
|
||||
this.modelManager.setOriginalModel(gltf)
|
||||
model = gltf.scene
|
||||
|
||||
gltf.scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry.computeVertexNormals()
|
||||
this.modelManager.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
}
|
||||
313
src/extensions/core/load3d/ModelManager.ts
Normal file
313
src/extensions/core/load3d/ModelManager.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import * as THREE from 'three'
|
||||
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
|
||||
import {
|
||||
EventManagerInterface,
|
||||
MaterialMode,
|
||||
ModelManagerInterface,
|
||||
UpDirection
|
||||
} from './interfaces'
|
||||
|
||||
export class ModelManager implements ModelManagerInterface {
|
||||
currentModel: THREE.Object3D | null = null
|
||||
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
|
||||
originalRotation: THREE.Euler | null = null
|
||||
currentUpDirection: UpDirection = 'original'
|
||||
materialMode: MaterialMode = 'original'
|
||||
originalMaterials: WeakMap<THREE.Mesh, THREE.Material | THREE.Material[]> =
|
||||
new WeakMap()
|
||||
normalMaterial: THREE.MeshNormalMaterial
|
||||
standardMaterial: THREE.MeshStandardMaterial
|
||||
wireframeMaterial: THREE.MeshBasicMaterial
|
||||
depthMaterial: THREE.MeshDepthMaterial
|
||||
|
||||
private scene: THREE.Scene
|
||||
private renderer: THREE.WebGLRenderer
|
||||
private eventManager: EventManagerInterface
|
||||
private activeCamera: THREE.Camera
|
||||
private setupCamera: (size: THREE.Vector3) => void
|
||||
|
||||
constructor(
|
||||
scene: THREE.Scene,
|
||||
renderer: THREE.WebGLRenderer,
|
||||
eventManager: EventManagerInterface,
|
||||
getActiveCamera: () => THREE.Camera,
|
||||
setupCamera: (size: THREE.Vector3) => void
|
||||
) {
|
||||
this.scene = scene
|
||||
this.renderer = renderer
|
||||
this.eventManager = eventManager
|
||||
this.activeCamera = getActiveCamera()
|
||||
this.setupCamera = setupCamera
|
||||
|
||||
this.normalMaterial = new THREE.MeshNormalMaterial({
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide,
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
|
||||
this.wireframeMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
wireframe: true,
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
|
||||
this.depthMaterial = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.BasicDepthPacking,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
this.standardMaterial = this.createSTLMaterial()
|
||||
}
|
||||
|
||||
init(): void {}
|
||||
|
||||
dispose(): void {
|
||||
this.clearModel()
|
||||
this.normalMaterial.dispose()
|
||||
this.standardMaterial.dispose()
|
||||
this.wireframeMaterial.dispose()
|
||||
this.depthMaterial.dispose()
|
||||
}
|
||||
|
||||
createSTLMaterial(): THREE.MeshStandardMaterial {
|
||||
return new THREE.MeshStandardMaterial({
|
||||
color: 0x808080,
|
||||
metalness: 0.1,
|
||||
roughness: 0.8,
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
}
|
||||
|
||||
setMaterialMode(mode: MaterialMode): void {
|
||||
this.materialMode = mode
|
||||
|
||||
if (!this.currentModel) return
|
||||
|
||||
if (mode === 'depth') {
|
||||
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
} else {
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
}
|
||||
|
||||
this.currentModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
switch (mode) {
|
||||
case 'depth':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
const depthMat = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.BasicDepthPacking,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
depthMat.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.cameraType = {
|
||||
value:
|
||||
this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? 1.0
|
||||
: 0.0
|
||||
}
|
||||
|
||||
shader.fragmentShader = `
|
||||
uniform float cameraType;
|
||||
${shader.fragmentShader}
|
||||
`
|
||||
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
/gl_FragColor\s*=\s*vec4\(\s*vec3\(\s*1.0\s*-\s*fragCoordZ\s*\)\s*,\s*opacity\s*\)\s*;/,
|
||||
`
|
||||
float depth = 1.0 - fragCoordZ;
|
||||
if (cameraType > 0.5) {
|
||||
depth = pow(depth, 400.0);
|
||||
} else {
|
||||
depth = pow(depth, 0.6);
|
||||
}
|
||||
gl_FragColor = vec4(vec3(depth), opacity);
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
depthMat.customProgramCacheKey = () => {
|
||||
return this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? 'ortho'
|
||||
: 'persp'
|
||||
}
|
||||
|
||||
child.material = depthMat
|
||||
break
|
||||
case 'normal':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
child.material = new THREE.MeshNormalMaterial({
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide,
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
child.geometry.computeVertexNormals()
|
||||
break
|
||||
|
||||
case 'wireframe':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
child.material = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
wireframe: true,
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
break
|
||||
|
||||
case 'original':
|
||||
const originalMaterial = this.originalMaterials.get(child)
|
||||
if (originalMaterial) {
|
||||
child.material = originalMaterial
|
||||
} else {
|
||||
child.material = this.standardMaterial
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.eventManager.emitEvent('materialModeChange', mode)
|
||||
}
|
||||
|
||||
setupModelMaterials(model: THREE.Object3D): void {
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
|
||||
if (this.materialMode !== 'original') {
|
||||
this.setMaterialMode(this.materialMode)
|
||||
}
|
||||
}
|
||||
|
||||
clearModel(): void {
|
||||
const objectsToRemove: THREE.Object3D[] = []
|
||||
|
||||
this.scene.traverse((object) => {
|
||||
const isEnvironmentObject =
|
||||
object instanceof THREE.GridHelper ||
|
||||
object instanceof THREE.Light ||
|
||||
object instanceof THREE.Camera
|
||||
|
||||
if (!isEnvironmentObject) {
|
||||
objectsToRemove.push(object)
|
||||
}
|
||||
})
|
||||
|
||||
objectsToRemove.forEach((obj) => {
|
||||
if (obj.parent && obj.parent !== this.scene) {
|
||||
obj.parent.remove(obj)
|
||||
} else {
|
||||
this.scene.remove(obj)
|
||||
}
|
||||
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry?.dispose()
|
||||
if (Array.isArray(obj.material)) {
|
||||
obj.material.forEach((material) => material.dispose())
|
||||
} else {
|
||||
obj.material?.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.reset()
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.currentModel = null
|
||||
this.originalRotation = null
|
||||
this.currentUpDirection = 'original'
|
||||
this.setMaterialMode('original')
|
||||
|
||||
this.originalMaterials = new WeakMap()
|
||||
}
|
||||
|
||||
async setupModel(model: THREE.Object3D): Promise<void> {
|
||||
this.currentModel = model
|
||||
|
||||
const box = new THREE.Box3().setFromObject(model)
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
|
||||
const maxDim = Math.max(size.x, size.y, size.z)
|
||||
const targetSize = 5
|
||||
const scale = targetSize / maxDim
|
||||
model.scale.multiplyScalar(scale)
|
||||
|
||||
box.setFromObject(model)
|
||||
box.getCenter(center)
|
||||
box.getSize(size)
|
||||
|
||||
model.position.set(-center.x, -box.min.y, -center.z)
|
||||
|
||||
this.scene.add(model)
|
||||
|
||||
if (this.materialMode !== 'original') {
|
||||
this.setMaterialMode(this.materialMode)
|
||||
}
|
||||
|
||||
if (this.currentUpDirection !== 'original') {
|
||||
this.setUpDirection(this.currentUpDirection)
|
||||
}
|
||||
this.setupModelMaterials(model)
|
||||
|
||||
this.setupCamera(size)
|
||||
}
|
||||
|
||||
setOriginalModel(model: THREE.Object3D | THREE.BufferGeometry | GLTF): void {
|
||||
this.originalModel = model
|
||||
}
|
||||
|
||||
setUpDirection(direction: UpDirection): void {
|
||||
if (!this.currentModel) return
|
||||
|
||||
if (!this.originalRotation && this.currentModel.rotation) {
|
||||
this.originalRotation = this.currentModel.rotation.clone()
|
||||
}
|
||||
|
||||
this.currentUpDirection = direction
|
||||
|
||||
if (this.originalRotation) {
|
||||
this.currentModel.rotation.copy(this.originalRotation)
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case 'original':
|
||||
break
|
||||
case '-x':
|
||||
this.currentModel.rotation.z = Math.PI / 2
|
||||
break
|
||||
case '+x':
|
||||
this.currentModel.rotation.z = -Math.PI / 2
|
||||
break
|
||||
case '-y':
|
||||
this.currentModel.rotation.x = Math.PI
|
||||
break
|
||||
case '+y':
|
||||
break
|
||||
case '-z':
|
||||
this.currentModel.rotation.x = Math.PI / 2
|
||||
break
|
||||
case '+z':
|
||||
this.currentModel.rotation.x = -Math.PI / 2
|
||||
break
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('upDirectionChange', direction)
|
||||
}
|
||||
}
|
||||
32
src/extensions/core/load3d/NodeStorage.ts
Normal file
32
src/extensions/core/load3d/NodeStorage.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { NodeStorageInterface } from './interfaces'
|
||||
|
||||
export class NodeStorage implements NodeStorageInterface {
|
||||
private node: LGraphNode
|
||||
|
||||
constructor(node: LGraphNode = {} as LGraphNode) {
|
||||
this.node = node
|
||||
}
|
||||
|
||||
storeNodeProperty(name: string, value: any): void {
|
||||
if (this.node && this.node.properties) {
|
||||
this.node.properties[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
loadNodeProperty(name: string, defaultValue: any): any {
|
||||
if (
|
||||
!this.node ||
|
||||
!this.node.properties ||
|
||||
!(name in this.node.properties)
|
||||
) {
|
||||
return defaultValue
|
||||
}
|
||||
return this.node.properties[name]
|
||||
}
|
||||
|
||||
setNode(node: LGraphNode): void {
|
||||
this.node = node
|
||||
}
|
||||
}
|
||||
324
src/extensions/core/load3d/PreviewManager.ts
Normal file
324
src/extensions/core/load3d/PreviewManager.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import { EventManagerInterface, PreviewManagerInterface } from './interfaces'
|
||||
|
||||
export class PreviewManager implements PreviewManagerInterface {
|
||||
previewRenderer: THREE.WebGLRenderer | null = null
|
||||
previewCamera: THREE.Camera
|
||||
previewContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
showPreview: boolean = true
|
||||
previewWidth: number = 120
|
||||
|
||||
private targetWidth: number = 1024
|
||||
private targetHeight: number = 1024
|
||||
private scene: THREE.Scene
|
||||
private getActiveCamera: () => THREE.Camera
|
||||
private getControls: () => OrbitControls
|
||||
private eventManager: EventManagerInterface
|
||||
|
||||
private getRenderer: () => THREE.WebGLRenderer
|
||||
|
||||
private previewBackgroundScene: THREE.Scene
|
||||
private previewBackgroundCamera: THREE.OrthographicCamera
|
||||
private previewBackgroundMesh: THREE.Mesh | null = null
|
||||
private previewBackgroundTexture: THREE.Texture | null = null
|
||||
|
||||
constructor(
|
||||
scene: THREE.Scene,
|
||||
getActiveCamera: () => THREE.Camera,
|
||||
getControls: () => OrbitControls,
|
||||
getRenderer: () => THREE.WebGLRenderer,
|
||||
eventManager: EventManagerInterface,
|
||||
backgroundScene: THREE.Scene,
|
||||
backgroundCamera: THREE.OrthographicCamera
|
||||
) {
|
||||
this.scene = scene
|
||||
this.getActiveCamera = getActiveCamera
|
||||
this.getControls = getControls
|
||||
this.getRenderer = getRenderer
|
||||
this.eventManager = eventManager
|
||||
|
||||
this.previewCamera = this.getActiveCamera().clone()
|
||||
|
||||
this.previewBackgroundScene = backgroundScene.clone()
|
||||
this.previewBackgroundCamera = backgroundCamera.clone()
|
||||
|
||||
const planeGeometry = new THREE.PlaneGeometry(2, 2)
|
||||
const planeMaterial = new THREE.MeshBasicMaterial({
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
this.previewBackgroundMesh = new THREE.Mesh(planeGeometry, planeMaterial)
|
||||
this.previewBackgroundMesh.position.set(0, 0, 0)
|
||||
this.previewBackgroundScene.add(this.previewBackgroundMesh)
|
||||
}
|
||||
|
||||
init(): void {}
|
||||
|
||||
dispose(): void {
|
||||
if (this.previewRenderer) {
|
||||
this.previewRenderer.dispose()
|
||||
}
|
||||
|
||||
if (this.previewBackgroundTexture) {
|
||||
this.previewBackgroundTexture.dispose()
|
||||
}
|
||||
|
||||
if (this.previewBackgroundMesh) {
|
||||
this.previewBackgroundMesh.geometry.dispose()
|
||||
;(this.previewBackgroundMesh.material as THREE.Material).dispose()
|
||||
}
|
||||
}
|
||||
|
||||
createCapturePreview(container: Element | HTMLElement): void {
|
||||
this.previewRenderer = new THREE.WebGLRenderer({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
preserveDrawingBuffer: true
|
||||
})
|
||||
|
||||
this.previewRenderer.setSize(this.targetWidth, this.targetHeight)
|
||||
this.previewRenderer.setClearColor(0x282828)
|
||||
this.previewRenderer.autoClear = false
|
||||
this.previewRenderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
|
||||
this.previewContainer = document.createElement('div')
|
||||
this.previewContainer.style.cssText = `
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
transition: border-color 0.1s ease;
|
||||
`
|
||||
this.previewContainer.appendChild(this.previewRenderer.domElement)
|
||||
|
||||
const MIN_PREVIEW_WIDTH = 120
|
||||
const MAX_PREVIEW_WIDTH = 240
|
||||
|
||||
this.previewContainer.addEventListener('wheel', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const delta = event.deltaY
|
||||
const oldWidth = this.previewWidth
|
||||
|
||||
if (delta > 0) {
|
||||
this.previewWidth = Math.max(MIN_PREVIEW_WIDTH, this.previewWidth - 10)
|
||||
} else {
|
||||
this.previewWidth = Math.min(MAX_PREVIEW_WIDTH, this.previewWidth + 10)
|
||||
}
|
||||
|
||||
if (
|
||||
oldWidth !== this.previewWidth &&
|
||||
(this.previewWidth === MIN_PREVIEW_WIDTH ||
|
||||
this.previewWidth === MAX_PREVIEW_WIDTH)
|
||||
) {
|
||||
this.flashPreviewBorder()
|
||||
}
|
||||
|
||||
this.updatePreviewSize()
|
||||
this.updatePreviewRender()
|
||||
})
|
||||
|
||||
this.previewContainer.style.display = this.showPreview ? 'block' : 'none'
|
||||
|
||||
container.appendChild(this.previewContainer)
|
||||
|
||||
this.updatePreviewSize()
|
||||
}
|
||||
|
||||
flashPreviewBorder(): void {
|
||||
const originalBorder = this.previewContainer.style.border
|
||||
const originalBoxShadow = this.previewContainer.style.boxShadow
|
||||
|
||||
this.previewContainer.style.border = '2px solid rgba(255, 255, 255, 0.8)'
|
||||
this.previewContainer.style.boxShadow = '0 0 8px rgba(255, 255, 255, 0.5)'
|
||||
|
||||
setTimeout(() => {
|
||||
this.previewContainer.style.border = originalBorder
|
||||
this.previewContainer.style.boxShadow = originalBoxShadow
|
||||
}, 100)
|
||||
}
|
||||
|
||||
updatePreviewSize(): void {
|
||||
if (!this.previewContainer) return
|
||||
|
||||
const previewHeight =
|
||||
(this.previewWidth * this.targetHeight) / this.targetWidth
|
||||
this.previewRenderer?.setSize(this.previewWidth, previewHeight, false)
|
||||
}
|
||||
|
||||
updatePreviewRender(): void {
|
||||
if (!this.previewRenderer || !this.previewContainer || !this.showPreview)
|
||||
return
|
||||
|
||||
if (
|
||||
!this.previewCamera ||
|
||||
(this.getActiveCamera() instanceof THREE.PerspectiveCamera &&
|
||||
!(this.previewCamera instanceof THREE.PerspectiveCamera)) ||
|
||||
(this.getActiveCamera() instanceof THREE.OrthographicCamera &&
|
||||
!(this.previewCamera instanceof THREE.OrthographicCamera))
|
||||
) {
|
||||
this.previewCamera = this.getActiveCamera().clone()
|
||||
}
|
||||
|
||||
this.previewCamera.position.copy(this.getActiveCamera().position)
|
||||
this.previewCamera.rotation.copy(this.getActiveCamera().rotation)
|
||||
|
||||
const aspect = this.targetWidth / this.targetHeight
|
||||
|
||||
if (this.getActiveCamera() instanceof THREE.OrthographicCamera) {
|
||||
const activeOrtho = this.getActiveCamera() as THREE.OrthographicCamera
|
||||
const previewOrtho = this.previewCamera as THREE.OrthographicCamera
|
||||
|
||||
const frustumHeight =
|
||||
(activeOrtho.top - activeOrtho.bottom) / activeOrtho.zoom
|
||||
|
||||
const frustumWidth = frustumHeight * aspect
|
||||
|
||||
previewOrtho.top = frustumHeight / 2
|
||||
previewOrtho.left = -frustumWidth / 2
|
||||
previewOrtho.right = frustumWidth / 2
|
||||
previewOrtho.bottom = -frustumHeight / 2
|
||||
previewOrtho.zoom = 1
|
||||
|
||||
previewOrtho.updateProjectionMatrix()
|
||||
} else {
|
||||
;(this.previewCamera as THREE.PerspectiveCamera).aspect = aspect
|
||||
;(this.previewCamera as THREE.PerspectiveCamera).fov = (
|
||||
this.getActiveCamera() as THREE.PerspectiveCamera
|
||||
).fov
|
||||
;(this.previewCamera as THREE.PerspectiveCamera).updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.previewCamera.lookAt(this.getControls().target)
|
||||
|
||||
const previewHeight =
|
||||
(this.previewWidth * this.targetHeight) / this.targetWidth
|
||||
this.previewRenderer.setSize(this.previewWidth, previewHeight, false)
|
||||
this.previewRenderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
this.previewRenderer.clear()
|
||||
|
||||
if (this.previewBackgroundMesh && this.previewBackgroundTexture) {
|
||||
const material = this.previewBackgroundMesh
|
||||
.material as THREE.MeshBasicMaterial
|
||||
if (material.map) {
|
||||
const currentToneMapping = this.previewRenderer.toneMapping
|
||||
const currentExposure = this.previewRenderer.toneMappingExposure
|
||||
|
||||
this.previewRenderer.toneMapping = THREE.NoToneMapping
|
||||
this.previewRenderer.render(
|
||||
this.previewBackgroundScene,
|
||||
this.previewBackgroundCamera
|
||||
)
|
||||
|
||||
this.previewRenderer.toneMapping = currentToneMapping
|
||||
this.previewRenderer.toneMappingExposure = currentExposure
|
||||
}
|
||||
}
|
||||
|
||||
this.previewRenderer.render(this.scene, this.previewCamera)
|
||||
}
|
||||
|
||||
togglePreview(showPreview: boolean): void {
|
||||
if (this.previewRenderer) {
|
||||
this.showPreview = showPreview
|
||||
this.previewContainer.style.display = this.showPreview ? 'block' : 'none'
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('showPreviewChange', showPreview)
|
||||
}
|
||||
|
||||
setTargetSize(width: number, height: number): void {
|
||||
const oldAspect = this.targetWidth / this.targetHeight
|
||||
|
||||
this.targetWidth = width
|
||||
this.targetHeight = height
|
||||
|
||||
this.updatePreviewSize()
|
||||
|
||||
const newAspect = width / height
|
||||
if (Math.abs(oldAspect - newAspect) > 0.001) {
|
||||
this.updateBackgroundSize(
|
||||
this.previewBackgroundTexture,
|
||||
this.previewBackgroundMesh,
|
||||
width,
|
||||
height
|
||||
)
|
||||
}
|
||||
|
||||
if (this.previewRenderer && this.previewCamera) {
|
||||
if (this.previewCamera instanceof THREE.PerspectiveCamera) {
|
||||
this.previewCamera.aspect = width / height
|
||||
this.previewCamera.updateProjectionMatrix()
|
||||
} else if (this.previewCamera instanceof THREE.OrthographicCamera) {
|
||||
const frustumSize = 10
|
||||
const aspect = width / height
|
||||
this.previewCamera.left = (-frustumSize * aspect) / 2
|
||||
this.previewCamera.right = (frustumSize * aspect) / 2
|
||||
this.previewCamera.updateProjectionMatrix()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleResize(): void {
|
||||
this.updatePreviewSize()
|
||||
this.updatePreviewRender()
|
||||
}
|
||||
|
||||
updateBackgroundTexture(texture: THREE.Texture | null): void {
|
||||
if (this.previewBackgroundTexture) {
|
||||
this.previewBackgroundTexture.dispose()
|
||||
}
|
||||
|
||||
this.previewBackgroundTexture = texture
|
||||
|
||||
if (texture && this.previewBackgroundMesh) {
|
||||
const material2 = this.previewBackgroundMesh
|
||||
.material as THREE.MeshBasicMaterial
|
||||
material2.map = texture
|
||||
material2.needsUpdate = true
|
||||
|
||||
this.previewBackgroundMesh.position.set(0, 0, 0)
|
||||
|
||||
this.updateBackgroundSize(
|
||||
this.previewBackgroundTexture,
|
||||
this.previewBackgroundMesh,
|
||||
this.targetWidth,
|
||||
this.targetHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private updateBackgroundSize(
|
||||
backgroundTexture: THREE.Texture | null,
|
||||
backgroundMesh: THREE.Mesh | null,
|
||||
targetWidth: number,
|
||||
targetHeight: number
|
||||
): void {
|
||||
if (!backgroundTexture || !backgroundMesh) return
|
||||
|
||||
const material = backgroundMesh.material as THREE.MeshBasicMaterial
|
||||
|
||||
if (!material.map) return
|
||||
|
||||
const imageAspect =
|
||||
backgroundTexture.image.width / backgroundTexture.image.height
|
||||
const targetAspect = targetWidth / targetHeight
|
||||
|
||||
if (imageAspect > targetAspect) {
|
||||
backgroundMesh.scale.set(imageAspect / targetAspect, 1, 1)
|
||||
} else {
|
||||
backgroundMesh.scale.set(1, targetAspect / imageAspect, 1)
|
||||
}
|
||||
|
||||
material.needsUpdate = true
|
||||
}
|
||||
|
||||
reset(): void {}
|
||||
}
|
||||
277
src/extensions/core/load3d/SceneManager.ts
Normal file
277
src/extensions/core/load3d/SceneManager.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
import Load3dUtils from './Load3dUtils'
|
||||
import { EventManagerInterface, SceneManagerInterface } from './interfaces'
|
||||
|
||||
export class SceneManager implements SceneManagerInterface {
|
||||
scene: THREE.Scene
|
||||
gridHelper: THREE.GridHelper
|
||||
|
||||
backgroundScene: THREE.Scene
|
||||
backgroundCamera: THREE.OrthographicCamera
|
||||
backgroundMesh: THREE.Mesh | null = null
|
||||
backgroundTexture: THREE.Texture | null = null
|
||||
|
||||
private eventManager: EventManagerInterface
|
||||
private renderer: THREE.WebGLRenderer
|
||||
|
||||
private getActiveCamera: () => THREE.Camera
|
||||
private getControls: () => OrbitControls
|
||||
|
||||
constructor(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
getActiveCamera: () => THREE.Camera,
|
||||
getControls: () => OrbitControls,
|
||||
eventManager: EventManagerInterface
|
||||
) {
|
||||
this.renderer = renderer
|
||||
this.eventManager = eventManager
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
this.getActiveCamera = getActiveCamera
|
||||
this.getControls = getControls
|
||||
|
||||
this.gridHelper = new THREE.GridHelper(10, 10)
|
||||
this.gridHelper.position.set(0, 0, 0)
|
||||
this.scene.add(this.gridHelper)
|
||||
|
||||
this.backgroundScene = new THREE.Scene()
|
||||
this.backgroundCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1)
|
||||
|
||||
const planeGeometry = new THREE.PlaneGeometry(2, 2)
|
||||
const planeMaterial = new THREE.MeshBasicMaterial({
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
this.backgroundMesh = new THREE.Mesh(planeGeometry, planeMaterial)
|
||||
this.backgroundMesh.position.set(0, 0, 0)
|
||||
this.backgroundScene.add(this.backgroundMesh)
|
||||
}
|
||||
|
||||
init(): void {}
|
||||
|
||||
dispose(): void {
|
||||
if (this.backgroundTexture) {
|
||||
this.backgroundTexture.dispose()
|
||||
}
|
||||
|
||||
if (this.backgroundMesh) {
|
||||
this.backgroundMesh.geometry.dispose()
|
||||
;(this.backgroundMesh.material as THREE.Material).dispose()
|
||||
}
|
||||
|
||||
this.scene.clear()
|
||||
}
|
||||
|
||||
toggleGrid(showGrid: boolean): void {
|
||||
if (this.gridHelper) {
|
||||
this.gridHelper.visible = showGrid
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('showGridChange', showGrid)
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string): void {
|
||||
this.renderer.setClearColor(new THREE.Color(color))
|
||||
this.eventManager.emitEvent('backgroundColorChange', color)
|
||||
}
|
||||
|
||||
async setBackgroundImage(uploadPath: string): Promise<void> {
|
||||
if (uploadPath === '') {
|
||||
this.removeBackgroundImage()
|
||||
return
|
||||
}
|
||||
|
||||
let imageUrl = Load3dUtils.getResourceURL(
|
||||
...Load3dUtils.splitFilePath(uploadPath)
|
||||
)
|
||||
|
||||
if (!imageUrl.startsWith('/api')) {
|
||||
imageUrl = '/api' + imageUrl
|
||||
}
|
||||
|
||||
try {
|
||||
const textureLoader = new THREE.TextureLoader()
|
||||
const texture = await new Promise<THREE.Texture>((resolve, reject) => {
|
||||
textureLoader.load(imageUrl, resolve, undefined, reject)
|
||||
})
|
||||
|
||||
if (this.backgroundTexture) {
|
||||
this.backgroundTexture.dispose()
|
||||
}
|
||||
|
||||
texture.colorSpace = THREE.SRGBColorSpace
|
||||
|
||||
this.backgroundTexture = texture
|
||||
|
||||
const material = this.backgroundMesh?.material as THREE.MeshBasicMaterial
|
||||
material.map = texture
|
||||
material.needsUpdate = true
|
||||
|
||||
this.backgroundMesh?.position.set(0, 0, 0)
|
||||
|
||||
this.updateBackgroundSize(
|
||||
this.backgroundTexture,
|
||||
this.backgroundMesh,
|
||||
this.renderer.domElement.width,
|
||||
this.renderer.domElement.height
|
||||
)
|
||||
|
||||
this.eventManager.emitEvent('backgroundImageChange', uploadPath)
|
||||
} catch (error) {
|
||||
console.error('Error loading background image:', error)
|
||||
}
|
||||
}
|
||||
|
||||
removeBackgroundImage(): void {
|
||||
if (this.backgroundMesh) {
|
||||
const material = this.backgroundMesh.material as THREE.MeshBasicMaterial
|
||||
material.map = null
|
||||
material.needsUpdate = true
|
||||
}
|
||||
|
||||
if (this.backgroundTexture) {
|
||||
this.backgroundTexture.dispose()
|
||||
this.backgroundTexture = null
|
||||
}
|
||||
}
|
||||
|
||||
updateBackgroundSize(
|
||||
backgroundTexture: THREE.Texture | null,
|
||||
backgroundMesh: THREE.Mesh | null,
|
||||
targetWidth: number,
|
||||
targetHeight: number
|
||||
): void {
|
||||
if (!backgroundTexture || !backgroundMesh) return
|
||||
|
||||
const material = backgroundMesh.material as THREE.MeshBasicMaterial
|
||||
|
||||
if (!material.map) return
|
||||
|
||||
const imageAspect =
|
||||
backgroundTexture.image.width / backgroundTexture.image.height
|
||||
const targetAspect = targetWidth / targetHeight
|
||||
|
||||
if (imageAspect > targetAspect) {
|
||||
backgroundMesh.scale.set(imageAspect / targetAspect, 1, 1)
|
||||
} else {
|
||||
backgroundMesh.scale.set(1, targetAspect / imageAspect, 1)
|
||||
}
|
||||
|
||||
material.needsUpdate = true
|
||||
}
|
||||
|
||||
handleResize(width: number, height: number): void {
|
||||
if (this.backgroundTexture && this.backgroundMesh) {
|
||||
this.updateBackgroundSize(
|
||||
this.backgroundTexture,
|
||||
this.backgroundMesh,
|
||||
width,
|
||||
height
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderBackground(): void {
|
||||
if (this.backgroundMesh && this.backgroundTexture) {
|
||||
const material = this.backgroundMesh.material as THREE.MeshBasicMaterial
|
||||
if (material.map) {
|
||||
const currentToneMapping = this.renderer.toneMapping
|
||||
const currentExposure = this.renderer.toneMappingExposure
|
||||
|
||||
this.renderer.toneMapping = THREE.NoToneMapping
|
||||
this.renderer.render(this.backgroundScene, this.backgroundCamera)
|
||||
|
||||
this.renderer.toneMapping = currentToneMapping
|
||||
this.renderer.toneMappingExposure = currentExposure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
captureScene(
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<{ scene: string; mask: string }> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const originalWidth = this.renderer.domElement.width
|
||||
const originalHeight = this.renderer.domElement.height
|
||||
const originalClearColor = this.renderer.getClearColor(
|
||||
new THREE.Color()
|
||||
)
|
||||
const originalClearAlpha = this.renderer.getClearAlpha()
|
||||
const originalToneMapping = this.renderer.toneMapping
|
||||
const originalExposure = this.renderer.toneMappingExposure
|
||||
|
||||
this.renderer.setSize(width, height)
|
||||
|
||||
if (this.getActiveCamera() instanceof THREE.PerspectiveCamera) {
|
||||
const perspectiveCamera =
|
||||
this.getActiveCamera() as THREE.PerspectiveCamera
|
||||
|
||||
perspectiveCamera.aspect = width / height
|
||||
perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const orthographicCamera =
|
||||
this.getActiveCamera() as THREE.OrthographicCamera
|
||||
|
||||
const frustumSize = 10
|
||||
const aspect = width / height
|
||||
|
||||
orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
orthographicCamera.top = frustumSize / 2
|
||||
orthographicCamera.bottom = -frustumSize / 2
|
||||
|
||||
orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
if (this.backgroundTexture && this.backgroundMesh) {
|
||||
this.updateBackgroundSize(
|
||||
this.backgroundTexture,
|
||||
this.backgroundMesh,
|
||||
width,
|
||||
height
|
||||
)
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
|
||||
if (this.backgroundMesh && this.backgroundTexture) {
|
||||
const material = this.backgroundMesh
|
||||
.material as THREE.MeshBasicMaterial
|
||||
|
||||
if (material.map) {
|
||||
this.renderer.toneMapping = THREE.NoToneMapping
|
||||
this.renderer.render(this.backgroundScene, this.backgroundCamera)
|
||||
this.renderer.toneMapping = originalToneMapping
|
||||
this.renderer.toneMappingExposure = originalExposure
|
||||
}
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.getActiveCamera())
|
||||
const sceneData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.getActiveCamera())
|
||||
const maskData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(originalClearColor, originalClearAlpha)
|
||||
this.renderer.setSize(originalWidth, originalHeight)
|
||||
|
||||
this.handleResize(width, height)
|
||||
|
||||
resolve({ scene: sceneData, mask: maskData })
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
reset(): void {}
|
||||
}
|
||||
104
src/extensions/core/load3d/ViewHelperManager.ts
Normal file
104
src/extensions/core/load3d/ViewHelperManager.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
|
||||
import { NodeStorageInterface, ViewHelperManagerInterface } from './interfaces'
|
||||
|
||||
export class ViewHelperManager implements ViewHelperManagerInterface {
|
||||
viewHelper: ViewHelper = {} as ViewHelper
|
||||
viewHelperContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
|
||||
private getActiveCamera: () => THREE.Camera
|
||||
private getControls: () => OrbitControls
|
||||
private nodeStorage: NodeStorageInterface
|
||||
private renderer: THREE.WebGLRenderer
|
||||
|
||||
constructor(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
getActiveCamera: () => THREE.Camera,
|
||||
getControls: () => OrbitControls,
|
||||
nodeStorage: NodeStorageInterface
|
||||
) {
|
||||
this.renderer = renderer
|
||||
this.getActiveCamera = getActiveCamera
|
||||
this.getControls = getControls
|
||||
this.nodeStorage = nodeStorage
|
||||
}
|
||||
|
||||
init(): void {}
|
||||
|
||||
dispose(): void {
|
||||
if (this.viewHelper) {
|
||||
this.viewHelper.dispose()
|
||||
}
|
||||
|
||||
if (this.viewHelperContainer && this.viewHelperContainer.parentNode) {
|
||||
this.viewHelperContainer.parentNode.removeChild(this.viewHelperContainer)
|
||||
}
|
||||
}
|
||||
|
||||
createViewHelper(container: Element | HTMLElement): void {
|
||||
this.viewHelperContainer = document.createElement('div')
|
||||
|
||||
this.viewHelperContainer.style.position = 'absolute'
|
||||
this.viewHelperContainer.style.bottom = '0'
|
||||
this.viewHelperContainer.style.left = '0'
|
||||
this.viewHelperContainer.style.width = '128px'
|
||||
this.viewHelperContainer.style.height = '128px'
|
||||
|
||||
this.viewHelperContainer.addEventListener('pointerup', (event) => {
|
||||
event.stopPropagation()
|
||||
this.viewHelper.handleClick(event)
|
||||
})
|
||||
|
||||
this.viewHelperContainer.addEventListener('pointerdown', (event) => {
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
container.appendChild(this.viewHelperContainer)
|
||||
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.getActiveCamera(),
|
||||
this.viewHelperContainer
|
||||
)
|
||||
|
||||
this.viewHelper.center = this.getControls().target
|
||||
}
|
||||
|
||||
update(delta: number): void {
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
|
||||
if (!this.viewHelper.animating) {
|
||||
this.nodeStorage.storeNodeProperty('Camera Info', {
|
||||
position: this.getActiveCamera().position.clone(),
|
||||
target: this.getControls().target.clone(),
|
||||
zoom:
|
||||
this.getActiveCamera() instanceof THREE.OrthographicCamera
|
||||
? (this.getActiveCamera() as THREE.OrthographicCamera).zoom
|
||||
: (this.getActiveCamera() as THREE.PerspectiveCamera).zoom,
|
||||
cameraType:
|
||||
this.getActiveCamera() instanceof THREE.PerspectiveCamera
|
||||
? 'perspective'
|
||||
: 'orthographic'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleResize(): void {}
|
||||
|
||||
recreateViewHelper(): void {
|
||||
if (this.viewHelper) {
|
||||
this.viewHelper.dispose()
|
||||
}
|
||||
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.getActiveCamera(),
|
||||
this.viewHelperContainer
|
||||
)
|
||||
this.viewHelper.center = this.getControls().target
|
||||
}
|
||||
|
||||
reset(): void {}
|
||||
}
|
||||
164
src/extensions/core/load3d/interfaces.ts
Normal file
164
src/extensions/core/load3d/interfaces.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
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'
|
||||
|
||||
export type MaterialMode = 'original' | 'normal' | 'wireframe' | 'depth'
|
||||
export type UpDirection = 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
export type CameraType = 'perspective' | 'orthographic'
|
||||
|
||||
export interface CameraState {
|
||||
position: THREE.Vector3
|
||||
target: THREE.Vector3
|
||||
zoom: number
|
||||
cameraType: CameraType
|
||||
}
|
||||
|
||||
export interface EventCallback {
|
||||
(data?: any): void
|
||||
}
|
||||
|
||||
export interface Load3DOptions {
|
||||
createPreview?: boolean
|
||||
node?: LGraphNode
|
||||
}
|
||||
|
||||
export interface CaptureResult {
|
||||
scene: string
|
||||
mask: string
|
||||
}
|
||||
|
||||
export interface BaseManager {
|
||||
init(): void
|
||||
dispose(): void
|
||||
reset(): void
|
||||
}
|
||||
|
||||
export interface AnimationItem {
|
||||
name: string
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface SceneManagerInterface extends BaseManager {
|
||||
scene: THREE.Scene
|
||||
gridHelper: THREE.GridHelper
|
||||
toggleGrid(showGrid: boolean): void
|
||||
setBackgroundColor(color: string): void
|
||||
setBackgroundImage(uploadPath: string): Promise<void>
|
||||
removeBackgroundImage(): void
|
||||
handleResize(width: number, height: number): void
|
||||
captureScene(width: number, height: number): Promise<CaptureResult>
|
||||
}
|
||||
|
||||
export interface CameraManagerInterface extends BaseManager {
|
||||
activeCamera: THREE.Camera
|
||||
perspectiveCamera: THREE.PerspectiveCamera
|
||||
orthographicCamera: THREE.OrthographicCamera
|
||||
getCurrentCameraType(): CameraType
|
||||
toggleCamera(cameraType?: CameraType): void
|
||||
setFOV(fov: number): void
|
||||
setCameraState(state: CameraState): void
|
||||
getCameraState(): CameraState
|
||||
handleResize(width: number, height: number): void
|
||||
setControls(controls: OrbitControls): void
|
||||
}
|
||||
|
||||
export interface ControlsManagerInterface extends BaseManager {
|
||||
controls: OrbitControls
|
||||
handleResize(): void
|
||||
}
|
||||
|
||||
export interface LightingManagerInterface extends BaseManager {
|
||||
lights: THREE.Light[]
|
||||
setLightIntensity(intensity: number): void
|
||||
}
|
||||
|
||||
export interface ViewHelperManagerInterface extends BaseManager {
|
||||
viewHelper: ViewHelper
|
||||
viewHelperContainer: HTMLDivElement
|
||||
createViewHelper(container: Element | HTMLElement): void
|
||||
update(delta: number): void
|
||||
handleResize(): void
|
||||
}
|
||||
|
||||
export interface PreviewManagerInterface extends BaseManager {
|
||||
previewRenderer: THREE.WebGLRenderer | null
|
||||
previewCamera: THREE.Camera
|
||||
previewContainer: HTMLDivElement
|
||||
showPreview: boolean
|
||||
previewWidth: number
|
||||
createCapturePreview(container: Element | HTMLElement): void
|
||||
updatePreviewSize(): void
|
||||
updatePreviewRender(): void
|
||||
togglePreview(showPreview: boolean): void
|
||||
setTargetSize(width: number, height: number): void
|
||||
handleResize(): void
|
||||
updateBackgroundTexture(texture: THREE.Texture | null): void
|
||||
}
|
||||
|
||||
export interface EventManagerInterface {
|
||||
addEventListener(event: string, callback: EventCallback): void
|
||||
removeEventListener(event: string, callback: EventCallback): void
|
||||
emitEvent(event: string, data?: any): void
|
||||
}
|
||||
|
||||
export interface NodeStorageInterface {
|
||||
storeNodeProperty(name: string, value: any): void
|
||||
loadNodeProperty(name: string, defaultValue: any): any
|
||||
}
|
||||
|
||||
export interface AnimationManagerInterface extends BaseManager {
|
||||
currentAnimation: THREE.AnimationMixer | null
|
||||
animationActions: THREE.AnimationAction[]
|
||||
animationClips: THREE.AnimationClip[]
|
||||
selectedAnimationIndex: number
|
||||
isAnimationPlaying: boolean
|
||||
animationSpeed: number
|
||||
|
||||
setupModelAnimations(model: THREE.Object3D, originalModel: any): void
|
||||
updateAnimationList(): void
|
||||
setAnimationSpeed(speed: number): void
|
||||
updateSelectedAnimation(index: number): void
|
||||
toggleAnimation(play?: boolean): void
|
||||
update(delta: number): void
|
||||
}
|
||||
|
||||
export interface ModelManagerInterface {
|
||||
currentModel: THREE.Object3D | null
|
||||
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null
|
||||
originalRotation: THREE.Euler | null
|
||||
currentUpDirection: UpDirection
|
||||
|
||||
init(): void
|
||||
dispose(): void
|
||||
clearModel(): void
|
||||
reset(): void
|
||||
setupModel(model: THREE.Object3D): Promise<void>
|
||||
setOriginalModel(model: THREE.Object3D | THREE.BufferGeometry | GLTF): void
|
||||
setUpDirection(direction: UpDirection): void
|
||||
materialMode: MaterialMode
|
||||
originalMaterials: WeakMap<THREE.Mesh, THREE.Material | THREE.Material[]>
|
||||
normalMaterial: THREE.MeshNormalMaterial
|
||||
standardMaterial: THREE.MeshStandardMaterial
|
||||
wireframeMaterial: THREE.MeshBasicMaterial
|
||||
depthMaterial: THREE.MeshDepthMaterial
|
||||
setMaterialMode(mode: MaterialMode): void
|
||||
setupModelMaterials(model: THREE.Object3D): void
|
||||
}
|
||||
|
||||
export interface LoaderManagerInterface {
|
||||
gltfLoader: GLTFLoader
|
||||
objLoader: OBJLoader
|
||||
mtlLoader: MTLLoader
|
||||
fbxLoader: FBXLoader
|
||||
stlLoader: STLLoader
|
||||
|
||||
init(): void
|
||||
dispose(): void
|
||||
loadModel(url: string, originalFileName?: string): Promise<void>
|
||||
}
|
||||
@@ -847,6 +847,7 @@
|
||||
"fov": "FOV",
|
||||
"previewOutput": "Preview Output",
|
||||
"uploadBackgroundImage": "Upload Background Image",
|
||||
"removeBackgroundImage": "Remove Background Image"
|
||||
"removeBackgroundImage": "Remove Background Image",
|
||||
"loadingModel": "Loading 3D Model..."
|
||||
}
|
||||
}
|
||||
@@ -321,6 +321,7 @@
|
||||
"backgroundColor": "Couleur de fond",
|
||||
"fov": "FOV",
|
||||
"lightIntensity": "Intensité de la lumière",
|
||||
"loadingModel": "Chargement du modèle 3D...",
|
||||
"previewOutput": "Aperçu de la sortie",
|
||||
"removeBackgroundImage": "Supprimer l'image de fond",
|
||||
"showGrid": "Afficher la grille",
|
||||
|
||||
@@ -321,6 +321,7 @@
|
||||
"backgroundColor": "背景色",
|
||||
"fov": "FOV",
|
||||
"lightIntensity": "光の強度",
|
||||
"loadingModel": "3Dモデルを読み込んでいます...",
|
||||
"previewOutput": "出力のプレビュー",
|
||||
"removeBackgroundImage": "背景画像を削除",
|
||||
"showGrid": "グリッドを表示",
|
||||
|
||||
@@ -321,6 +321,7 @@
|
||||
"backgroundColor": "Цвет фона",
|
||||
"fov": "Угол обзора",
|
||||
"lightIntensity": "Интенсивность света",
|
||||
"loadingModel": "Загрузка 3D модели...",
|
||||
"previewOutput": "Предварительный просмотр",
|
||||
"removeBackgroundImage": "Удалить фоновое изображение",
|
||||
"showGrid": "Показать сетку",
|
||||
|
||||
@@ -321,6 +321,7 @@
|
||||
"backgroundColor": "背景颜色",
|
||||
"fov": "视场",
|
||||
"lightIntensity": "光照强度",
|
||||
"loadingModel": "正在加载3D模型...",
|
||||
"previewOutput": "预览输出",
|
||||
"removeBackgroundImage": "移除背景图片",
|
||||
"showGrid": "显示网格",
|
||||
|
||||
Reference in New Issue
Block a user