[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)
This commit is contained in:
Terry Jia
2025-11-15 06:36:36 -05:00
committed by GitHub
parent ba768c32f3
commit 7a11dc59b6
13 changed files with 187 additions and 234 deletions

View File

@@ -7,6 +7,7 @@ import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
AnimationItem,
CameraConfig,
CameraState,
CameraType,
LightConfig,
MaterialMode,
@@ -16,8 +17,10 @@ import type {
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useLoad3dService } from '@/services/load3dService'
type Load3dReadyCallback = (load3d: Load3d) => void
@@ -68,10 +71,6 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
const node = rawNode as LGraphNode
try {
load3d = new Load3d(containerRef, {
node
})
const widthWidget = node.widgets?.find((w) => w.name === 'width')
const heightWidget = node.widgets?.find((w) => w.name === 'height')
@@ -79,6 +78,27 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
isPreview.value = true
}
load3d = new Load3d(containerRef, {
width: widthWidget?.value as number | undefined,
height: heightWidget?.value as number | undefined,
// Provide dynamic dimension getter for reactive updates
getDimensions:
widthWidget && heightWidget
? () => ({
width: widthWidget.value as number,
height: heightWidget.value as number
})
: undefined,
onContextMenu: (event) => {
const menuOptions = app.canvas.getNodeMenuOptions(node)
new LiteGraph.ContextMenu(menuOptions, {
event,
title: node.type,
extra: node
})
}
})
await restoreConfigurationsFromNode(node)
node.onMouseEnter = function () {
@@ -487,8 +507,26 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
hasRecording.value = recordingDuration.value > 0
}
},
animationListChange: (newValue: any) => {
animationListChange: (newValue: AnimationItem[]) => {
animations.value = newValue
},
cameraChanged: (cameraState: CameraState) => {
const rawNode = toRaw(nodeRef.value)
if (rawNode) {
const node = rawNode as LGraphNode
if (!node.properties) node.properties = {}
const cameraConfigProp = node.properties['Camera Config']
if (cameraConfigProp) {
;(cameraConfigProp as CameraConfig).state = cameraState
} else {
node.properties['Camera Config'] = {
cameraType: cameraConfig.value.cameraType,
fov: cameraConfig.value.fov,
state: cameraState
}
}
}
}
} as const

View File

