mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-07 22:20:03 +00:00
[3d] add preview 3d animation node (#2341)
This commit is contained in:
139
src/extensions/core/load3d/Load3DConfiguration.ts
Normal file
139
src/extensions/core/load3d/Load3DConfiguration.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
class Load3DConfiguration {
|
||||
constructor(private load3d: Load3d) {}
|
||||
|
||||
configure(
|
||||
loadFolder: 'input' | 'output',
|
||||
modelWidget: IWidget,
|
||||
material: IWidget,
|
||||
bgColor: IWidget,
|
||||
lightIntensity: IWidget,
|
||||
upDirection: IWidget,
|
||||
fov: IWidget,
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
this.setupModelHandling(
|
||||
modelWidget,
|
||||
loadFolder,
|
||||
cameraState,
|
||||
postModelUpdateFunc
|
||||
)
|
||||
this.setupMaterial(material)
|
||||
this.setupBackground(bgColor)
|
||||
this.setupLighting(lightIntensity)
|
||||
this.setupDirection(upDirection)
|
||||
this.setupCamera(fov)
|
||||
this.setupDefaultProperties()
|
||||
}
|
||||
|
||||
private setupModelHandling(
|
||||
modelWidget: IWidget,
|
||||
loadFolder: 'input' | 'output',
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
const onModelWidgetUpdate = this.createModelUpdateHandler(
|
||||
loadFolder,
|
||||
cameraState,
|
||||
postModelUpdateFunc
|
||||
)
|
||||
if (modelWidget.value) {
|
||||
onModelWidgetUpdate(modelWidget.value)
|
||||
}
|
||||
modelWidget.callback = onModelWidgetUpdate
|
||||
}
|
||||
|
||||
private setupMaterial(material: IWidget) {
|
||||
material.callback = (value: 'original' | 'normal' | 'wireframe') => {
|
||||
this.load3d.setMaterialMode(value)
|
||||
}
|
||||
this.load3d.setMaterialMode(
|
||||
material.value as 'original' | 'normal' | 'wireframe'
|
||||
)
|
||||
}
|
||||
|
||||
private setupBackground(bgColor: IWidget) {
|
||||
bgColor.callback = (value: string) => {
|
||||
this.load3d.setBackgroundColor(value)
|
||||
}
|
||||
this.load3d.setBackgroundColor(bgColor.value as string)
|
||||
}
|
||||
|
||||
private setupLighting(lightIntensity: IWidget) {
|
||||
lightIntensity.callback = (value: number) => {
|
||||
this.load3d.setLightIntensity(value)
|
||||
}
|
||||
this.load3d.setLightIntensity(lightIntensity.value as number)
|
||||
}
|
||||
|
||||
private setupDirection(upDirection: IWidget) {
|
||||
upDirection.callback = (
|
||||
value: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
) => {
|
||||
this.load3d.setUpDirection(value)
|
||||
}
|
||||
this.load3d.setUpDirection(
|
||||
upDirection.value as 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
)
|
||||
}
|
||||
|
||||
private setupCamera(fov: IWidget) {
|
||||
fov.callback = (value: number) => {
|
||||
this.load3d.setFOV(value)
|
||||
}
|
||||
this.load3d.setFOV(fov.value as number)
|
||||
}
|
||||
|
||||
private setupDefaultProperties() {
|
||||
const cameraType = this.load3d.loadNodeProperty(
|
||||
'Camera Type',
|
||||
'perspective'
|
||||
)
|
||||
this.load3d.toggleCamera(cameraType)
|
||||
|
||||
const showGrid = this.load3d.loadNodeProperty('Show Grid', true)
|
||||
this.load3d.toggleGrid(showGrid)
|
||||
}
|
||||
|
||||
private createModelUpdateHandler(
|
||||
loadFolder: 'input' | 'output',
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
let isFirstLoad = true
|
||||
return async (value: string | number | boolean | object) => {
|
||||
if (!value) return
|
||||
|
||||
const filename = value as string
|
||||
const modelUrl = api.apiURL(
|
||||
Load3dUtils.getResourceURL(
|
||||
...Load3dUtils.splitFilePath(filename),
|
||||
loadFolder
|
||||
)
|
||||
)
|
||||
|
||||
await this.load3d.loadModel(modelUrl, filename)
|
||||
|
||||
if (postModelUpdateFunc) {
|
||||
postModelUpdateFunc(this.load3d)
|
||||
}
|
||||
|
||||
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
|
||||
try {
|
||||
this.load3d.setCameraState(cameraState)
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore camera state:', error)
|
||||
}
|
||||
isFirstLoad = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3DConfiguration
|
||||
122
src/extensions/core/load3d/Load3dUtils.ts
Normal file
122
src/extensions/core/load3d/Load3dUtils.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
class Load3dUtils {
|
||||
static async uploadTempImage(imageData: string, prefix: string) {
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
const name = `${prefix}_${Date.now()}.png`
|
||||
const file = new File([blob], name)
|
||||
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', 'threed')
|
||||
body.append('type', 'temp')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
const err = `Error uploading temp image: ${resp.status} - ${resp.statusText}`
|
||||
useToastStore().addAlert(err)
|
||||
throw new Error(err)
|
||||
}
|
||||
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
static async uploadFile(
|
||||
load3d: Load3d,
|
||||
file: File,
|
||||
fileInput?: HTMLInputElement
|
||||
) {
|
||||
let uploadPath
|
||||
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', '3d')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json()
|
||||
let path = data.name
|
||||
|
||||
if (data.subfolder) path = data.subfolder + '/' + path
|
||||
|
||||
uploadPath = path
|
||||
|
||||
const modelUrl = api.apiURL(
|
||||
this.getResourceURL(...this.splitFilePath(path), 'input')
|
||||
)
|
||||
await load3d.loadModel(modelUrl, file.name)
|
||||
|
||||
const fileExt = file.name.split('.').pop()?.toLowerCase()
|
||||
if (fileExt === 'obj' && fileInput?.files) {
|
||||
try {
|
||||
const mtlFile = Array.from(fileInput.files).find((f) =>
|
||||
f.name.toLowerCase().endsWith('.mtl')
|
||||
)
|
||||
|
||||
if (mtlFile) {
|
||||
const mtlFormData = new FormData()
|
||||
mtlFormData.append('image', mtlFile)
|
||||
mtlFormData.append('subfolder', '3d')
|
||||
|
||||
await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body: mtlFormData
|
||||
})
|
||||
}
|
||||
} catch (mtlError) {
|
||||
console.warn('Failed to upload MTL file:', mtlError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
useToastStore().addAlert(
|
||||
error instanceof Error ? error.message : 'Upload failed'
|
||||
)
|
||||
}
|
||||
|
||||
return uploadPath
|
||||
}
|
||||
|
||||
static splitFilePath(path: string): [string, string] {
|
||||
const folder_separator = path.lastIndexOf('/')
|
||||
if (folder_separator === -1) {
|
||||
return ['', path]
|
||||
}
|
||||
return [
|
||||
path.substring(0, folder_separator),
|
||||
path.substring(folder_separator + 1)
|
||||
]
|
||||
}
|
||||
|
||||
static getResourceURL(
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
type: string = 'input'
|
||||
): string {
|
||||
const params = [
|
||||
'filename=' + encodeURIComponent(filename),
|
||||
'type=' + type,
|
||||
'subfolder=' + subfolder,
|
||||
app.getRandParam().substring(1)
|
||||
].join('&')
|
||||
|
||||
return `/view?${params}`
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3dUtils
|
||||
Reference in New Issue
Block a user