mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
535 lines
14 KiB
TypeScript
535 lines
14 KiB
TypeScript
import { LGraphNode } from '@comfyorg/litegraph'
|
|
import * as THREE from 'three'
|
|
|
|
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
|
|
|
import { CameraManager } from './CameraManager'
|
|
import { ControlsManager } from './ControlsManager'
|
|
import { EventManager } from './EventManager'
|
|
import { LightingManager } from './LightingManager'
|
|
import { LoaderManager } from './LoaderManager'
|
|
import { ModelExporter } from './ModelExporter'
|
|
import { ModelManager } from './ModelManager'
|
|
import { NodeStorage } from './NodeStorage'
|
|
import { PreviewManager } from './PreviewManager'
|
|
import { RecordingManager } from './RecordingManager'
|
|
import { SceneManager } from './SceneManager'
|
|
import { ViewHelperManager } from './ViewHelperManager'
|
|
import {
|
|
CameraState,
|
|
CaptureResult,
|
|
Load3DOptions,
|
|
MaterialMode,
|
|
UpDirection
|
|
} from './interfaces'
|
|
|
|
class Load3d {
|
|
renderer: THREE.WebGLRenderer
|
|
protected clock: THREE.Clock
|
|
protected animationFrameId: number | null = null
|
|
node: LGraphNode
|
|
|
|
protected eventManager: EventManager
|
|
protected nodeStorage: NodeStorage
|
|
protected sceneManager: SceneManager
|
|
protected cameraManager: CameraManager
|
|
protected controlsManager: ControlsManager
|
|
protected lightingManager: LightingManager
|
|
protected viewHelperManager: ViewHelperManager
|
|
protected previewManager: PreviewManager
|
|
protected loaderManager: LoaderManager
|
|
protected modelManager: ModelManager
|
|
protected recordingManager: RecordingManager
|
|
|
|
STATUS_MOUSE_ON_NODE: boolean
|
|
STATUS_MOUSE_ON_SCENE: boolean
|
|
INITIAL_RENDER_DONE: boolean = false
|
|
|
|
constructor(
|
|
container: Element | HTMLElement,
|
|
options: Load3DOptions = {
|
|
node: {} as LGraphNode,
|
|
inputSpec: {} as CustomInputSpec
|
|
}
|
|
) {
|
|
this.node = options.node || ({} as LGraphNode)
|
|
this.clock = new THREE.Clock()
|
|
|
|
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
|
|
this.renderer.setSize(300, 300)
|
|
this.renderer.setClearColor(0x282828)
|
|
this.renderer.autoClear = false
|
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
container.appendChild(this.renderer.domElement)
|
|
|
|
this.eventManager = new EventManager()
|
|
this.nodeStorage = new NodeStorage(this.node)
|
|
|
|
this.sceneManager = new SceneManager(
|
|
this.renderer,
|
|
this.getActiveCamera.bind(this),
|
|
this.getControls.bind(this),
|
|
this.eventManager
|
|
)
|
|
|
|
this.cameraManager = new CameraManager(
|
|
this.renderer,
|
|
this.eventManager,
|
|
this.nodeStorage
|
|
)
|
|
|
|
this.controlsManager = new ControlsManager(
|
|
this.renderer,
|
|
this.cameraManager.activeCamera,
|
|
this.eventManager,
|
|
this.nodeStorage
|
|
)
|
|
|
|
this.cameraManager.setControls(this.controlsManager.controls)
|
|
|
|
this.lightingManager = new LightingManager(
|
|
this.sceneManager.scene,
|
|
this.eventManager
|
|
)
|
|
|
|
this.viewHelperManager = new ViewHelperManager(
|
|
this.renderer,
|
|
this.getActiveCamera.bind(this),
|
|
this.getControls.bind(this),
|
|
this.nodeStorage
|
|
)
|
|
|
|
this.previewManager = new PreviewManager(
|
|
this.sceneManager.scene,
|
|
this.getActiveCamera.bind(this),
|
|
this.getControls.bind(this),
|
|
() => this.renderer,
|
|
this.eventManager,
|
|
this.sceneManager.backgroundScene,
|
|
this.sceneManager.backgroundCamera
|
|
)
|
|
|
|
this.modelManager = new ModelManager(
|
|
this.sceneManager.scene,
|
|
this.renderer,
|
|
this.eventManager,
|
|
this.getActiveCamera.bind(this),
|
|
this.setupCamera.bind(this),
|
|
options
|
|
)
|
|
|
|
this.loaderManager = new LoaderManager(
|
|
this.modelManager,
|
|
this.eventManager,
|
|
options
|
|
)
|
|
|
|
this.recordingManager = new RecordingManager(
|
|
this.sceneManager.scene,
|
|
this.renderer,
|
|
this.eventManager
|
|
)
|
|
this.sceneManager.init()
|
|
this.cameraManager.init()
|
|
this.controlsManager.init()
|
|
this.lightingManager.init()
|
|
this.loaderManager.init()
|
|
this.loaderManager.init()
|
|
|
|
this.viewHelperManager.createViewHelper(container)
|
|
this.viewHelperManager.init()
|
|
|
|
if (options && !options.inputSpec?.isPreview) {
|
|
this.previewManager.createCapturePreview(container)
|
|
this.previewManager.init()
|
|
}
|
|
|
|
this.STATUS_MOUSE_ON_NODE = false
|
|
this.STATUS_MOUSE_ON_SCENE = false
|
|
|
|
this.handleResize()
|
|
this.startAnimation()
|
|
|
|
setTimeout(() => {
|
|
this.forceRender()
|
|
}, 100)
|
|
}
|
|
|
|
forceRender(): void {
|
|
const delta = this.clock.getDelta()
|
|
this.viewHelperManager.update(delta)
|
|
this.controlsManager.update()
|
|
|
|
this.renderMainScene()
|
|
|
|
if (this.previewManager.showPreview) {
|
|
this.renderPreviewScene()
|
|
}
|
|
|
|
this.resetViewport()
|
|
|
|
if (this.viewHelperManager.viewHelper.render) {
|
|
this.viewHelperManager.viewHelper.render(this.renderer)
|
|
}
|
|
|
|
this.INITIAL_RENDER_DONE = true
|
|
}
|
|
|
|
renderMainScene(): void {
|
|
const width = this.renderer.domElement.clientWidth
|
|
const height = this.renderer.domElement.clientHeight
|
|
|
|
this.renderer.setViewport(0, 0, width, height)
|
|
this.renderer.setScissor(0, 0, width, height)
|
|
this.renderer.setScissorTest(true)
|
|
|
|
this.renderer.clear()
|
|
this.sceneManager.renderBackground()
|
|
this.renderer.render(
|
|
this.sceneManager.scene,
|
|
this.cameraManager.activeCamera
|
|
)
|
|
}
|
|
|
|
renderPreviewScene(): void {
|
|
this.previewManager.renderPreview()
|
|
}
|
|
|
|
resetViewport(): void {
|
|
const width = this.renderer.domElement.clientWidth
|
|
const height = this.renderer.domElement.clientHeight
|
|
|
|
this.renderer.setViewport(0, 0, width, height)
|
|
this.renderer.setScissor(0, 0, width, height)
|
|
this.renderer.setScissorTest(false)
|
|
}
|
|
|
|
private getActiveCamera(): THREE.Camera {
|
|
return this.cameraManager.activeCamera
|
|
}
|
|
|
|
private getControls() {
|
|
return this.controlsManager.controls
|
|
}
|
|
|
|
private setupCamera(size: THREE.Vector3): void {
|
|
this.cameraManager.setupForModel(size)
|
|
}
|
|
|
|
private startAnimation(): void {
|
|
const animate = () => {
|
|
this.animationFrameId = requestAnimationFrame(animate)
|
|
|
|
if (!this.isActive()) {
|
|
return
|
|
}
|
|
|
|
const delta = this.clock.getDelta()
|
|
this.viewHelperManager.update(delta)
|
|
this.controlsManager.update()
|
|
|
|
this.renderMainScene()
|
|
|
|
if (this.previewManager.showPreview) {
|
|
this.renderPreviewScene()
|
|
}
|
|
|
|
this.resetViewport()
|
|
|
|
if (this.viewHelperManager.viewHelper.render) {
|
|
this.viewHelperManager.viewHelper.render(this.renderer)
|
|
}
|
|
}
|
|
|
|
animate()
|
|
}
|
|
|
|
updateStatusMouseOnNode(onNode: boolean): void {
|
|
this.STATUS_MOUSE_ON_NODE = onNode
|
|
}
|
|
|
|
updateStatusMouseOnScene(onScene: boolean): void {
|
|
this.STATUS_MOUSE_ON_SCENE = onScene
|
|
}
|
|
|
|
isActive(): boolean {
|
|
return (
|
|
this.STATUS_MOUSE_ON_NODE ||
|
|
this.STATUS_MOUSE_ON_SCENE ||
|
|
this.isRecording() ||
|
|
!this.INITIAL_RENDER_DONE
|
|
)
|
|
}
|
|
|
|
async exportModel(format: string): Promise<void> {
|
|
if (!this.modelManager.currentModel) {
|
|
throw new Error('No model to export')
|
|
}
|
|
|
|
const exportMessage = `Exporting as ${format.toUpperCase()}...`
|
|
this.eventManager.emitEvent('exportLoadingStart', exportMessage)
|
|
|
|
try {
|
|
const model = this.modelManager.currentModel.clone()
|
|
|
|
const originalFileName = this.modelManager.originalFileName || 'model'
|
|
const filename = `${originalFileName}.${format}`
|
|
|
|
const originalURL = this.modelManager.originalURL
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
|
|
switch (format) {
|
|
case 'glb':
|
|
await ModelExporter.exportGLB(model, filename, originalURL)
|
|
break
|
|
case 'obj':
|
|
await ModelExporter.exportOBJ(model, filename, originalURL)
|
|
break
|
|
case 'stl':
|
|
await ModelExporter.exportSTL(model, filename), originalURL
|
|
break
|
|
default:
|
|
throw new Error(`Unsupported export format: ${format}`)
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
} catch (error) {
|
|
console.error(`Error exporting model as ${format}:`, error)
|
|
throw error
|
|
} finally {
|
|
this.eventManager.emitEvent('exportLoadingEnd', null)
|
|
}
|
|
}
|
|
|
|
async applyTexture(texturePath: string): Promise<void> {
|
|
if (!this.modelManager.currentModel) {
|
|
throw new Error('No model to apply texture to')
|
|
}
|
|
|
|
this.eventManager.emitEvent('textureLoadingStart', null)
|
|
|
|
try {
|
|
await this.modelManager.applyTexture(texturePath)
|
|
} catch (error) {
|
|
console.error('Error applying texture:', error)
|
|
throw error
|
|
} finally {
|
|
this.eventManager.emitEvent('textureLoadingEnd', null)
|
|
}
|
|
}
|
|
|
|
setBackgroundColor(color: string): void {
|
|
this.sceneManager.setBackgroundColor(color)
|
|
this.forceRender()
|
|
}
|
|
|
|
async setBackgroundImage(uploadPath: string): Promise<void> {
|
|
await this.sceneManager.setBackgroundImage(uploadPath)
|
|
|
|
this.previewManager.updateBackgroundTexture(
|
|
this.sceneManager.backgroundTexture
|
|
)
|
|
|
|
this.forceRender()
|
|
}
|
|
|
|
removeBackgroundImage(): void {
|
|
this.sceneManager.removeBackgroundImage()
|
|
|
|
if (this.previewManager.previewCamera) {
|
|
this.previewManager.updateBackgroundTexture(null)
|
|
}
|
|
|
|
this.forceRender()
|
|
}
|
|
|
|
toggleGrid(showGrid: boolean): void {
|
|
this.sceneManager.toggleGrid(showGrid)
|
|
this.forceRender()
|
|
}
|
|
|
|
toggleCamera(cameraType?: 'perspective' | 'orthographic'): void {
|
|
this.cameraManager.toggleCamera(cameraType)
|
|
|
|
this.controlsManager.updateCamera(this.cameraManager.activeCamera)
|
|
this.viewHelperManager.recreateViewHelper()
|
|
|
|
this.handleResize()
|
|
this.forceRender()
|
|
}
|
|
|
|
getCurrentCameraType(): 'perspective' | 'orthographic' {
|
|
return this.cameraManager.getCurrentCameraType()
|
|
}
|
|
|
|
setCameraState(state: CameraState): void {
|
|
this.cameraManager.setCameraState(state)
|
|
|
|
if (this.previewManager.showPreview) {
|
|
this.previewManager.syncWithMainCamera()
|
|
}
|
|
|
|
this.forceRender()
|
|
}
|
|
|
|
getCameraState(): CameraState {
|
|
return this.cameraManager.getCameraState()
|
|
}
|
|
|
|
setFOV(fov: number): void {
|
|
this.cameraManager.setFOV(fov)
|
|
this.forceRender()
|
|
}
|
|
|
|
setEdgeThreshold(threshold: number): void {
|
|
this.modelManager.setEdgeThreshold(threshold)
|
|
this.forceRender()
|
|
}
|
|
|
|
setMaterialMode(mode: MaterialMode): void {
|
|
this.modelManager.setMaterialMode(mode)
|
|
this.forceRender()
|
|
}
|
|
|
|
async loadModel(url: string, originalFileName?: string): Promise<void> {
|
|
this.cameraManager.reset()
|
|
this.controlsManager.reset()
|
|
this.modelManager.reset()
|
|
|
|
await this.loaderManager.loadModel(url, originalFileName)
|
|
|
|
this.handleResize()
|
|
this.forceRender()
|
|
}
|
|
|
|
clearModel(): void {
|
|
this.modelManager.clearModel()
|
|
this.forceRender()
|
|
}
|
|
|
|
setUpDirection(direction: UpDirection): void {
|
|
this.modelManager.setUpDirection(direction)
|
|
this.forceRender()
|
|
}
|
|
|
|
setLightIntensity(intensity: number): void {
|
|
this.lightingManager.setLightIntensity(intensity)
|
|
this.forceRender()
|
|
}
|
|
|
|
togglePreview(showPreview: boolean): void {
|
|
this.previewManager.togglePreview(showPreview)
|
|
this.forceRender()
|
|
}
|
|
|
|
setTargetSize(width: number, height: number): void {
|
|
this.previewManager.setTargetSize(width, height)
|
|
this.forceRender()
|
|
}
|
|
|
|
addEventListener(event: string, callback: (data?: any) => void): void {
|
|
this.eventManager.addEventListener(event, callback)
|
|
}
|
|
|
|
removeEventListener(event: string, callback: (data?: any) => void): void {
|
|
this.eventManager.removeEventListener(event, callback)
|
|
}
|
|
|
|
refreshViewport(): void {
|
|
this.handleResize()
|
|
this.forceRender()
|
|
}
|
|
|
|
handleResize(): void {
|
|
const parentElement = this.renderer?.domElement?.parentElement
|
|
|
|
if (!parentElement) {
|
|
console.warn('Parent element not found')
|
|
return
|
|
}
|
|
|
|
const width = parentElement.clientWidth
|
|
const height = parentElement.clientHeight
|
|
|
|
this.cameraManager.handleResize(width, height)
|
|
this.sceneManager.handleResize(width, height)
|
|
|
|
this.renderer.setSize(width, height)
|
|
|
|
this.previewManager.handleResize()
|
|
this.forceRender()
|
|
}
|
|
|
|
captureScene(width: number, height: number): Promise<CaptureResult> {
|
|
return this.sceneManager.captureScene(width, height)
|
|
}
|
|
|
|
loadNodeProperty(name: string, defaultValue: any) {
|
|
return this.nodeStorage.loadNodeProperty(name, defaultValue)
|
|
}
|
|
|
|
public async startRecording(): Promise<void> {
|
|
this.viewHelperManager.visibleViewHelper(false)
|
|
|
|
return this.recordingManager.startRecording()
|
|
}
|
|
|
|
public stopRecording(): void {
|
|
this.viewHelperManager.visibleViewHelper(true)
|
|
|
|
this.recordingManager.stopRecording()
|
|
|
|
this.eventManager.emitEvent('recordingStatusChange', false)
|
|
}
|
|
|
|
public isRecording(): boolean {
|
|
return this.recordingManager.getIsRecording()
|
|
}
|
|
|
|
public getRecordingDuration(): number {
|
|
return this.recordingManager.getRecordingDuration()
|
|
}
|
|
|
|
public getRecordingData(): string | null {
|
|
return this.recordingManager.getRecordingData()
|
|
}
|
|
|
|
public exportRecording(filename?: string): void {
|
|
this.recordingManager.exportRecording(filename)
|
|
}
|
|
|
|
public clearRecording(): void {
|
|
this.recordingManager.clearRecording()
|
|
}
|
|
|
|
public remove(): void {
|
|
this.renderer.forceContextLoss()
|
|
const canvas = this.renderer.domElement
|
|
const event = new Event('webglcontextlost', {
|
|
bubbles: true,
|
|
cancelable: true
|
|
})
|
|
canvas.dispatchEvent(event)
|
|
|
|
if (this.animationFrameId !== null) {
|
|
cancelAnimationFrame(this.animationFrameId)
|
|
}
|
|
|
|
this.sceneManager.dispose()
|
|
this.cameraManager.dispose()
|
|
this.controlsManager.dispose()
|
|
this.lightingManager.dispose()
|
|
this.viewHelperManager.dispose()
|
|
this.previewManager.dispose()
|
|
this.loaderManager.dispose()
|
|
this.modelManager.dispose()
|
|
this.recordingManager.dispose()
|
|
|
|
this.renderer.dispose()
|
|
this.renderer.domElement.remove()
|
|
}
|
|
}
|
|
|
|
export default Load3d
|