Files
ComfyUI_frontend/src/extensions/core/load3d/ViewHelperManager.ts
Terry Jia 7a11dc59b6 [refactor] remove node as dependency in 3d node (#6707)
## Summary

This PR refactors the Load3d 3D rendering system to remove its direct
dependency on LGraphNode, making it a more decoupled and reusable
component. The core rendering engine is now framework-agnostic and can
be used in any context, not just within LiteGraph nodes.

## Changes

1. Decoupled Load3d from LGraphNode
  - Before: Load3d directly accessed node.widgets and node.properties
- After: Load3d accepts optional parameters and callbacks, delegating
node integration to the calling code

2. Event-Driven State Management
  - Removed internal storage from Load3d core components
- Camera, controls, and view helper managers now emit cameraChanged
events instead of directly storing state
- External code (e.g., useLoad3d) listens to events and handles
persistence to node.properties

3. Reactive Dimension Updates

- Introduced getDimensions callback to support reactive dimension
updates
- Fixes the issue where dimension changes in vueNodes mode required a
refresh
- The callback is invoked on every render to get fresh width/height
values

4. Improved Configuration System

- Load3DConfiguration now accepts properties: Dictionary<NodeProperty |
undefined> instead of custom storage
  interface
  - Uses official LiteGraph type definitions (Dictionary, NodeProperty)
  - More semantic parameter naming: storage → properties

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6707-refactor-remove-node-as-dependency-in-3d-node-2ab6d73d365081ffac1cdce354781ce8)
by [Unito](https://www.unito.io)
2025-11-15 06:36:36 -05:00

121 lines
3.4 KiB
TypeScript

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import {
type EventManagerInterface,
type ViewHelperManagerInterface
} from './interfaces'
export class ViewHelperManager implements ViewHelperManagerInterface {
viewHelper: ViewHelper = {} as ViewHelper
viewHelperContainer: HTMLDivElement = {} as HTMLDivElement
private getActiveCamera: () => THREE.Camera
private getControls: () => OrbitControls
private eventManager: EventManagerInterface
// @ts-expect-error unused variable
private renderer: THREE.WebGLRenderer
constructor(
renderer: THREE.WebGLRenderer,
getActiveCamera: () => THREE.Camera,
getControls: () => OrbitControls,
eventManager: EventManagerInterface
) {
this.renderer = renderer
this.getActiveCamera = getActiveCamera
this.getControls = getControls
this.eventManager = eventManager
}
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) {
const cameraState = {
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'
}
this.eventManager.emitEvent('cameraChanged', cameraState)
}
}
}
handleResize(): void {}
visibleViewHelper(visible: boolean) {
if (visible) {
this.viewHelper.visible = true
this.viewHelperContainer.style.display = 'block'
} else {
this.viewHelper.visible = false
this.viewHelperContainer.style.display = 'none'
}
}
recreateViewHelper(): void {
if (this.viewHelper) {
this.viewHelper.dispose()
}
this.viewHelper = new ViewHelper(
this.getActiveCamera(),
this.viewHelperContainer
)
this.viewHelper.center = this.getControls().target
}
reset(): void {}
}