mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
371 lines
12 KiB
TypeScript
371 lines
12 KiB
TypeScript
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
|
|
|
|
// @ts-expect-error unused variable
|
|
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)
|
|
}
|
|
|
|
syncWithMainCamera(): void {
|
|
if (!this.previewRenderer || !this.previewContainer || !this.showPreview) {
|
|
return
|
|
}
|
|
|
|
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
|
|
|
|
previewOrtho.zoom = activeOrtho.zoom
|
|
|
|
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.updateProjectionMatrix()
|
|
} else {
|
|
const activePerspective =
|
|
this.getActiveCamera() as THREE.PerspectiveCamera
|
|
const previewPerspective = this.previewCamera as THREE.PerspectiveCamera
|
|
|
|
previewPerspective.fov = activePerspective.fov
|
|
previewPerspective.zoom = activePerspective.zoom
|
|
previewPerspective.aspect = aspect
|
|
|
|
previewPerspective.updateProjectionMatrix()
|
|
}
|
|
|
|
this.previewCamera.lookAt(this.getControls().target)
|
|
|
|
this.updatePreviewRender()
|
|
}
|
|
|
|
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 {}
|
|
}
|