From 7a11dc59b6b39e65c29db69cba8495018c5eab56 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Sat, 15 Nov 2025 06:36:36 -0500 Subject: [PATCH] [refactor] remove node as dependency in 3d node (#6707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 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) --- src/composables/useLoad3d.ts | 48 ++++++- src/composables/useLoad3dViewer.ts | 41 +++--- src/extensions/core/load3d.ts | 4 +- src/extensions/core/load3d/CameraManager.ts | 20 +-- src/extensions/core/load3d/ControlsManager.ts | 19 +-- .../core/load3d/Load3DConfiguration.ts | 69 +++++----- src/extensions/core/load3d/Load3d.ts | 119 +++++++----------- src/extensions/core/load3d/NodeStorage.ts | 32 ----- .../core/load3d/ViewHelperManager.ts | 21 +--- src/extensions/core/load3d/interfaces.ts | 23 ++-- src/extensions/core/saveMesh.ts | 2 +- tests-ui/tests/composables/useLoad3d.test.ts | 16 ++- .../tests/composables/useLoad3dViewer.test.ts | 7 +- 13 files changed, 187 insertions(+), 234 deletions(-) delete mode 100644 src/extensions/core/load3d/NodeStorage.ts diff --git a/src/composables/useLoad3d.ts b/src/composables/useLoad3d.ts index 30cfd0cf13..41a2cfff61 100644 --- a/src/composables/useLoad3d.ts +++ b/src/composables/useLoad3d.ts @@ -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) => { 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) => { 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) => { 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 diff --git a/src/composables/useLoad3dViewer.ts b/src/composables/useLoad3dViewer.ts index 36aac5ebfc..eb536c7358 100644 --- a/src/composables/useLoad3dViewer.ts +++ b/src/composables/useLoad3dViewer.ts @@ -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 }) diff --git a/src/extensions/core/load3d.ts b/src/extensions/core/load3d.ts index 143054c65c..c142cb272f 100644 --- a/src/extensions/core/load3d.ts +++ b/src/extensions/core/load3d.ts @@ -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') diff --git a/src/extensions/core/load3d/CameraManager.ts b/src/extensions/core/load3d/CameraManager.ts index 39d98a22c3..c87d384832 100644 --- a/src/extensions/core/load3d/CameraManager.ts +++ b/src/extensions/core/load3d/CameraManager.ts @@ -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()) }) } } diff --git a/src/extensions/core/load3d/ControlsManager.ts b/src/extensions/core/load3d/ControlsManager.ts index aae6d2ab3e..3018a0bcb4 100644 --- a/src/extensions/core/load3d/ControlsManager.ts +++ b/src/extensions/core/load3d/ControlsManager.ts @@ -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) }) } diff --git a/src/extensions/core/load3d/Load3DConfiguration.ts b/src/extensions/core/load3d/Load3DConfiguration.ts index a914de3883..0510a9c82a 100644 --- a/src/extensions/core/load3d/Load3DConfiguration.ts +++ b/src/extensions/core/load3d/Load3DConfiguration.ts @@ -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 + ) {} 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 } } } diff --git a/src/extensions/core/load3d/Load3d.ts b/src/extensions/core/load3d/Load3d.ts index 686e0b135f..c759acc1fa 100644 --- a/src/extensions/core/load3d/Load3d.ts +++ b/src/extensions/core/load3d/Load3d.ts @@ -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 | 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 { this.viewHelperManager.visibleViewHelper(false) diff --git a/src/extensions/core/load3d/NodeStorage.ts b/src/extensions/core/load3d/NodeStorage.ts deleted file mode 100644 index 09aac60d4b..0000000000 --- a/src/extensions/core/load3d/NodeStorage.ts +++ /dev/null @@ -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 - } -} diff --git a/src/extensions/core/load3d/ViewHelperManager.ts b/src/extensions/core/load3d/ViewHelperManager.ts index 8906a3426a..380153ed0d 100644 --- a/src/extensions/core/load3d/ViewHelperManager.ts +++ b/src/extensions/core/load3d/ViewHelperManager.ts @@ -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) } } } diff --git a/src/extensions/core/load3d/interfaces.ts b/src/extensions/core/load3d/interfaces.ts index b1be8af38b..fd25117388 100644 --- a/src/extensions/core/load3d/interfaces.ts +++ b/src/extensions/core/load3d/interfaces.ts @@ -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[] diff --git a/src/extensions/core/saveMesh.ts b/src/extensions/core/saveMesh.ts index 0955fe4cc5..ae311d47d0 100644 --- a/src/extensions/core/saveMesh.ts +++ b/src/extensions/core/saveMesh.ts @@ -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) } diff --git a/tests-ui/tests/composables/useLoad3d.test.ts b/tests-ui/tests/composables/useLoad3d.test.ts index fadafa4f87..a471d2ff66 100644 --- a/tests-ui/tests/composables/useLoad3d.test.ts +++ b/tests-ui/tests/composables/useLoad3d.test.ts @@ -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) }) diff --git a/tests-ui/tests/composables/useLoad3dViewer.test.ts b/tests-ui/tests/composables/useLoad3dViewer.test.ts index 4ec2d77984..a49c7aa75f 100644 --- a/tests-ui/tests/composables/useLoad3dViewer.test.ts +++ b/tests-ui/tests/composables/useLoad3dViewer.test.ts @@ -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(