@@ -4,6 +4,7 @@ import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
BackgroundRenderModeType,
CameraState,
CameraType,
MaterialMode,
UpDirection
@@ -20,7 +21,7 @@ interface Load3dViewerState {
cameraType: CameraType
fov: number
lightIntensity: number
cameraState: any
cameraState: CameraState | null
backgroundImage: string
backgroundRenderMode: BackgroundRenderModeType
upDirection: UpDirection
@@ -183,9 +184,19 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
sourceLoad3d = source
try {
const width = node.widgets?.find((w) => w.name === 'width')
const height = node.widgets?.find((w) => w.name === 'height')
load3d = new Load3d(containerRef, {
node: node,
disablePreview: true,
width: width ? (toRaw(width).value as number) : undefined,
height: height ? (toRaw(height).value as number) : undefined,
getDimensions:
width && height
? () => ({
width: width.value as number,
height: height.value as number
})
: undefined,
isViewerMode: true
})
@@ -253,16 +264,6 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
upDirection: upDirection.value,
materialMode: materialMode.value
}
const width = node.widgets?.find((w) => w.name === 'width')
const height = node.widgets?.find((w) => w.name === 'height')
if (width && height) {
load3d.setTargetSize(
toRaw(width).value as number,
toRaw(height).value as number
)
}
} catch (error) {
console.error('Error initializing Load3d viewer:', error)
useToastStore().addAlert(
@@ -283,19 +284,9 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
try {
isStandaloneMode.value = true
const mockNode = {
widgets: [
{ name: 'width', value: 800 },
{ name: 'height', value: 600 }
],
properties: {},
graph: null,
type: 'AssetPreview'
} as unknown as LGraphNode
load3d = new Load3d(containerRef, {
node: mockNode,
disablePreview: true,
width: 800,
height: 600,
isViewerMode: true
})

View File

@@ -317,7 +317,7 @@ useExtensionService().registerExtension({
const cameraConfig = node.properties['Camera Config'] as any
const cameraState = cameraConfig?.state
const config = new Load3DConfiguration(load3d)
const config = new Load3DConfiguration(load3d, node.properties)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
const width = node.widgets?.find((w) => w.name === 'width')
@@ -444,7 +444,7 @@ useExtensionService().registerExtension({
const onExecuted = node.onExecuted
useLoad3d(node).waitForLoad3d((load3d) => {
const config = new Load3DConfiguration(load3d)
const config = new Load3DConfiguration(load3d, node.properties)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')

View File

@@ -5,8 +5,7 @@ import {
type CameraManagerInterface,
type CameraState,
type CameraType,
type EventManagerInterface,
type NodeStorageInterface
type EventManagerInterface
} from './interfaces'
export class CameraManager implements CameraManagerInterface {
@@ -17,7 +16,6 @@ export class CameraManager implements CameraManagerInterface {
// @ts-expect-error unused variable
private renderer: THREE.WebGLRenderer
private eventManager: EventManagerInterface
private nodeStorage: NodeStorageInterface
private controls: OrbitControls | null = null
@@ -45,12 +43,10 @@ export class CameraManager implements CameraManagerInterface {
constructor(
renderer: THREE.WebGLRenderer,
eventManager: EventManagerInterface,
nodeStorage: NodeStorageInterface
eventManager: EventManagerInterface
) {
this.renderer = renderer
this.eventManager = eventManager
this.nodeStorage = nodeStorage
this.perspectiveCamera = new THREE.PerspectiveCamera(
this.DEFAULT_PERSPECTIVE_CAMERA.fov,
@@ -82,17 +78,7 @@ export class CameraManager implements CameraManagerInterface {
if (this.controls) {
this.controls.addEventListener('end', () => {
const cameraState = this.getCameraState()
const cameraConfig = this.nodeStorage.loadNodeProperty(
'Camera Config',
{
cameraType: this.getCurrentCameraType(),
fov: this.perspectiveCamera.fov
}
)
cameraConfig.state = cameraState
this.nodeStorage.storeNodeProperty('Camera Config', cameraConfig)
this.eventManager.emitEvent('cameraChanged', this.getCameraState())
})
}
}

View File

@@ -3,25 +3,20 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {
type ControlsManagerInterface,
type EventManagerInterface,
type NodeStorageInterface
type EventManagerInterface
} from './interfaces'
export class ControlsManager implements ControlsManagerInterface {
controls: OrbitControls
// @ts-expect-error unused variable
private eventManager: EventManagerInterface
private nodeStorage: NodeStorageInterface
private camera: THREE.Camera
constructor(
renderer: THREE.WebGLRenderer,
camera: THREE.Camera,
eventManager: EventManagerInterface,
nodeStorage: NodeStorageInterface
eventManager: EventManagerInterface
) {
this.eventManager = eventManager
this.nodeStorage = nodeStorage
this.camera = camera
const container = renderer.domElement.parentElement || renderer.domElement
@@ -44,15 +39,7 @@ export class ControlsManager implements ControlsManagerInterface {
: 'orthographic'
}
const cameraConfig = this.nodeStorage.loadNodeProperty('Camera Config', {
cameraType: cameraState.cameraType,
fov:
this.camera instanceof THREE.PerspectiveCamera
? (this.camera as THREE.PerspectiveCamera).fov
: 75
})
cameraConfig.state = cameraState
this.nodeStorage.storeNodeProperty('Camera Config', cameraConfig)
this.eventManager.emitEvent('cameraChanged', cameraState)
})
}

View File

@@ -2,10 +2,13 @@ import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
CameraConfig,
CameraState,
LightConfig,
ModelConfig,
SceneConfig
} from '@/extensions/core/load3d/interfaces'
import type { Dictionary } from '@/lib/litegraph/src/interfaces'
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
import { api } from '@/scripts/api'
@@ -13,14 +16,17 @@ import { api } from '@/scripts/api'
type Load3DConfigurationSettings = {
loadFolder: string
modelWidget: IBaseWidget
cameraState?: any
cameraState?: CameraState
width?: IBaseWidget
height?: IBaseWidget
bgImagePath?: string
}
class Load3DConfiguration {
constructor(private load3d: Load3d) {}
constructor(
private load3d: Load3d,
private properties?: Dictionary<NodeProperty | undefined>
) {}
configureForSaveMesh(loadFolder: 'input' | 'output', filePath: string) {
this.setupModelHandlingForSaveMesh(filePath, loadFolder)
@@ -62,7 +68,7 @@ class Load3DConfiguration {
private setupModelHandling(
modelWidget: IBaseWidget,
loadFolder: string,
cameraState?: any
cameraState?: CameraState
) {
const onModelWidgetUpdate = this.createModelUpdateHandler(
loadFolder,
@@ -110,48 +116,48 @@ class Load3DConfiguration {
}
private loadSceneConfig(): SceneConfig {
const defaultConfig: SceneConfig = {
if (this.properties && 'Scene Config' in this.properties) {
return this.properties['Scene Config'] as SceneConfig
}
return {
showGrid: useSettingStore().get('Comfy.Load3D.ShowGrid'),
backgroundColor:
'#' + useSettingStore().get('Comfy.Load3D.BackgroundColor'),
backgroundImage: ''
}
const config = this.load3d.loadNodeProperty('Scene Config', defaultConfig)
this.load3d.node.properties['Scene Config'] = config
return config
} as SceneConfig
}
private loadCameraConfig(): CameraConfig {
const defaultConfig: CameraConfig = {
cameraType: useSettingStore().get('Comfy.Load3D.CameraType'),
fov: 35
if (this.properties && 'Camera Config' in this.properties) {
return this.properties['Camera Config'] as CameraConfig
}
const config = this.load3d.loadNodeProperty('Camera Config', defaultConfig)
this.load3d.node.properties['Camera Config'] = config
return config
return {
cameraType: useSettingStore().get('Comfy.Load3D.CameraType'),
fov: 35
} as CameraConfig
}
private loadLightConfig(): LightConfig {
const defaultConfig: LightConfig = {
intensity: useSettingStore().get('Comfy.Load3D.LightIntensity')
if (this.properties && 'Light Config' in this.properties) {
return this.properties['Light Config'] as LightConfig
}
const config = this.load3d.loadNodeProperty('Light Config', defaultConfig)
this.load3d.node.properties['Light Config'] = config
return config
return {
intensity: useSettingStore().get('Comfy.Load3D.LightIntensity')
} as LightConfig
}
private loadModelConfig(): ModelConfig {
const defaultConfig: ModelConfig = {
upDirection: 'original',
materialMode: 'original'
if (this.properties && 'Model Config' in this.properties) {
return this.properties['Model Config'] as ModelConfig
}
const config = this.load3d.loadNodeProperty('Model Config', defaultConfig)
this.load3d.node.properties['Model Config'] = config
return config
return {
upDirection: 'original',
materialMode: 'original'
} as ModelConfig
}
private applySceneConfig(config: SceneConfig, bgImagePath?: string) {
@@ -188,7 +194,10 @@ class Load3DConfiguration {
this.load3d.setMaterialMode(config.materialMode)
}
private createModelUpdateHandler(loadFolder: string, cameraState?: any) {
private createModelUpdateHandler(
loadFolder: string,
cameraState?: CameraState
) {
let isFirstLoad = true
return async (value: string | number | boolean | object) => {
if (!value) return
@@ -209,7 +218,7 @@ class Load3DConfiguration {
const modelConfig = this.loadModelConfig()
this.applyModelConfig(modelConfig)
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
if (isFirstLoad && cameraState) {
try {
this.load3d.setCameraState(cameraState)
} catch (error) {
@@ -230,8 +239,8 @@ class Load3DConfiguration {
const subfolderParts = pathParts.slice(1, -1)
const subfolder = subfolderParts.join('/')
if (subfolder) {
this.load3d.node.properties['Resource Folder'] = subfolder
if (subfolder && this.properties) {
this.properties['Resource Folder'] = subfolder
}
}
}

View File

@@ -1,7 +1,5 @@
import * as THREE from 'three'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { AnimationManager } from './AnimationManager'
import { CameraManager } from './CameraManager'
import { ControlsManager } from './ControlsManager'
@@ -9,7 +7,6 @@ import { EventManager } from './EventManager'
import { LightingManager } from './LightingManager'
import { LoaderManager } from './LoaderManager'
import { ModelExporter } from './ModelExporter'
import { NodeStorage } from './NodeStorage'
import { RecordingManager } from './RecordingManager'
import { SceneManager } from './SceneManager'
import { SceneModelManager } from './SceneModelManager'
@@ -21,17 +18,16 @@ import {
type MaterialMode,
type UpDirection
} from './interfaces'
import { app } from '@/scripts/app'
class Load3d {
renderer: THREE.WebGLRenderer
protected clock: THREE.Clock
protected animationFrameId: number | null = null
node: LGraphNode
private loadingPromise: Promise<void> | null = null
private onContextMenuCallback?: (event: MouseEvent) => void
private getDimensionsCallback?: () => { width: number; height: number } | null
eventManager: EventManager
nodeStorage: NodeStorage
sceneManager: SceneManager
cameraManager: CameraManager
controlsManager: ControlsManager
@@ -59,23 +55,16 @@ class Load3d {
private readonly dragThreshold: number = 5
private contextMenuAbortController: AbortController | null = null
constructor(
container: Element | HTMLElement,
options: Load3DOptions = {
node: {} as LGraphNode
}
) {
this.node = options.node || ({} as LGraphNode)
constructor(container: Element | HTMLElement, options: Load3DOptions = {}) {
this.clock = new THREE.Clock()
this.isViewerMode = options.isViewerMode || false
this.onContextMenuCallback = options.onContextMenu
this.getDimensionsCallback = options.getDimensions
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
if (widthWidget && heightWidget) {
this.targetWidth = widthWidget.value as number
this.targetHeight = heightWidget.value as number
this.targetAspectRatio = this.targetWidth / this.targetHeight
if (options.width && options.height) {
this.targetWidth = options.width
this.targetHeight = options.height
this.targetAspectRatio = options.width / options.height
}
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
@@ -87,7 +76,6 @@ class Load3d {
container.appendChild(this.renderer.domElement)
this.eventManager = new EventManager()
this.nodeStorage = new NodeStorage(this.node)
this.sceneManager = new SceneManager(
this.renderer,
@@ -96,17 +84,12 @@ class Load3d {
this.eventManager
)
this.cameraManager = new CameraManager(
this.renderer,
this.eventManager,
this.nodeStorage
)
this.cameraManager = new CameraManager(this.renderer, this.eventManager)
this.controlsManager = new ControlsManager(
this.renderer,
this.cameraManager.activeCamera,
this.eventManager,
this.nodeStorage
this.eventManager
)
this.cameraManager.setControls(this.controlsManager.controls)
@@ -120,7 +103,7 @@ class Load3d {
this.renderer,
this.getActiveCamera.bind(this),
this.getControls.bind(this),
this.nodeStorage
this.eventManager
)
this.modelManager = new SceneModelManager(
@@ -221,13 +204,9 @@ class Load3d {
}
private showNodeContextMenu(event: MouseEvent): void {
const menuOptions = app.canvas.getNodeMenuOptions(this.node)
new LiteGraph.ContextMenu(menuOptions, {
event,
title: this.node.type,
extra: this.node
})
if (this.onContextMenuCallback) {
this.onContextMenuCallback(event)
}
}
getEventManager(): EventManager {
@@ -259,6 +238,17 @@ class Load3d {
return this.recordingManager
}
getTargetSize(): { width: number; height: number } {
return {
width: this.targetWidth,
height: this.targetHeight
}
}
private shouldMaintainAspectRatio(): boolean {
return this.isViewerMode || (this.targetWidth > 0 && this.targetHeight > 0)
}
forceRender(): void {
const delta = this.clock.getDelta()
this.animationManager.update(delta)
@@ -280,18 +270,16 @@ class Load3d {
const containerWidth = this.renderer.domElement.clientWidth
const containerHeight = this.renderer.domElement.clientHeight
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
const shouldMaintainAspectRatio =
(widthWidget && heightWidget) || this.isViewerMode
if (shouldMaintainAspectRatio) {
if (widthWidget && heightWidget) {
this.targetWidth = widthWidget.value as number
this.targetHeight = heightWidget.value as number
this.targetAspectRatio = this.targetWidth / this.targetHeight
if (this.getDimensionsCallback) {
const dims = this.getDimensionsCallback()
if (dims) {
this.targetWidth = dims.width
this.targetHeight = dims.height
this.targetAspectRatio = dims.width / dims.height
}
}
if (this.shouldMaintainAspectRatio()) {
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
@@ -321,7 +309,7 @@ class Load3d {
const renderAspectRatio = renderWidth / renderHeight
this.cameraManager.updateAspectRatio(renderAspectRatio)
} else {
// Preview3D: fill the entire container
// No aspect ratio constraint: fill the entire container
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
this.renderer.setScissor(0, 0, containerWidth, containerHeight)
this.renderer.setScissorTest(true)
@@ -459,13 +447,7 @@ class Load3d {
const containerWidth = this.renderer.domElement.clientWidth
const containerHeight = this.renderer.domElement.clientHeight
// Calculate the actual render area based on target aspect ratio
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
const shouldMaintainAspectRatio =
(widthWidget && heightWidget) || this.isViewerMode
if (shouldMaintainAspectRatio) {
if (this.shouldMaintainAspectRatio()) {
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
@@ -486,7 +468,7 @@ class Load3d {
renderHeight
)
} else {
// For Preview3D mode without aspect ratio constraints
// No aspect ratio constraints: fill container
this.sceneManager.updateBackgroundSize(
this.sceneManager.backgroundTexture,
this.sceneManager.backgroundMesh,
@@ -609,6 +591,7 @@ class Load3d {
this.targetWidth = width
this.targetHeight = height
this.targetAspectRatio = width / height
this.handleResize()
this.forceRender()
}
@@ -636,20 +619,16 @@ class Load3d {
const containerWidth = parentElement.clientWidth
const containerHeight = parentElement.clientHeight
// Check if we have width/height widgets (Load3D nodes) or if it's viewer mode
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
const shouldMaintainAspectRatio =
(widthWidget && heightWidget) || this.isViewerMode
if (shouldMaintainAspectRatio) {
// Load3D or viewer mode: maintain aspect ratio
if (widthWidget && heightWidget) {
this.targetWidth = widthWidget.value as number
this.targetHeight = heightWidget.value as number
this.targetAspectRatio = this.targetWidth / this.targetHeight
if (this.getDimensionsCallback) {
const dims = this.getDimensionsCallback()
if (dims) {
this.targetWidth = dims.width
this.targetHeight = dims.height
this.targetAspectRatio = dims.width / dims.height
}
}
if (this.shouldMaintainAspectRatio()) {
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
let renderHeight: number
@@ -666,7 +645,7 @@ class Load3d {
this.cameraManager.handleResize(renderWidth, renderHeight)
this.sceneManager.handleResize(renderWidth, renderHeight)
} else {
// Preview3D: use container dimensions directly
// No aspect ratio constraint: use container dimensions directly
this.renderer.setSize(containerWidth, containerHeight)
this.cameraManager.handleResize(containerWidth, containerHeight)
this.sceneManager.handleResize(containerWidth, containerHeight)
@@ -679,10 +658,6 @@ class Load3d {
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)

View File

@@ -1,32 +0,0 @@
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { type NodeStorageInterface } from './interfaces'
export class NodeStorage implements NodeStorageInterface {
private node: LGraphNode
constructor(node: LGraphNode = {} as LGraphNode) {
this.node = node
}
storeNodeProperty(name: string, value: any): void {
if (this.node && this.node.properties) {
this.node.properties[name] = value
}
}
loadNodeProperty(name: string, defaultValue: any): any {
if (
!this.node ||
!this.node.properties ||
!(name in this.node.properties)
) {
return defaultValue
}
return this.node.properties[name]
}
setNode(node: LGraphNode): void {
this.node = node
}
}

View File

@@ -3,7 +3,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
import {
type NodeStorageInterface,
type EventManagerInterface,
type ViewHelperManagerInterface
} from './interfaces'
@@ -13,7 +13,7 @@ export class ViewHelperManager implements ViewHelperManagerInterface {
private getActiveCamera: () => THREE.Camera
private getControls: () => OrbitControls
private nodeStorage: NodeStorageInterface
private eventManager: EventManagerInterface
// @ts-expect-error unused variable
private renderer: THREE.WebGLRenderer
@@ -21,12 +21,12 @@ export class ViewHelperManager implements ViewHelperManagerInterface {
renderer: THREE.WebGLRenderer,
getActiveCamera: () => THREE.Camera,
getControls: () => OrbitControls,
nodeStorage: NodeStorageInterface
eventManager: EventManagerInterface
) {
this.renderer = renderer
this.getActiveCamera = getActiveCamera
this.getControls = getControls
this.nodeStorage = nodeStorage
this.eventManager = eventManager
}
init(): void {}
@@ -87,18 +87,7 @@ export class ViewHelperManager implements ViewHelperManagerInterface {
: 'orthographic'
}
const cameraConfig = this.nodeStorage.loadNodeProperty(
'Camera Config',
{
cameraType: cameraState.cameraType,
fov:
this.getActiveCamera() instanceof THREE.PerspectiveCamera
? (this.getActiveCamera() as THREE.PerspectiveCamera).fov
: 75
}
)
cameraConfig.state = cameraState
this.nodeStorage.storeNodeProperty('Camera Config', cameraConfig)
this.eventManager.emitEvent('cameraChanged', cameraState)
}
}
}

View File

@@ -7,9 +7,6 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
export type MaterialMode = 'original' | 'normal' | 'wireframe' | 'depth'
export type UpDirection = 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
export type CameraType = 'perspective' | 'orthographic'
@@ -49,10 +46,19 @@ export interface EventCallback {
}
export interface Load3DOptions {
node?: LGraphNode
inputSpec?: CustomInputSpec
disablePreview?: boolean
// Optional target dimensions for aspect ratio control
width?: number
height?: number
// Dynamic dimension provider (called on every render)
// Use this for reactive dimensions that change over time
getDimensions?: () => { width: number; height: number } | null
// Viewer mode flag (affects aspect ratio behavior)
isViewerMode?: boolean
// Optional context menu callback
onContextMenu?: (event: MouseEvent) => void
}
export interface CaptureResult {
@@ -121,11 +127,6 @@ export interface EventManagerInterface {
emitEvent(event: string, data?: any): void
}
export interface NodeStorageInterface {
storeNodeProperty(name: string, value: any): void
loadNodeProperty(name: string, defaultValue: any): any
}
export interface AnimationManagerInterface extends BaseManager {
currentAnimation: THREE.AnimationMixer | null
animationActions: THREE.AnimationAction[]

View File

@@ -81,7 +81,7 @@ useExtensionService().registerExtension({
modelWidget.value = filePath
const config = new Load3DConfiguration(load3d)
const config = new Load3DConfiguration(load3d, node.properties)
config.configureForSaveMesh(fileInfo['type'], filePath)
}

View File

@@ -25,7 +25,9 @@ vi.mock('@/platform/updates/common/toastStore', () => ({
vi.mock('@/scripts/api', () => ({
api: {
apiURL: vi.fn()
apiURL: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn()
}
}))
@@ -157,9 +159,15 @@ describe('useLoad3d', () => {
await composable.initializeLoad3d(containerRef)
expect(Load3d).toHaveBeenCalledWith(containerRef, {
node: mockNode
})
expect(Load3d).toHaveBeenCalledWith(
containerRef,
expect.objectContaining({
width: 512,
height: 512,
getDimensions: expect.any(Function),
onContextMenu: expect.any(Function)
})
)
expect(nodeToLoad3dMap.has(mockNode)).toBe(true)
})

View File

@@ -161,9 +161,10 @@ describe('useLoad3dViewer', () => {
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
expect(Load3d).toHaveBeenCalledWith(containerRef, {
disablePreview: true,
isViewerMode: true,
node: mockNode
width: undefined,
height: undefined,
getDimensions: undefined,
isViewerMode: true
})
expect(mockLoad3dService.copyLoad3dState).toHaveBeenCalledWith(