[refactor] refactor load3d (#5765)

Summary

Fully Refactored the Load3D module to improve architecture and
maintainability by consolidating functionality into a
centralized composable pattern and simplifying component structure. and
support VueNodes system

  Changes

- Architecture: Introduced new useLoad3d composable to centralize 3D
loading logic and state
  management
- Component Simplification: Removed redundant components
(Load3DAnimation.vue, Load3DAnimationScene.vue,
  PreviewManager.ts) 
- Support VueNodes
- improve config store
- remove lineart output due Animation doesnot support it, may add it
back later
- remove Preview screen and keep scene in fixed ratio in load3d (not
affect preview3d)
- improve record video feature which will already record video by same
ratio as scene
Need BE change https://github.com/comfyanonymous/ComfyUI/pull/10025


https://github.com/user-attachments/assets/9e038729-84a0-45ad-b0f2-11c57d7e0c9a



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5765-refactor-refactor-load3d-2796d73d365081728297cc486e2e9052)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2025-10-31 16:19:35 -04:00
committed by GitHub
parent 91b5a7de17
commit afa10f7a1e
51 changed files with 2784 additions and 4200 deletions

View File

@@ -1,20 +1,15 @@
import { toRaw } from 'vue'
import { nodeToLoad3dMap } from '@/composables/useLoad3d'
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import type Load3d from '@/extensions/core/load3d/Load3d'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
type Load3dReadyCallback = (load3d: Load3d | Load3dAnimation) => void
const viewerInstances = new Map<NodeId, any>()
export class Load3dService {
private static instance: Load3dService
private nodeToLoad3dMap = new Map<LGraphNode, Load3d | Load3dAnimation>()
private pendingCallbacks = new Map<LGraphNode, Load3dReadyCallback[]>()
private constructor() {}
@@ -25,84 +20,14 @@ export class Load3dService {
return Load3dService.instance
}
registerLoad3d(
node: LGraphNode,
container: HTMLElement,
inputSpec: CustomInputSpec
) {
getLoad3d(node: LGraphNode): Load3d | null {
const rawNode = toRaw(node)
if (this.nodeToLoad3dMap.has(rawNode)) {
this.removeLoad3d(rawNode)
}
const type = inputSpec.type
const isAnimation = type.includes('Animation')
const Load3dClass = isAnimation ? Load3dAnimation : Load3d
const instance = new Load3dClass(container, {
node: rawNode,
inputSpec: inputSpec
})
rawNode.onMouseEnter = function () {
instance.refreshViewport()
instance.updateStatusMouseOnNode(true)
}
rawNode.onMouseLeave = function () {
instance.updateStatusMouseOnNode(false)
}
rawNode.onResize = function () {
instance.handleResize()
}
rawNode.onDrawBackground = function () {
instance.renderer.domElement.hidden = this.flags.collapsed ?? false
}
this.nodeToLoad3dMap.set(rawNode, instance)
const callbacks = this.pendingCallbacks.get(rawNode)
if (callbacks) {
callbacks.forEach((callback) => callback(instance))
this.pendingCallbacks.delete(rawNode)
}
return instance
return nodeToLoad3dMap.get(rawNode) || null
}
getLoad3d(node: LGraphNode): Load3d | Load3dAnimation | null {
const rawNode = toRaw(node)
return this.nodeToLoad3dMap.get(rawNode) || null
}
waitForLoad3d(node: LGraphNode, callback: Load3dReadyCallback): void {
const rawNode = toRaw(node)
const existingInstance = this.nodeToLoad3dMap.get(rawNode)
if (existingInstance) {
callback(existingInstance)
return
}
if (!this.pendingCallbacks.has(rawNode)) {
this.pendingCallbacks.set(rawNode, [])
}
this.pendingCallbacks.get(rawNode)!.push(callback)
}
getNodeByLoad3d(load3d: Load3d | Load3dAnimation): LGraphNode | null {
for (const [node, instance] of this.nodeToLoad3dMap) {
getNodeByLoad3d(load3d: Load3d): LGraphNode | null {
for (const [node, instance] of nodeToLoad3dMap) {
if (instance === load3d) {
return node
}
@@ -113,22 +38,19 @@ export class Load3dService {
removeLoad3d(node: LGraphNode) {
const rawNode = toRaw(node)
const instance = this.nodeToLoad3dMap.get(rawNode)
const instance = nodeToLoad3dMap.get(rawNode)
if (instance) {
instance.remove()
this.nodeToLoad3dMap.delete(rawNode)
nodeToLoad3dMap.delete(rawNode)
}
this.pendingCallbacks.delete(rawNode)
}
clear() {
for (const [node] of this.nodeToLoad3dMap) {
for (const [node] of nodeToLoad3dMap) {
this.removeLoad3d(node)
}
this.pendingCallbacks.clear()
}
getOrCreateViewer(node: LGraphNode) {
@@ -149,7 +71,7 @@ export class Load3dService {
viewerInstances.delete(node.id)
}
async copyLoad3dState(source: Load3d, target: Load3d | Load3dAnimation) {
async copyLoad3dState(source: Load3d, target: Load3d) {
const sourceModel = source.modelManager.currentModel
if (sourceModel) {
@@ -188,12 +110,13 @@ export class Load3dService {
.getCurrentBackgroundInfo()
if (sourceBackgroundInfo.type === 'image') {
const sourceNode = this.getNodeByLoad3d(source)
const backgroundPath = sourceNode?.properties?.[
'Background Image'
] as string
const sceneConfig = sourceNode?.properties?.['Scene Config'] as any
const backgroundPath = sceneConfig?.backgroundImage
if (backgroundPath) {
await target.setBackgroundImage(backgroundPath)
}
} else {
await target.setBackgroundImage('')
}
target.setLightIntensity(
@@ -203,11 +126,6 @@ export class Load3dService {
if (sourceCameraType === 'perspective') {
target.setFOV(source.getCameraManager().perspectiveCamera.fov)
}
const sourceNode = this.getNodeByLoad3d(source)
if (sourceNode?.properties?.['Edge Threshold']) {
target.setEdgeThreshold(sourceNode.properties['Edge Threshold'] as number)
}
}
handleViewportRefresh(load3d: Load3d | null) {
@@ -230,6 +148,11 @@ export class Load3dService {
if (viewer.needApplyChanges.value) {
await viewer.applyChanges()
// Sync configuration back to the node's UI
if ((node as any).syncLoad3dConfig) {
;(node as any).syncLoad3dConfig()
}
}
useLoad3dService().removeViewer(node)