Skip to content

Commit 7a11dc5

Browse files
authored
[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)
1 parent ba768c3 commit 7a11dc5

File tree

13 files changed

+187
-234
lines changed

13 files changed

+187
-234
lines changed

src/composables/useLoad3d.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
77
import type {
88
AnimationItem,
99
CameraConfig,
10+
CameraState,
1011
CameraType,
1112
LightConfig,
1213
MaterialMode,
@@ -16,8 +17,10 @@ import type {
1617
} from '@/extensions/core/load3d/interfaces'
1718
import { t } from '@/i18n'
1819
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
20+
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
1921
import { useToastStore } from '@/platform/updates/common/toastStore'
2022
import { api } from '@/scripts/api'
23+
import { app } from '@/scripts/app'
2124
import { useLoad3dService } from '@/services/load3dService'
2225

2326
type Load3dReadyCallback = (load3d: Load3d) => void
@@ -68,17 +71,34 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
6871
const node = rawNode as LGraphNode
6972

7073
try {
71-
load3d = new Load3d(containerRef, {
72-
node
73-
})
74-
7574
const widthWidget = node.widgets?.find((w) => w.name === 'width')
7675
const heightWidget = node.widgets?.find((w) => w.name === 'height')
7776

7877
if (!(widthWidget && heightWidget)) {
7978
isPreview.value = true
8079
}
8180

81+
load3d = new Load3d(containerRef, {
82+
width: widthWidget?.value as number | undefined,
83+
height: heightWidget?.value as number | undefined,
84+
// Provide dynamic dimension getter for reactive updates
85+
getDimensions:
86+
widthWidget && heightWidget
87+
? () => ({
88+
width: widthWidget.value as number,
89+
height: heightWidget.value as number
90+
})
91+
: undefined,
92+
onContextMenu: (event) => {
93+
const menuOptions = app.canvas.getNodeMenuOptions(node)
94+
new LiteGraph.ContextMenu(menuOptions, {
95+
event,
96+
title: node.type,
97+
extra: node
98+
})
99+
}
100+
})
101+
82102
await restoreConfigurationsFromNode(node)
83103

84104
node.onMouseEnter = function () {
@@ -487,8 +507,26 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
487507
hasRecording.value = recordingDuration.value > 0
488508
}
489509
},
490-
animationListChange: (newValue: any) => {
510+
animationListChange: (newValue: AnimationItem[]) => {
491511
animations.value = newValue
512+
},
513+
cameraChanged: (cameraState: CameraState) => {
514+
const rawNode = toRaw(nodeRef.value)
515+
if (rawNode) {
516+
const node = rawNode as LGraphNode
517+
if (!node.properties) node.properties = {}
518+
const cameraConfigProp = node.properties['Camera Config']
519+
520+
if (cameraConfigProp) {
521+
;(cameraConfigProp as CameraConfig).state = cameraState
522+
} else {
523+
node.properties['Camera Config'] = {
524+
cameraType: cameraConfig.value.cameraType,
525+
fov: cameraConfig.value.fov,
526+
state: cameraState
527+
}
528+
}
529+
}
492530
}
493531
} as const
494532

src/composables/useLoad3dViewer.ts

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Load3d from '@/extensions/core/load3d/Load3d'
44
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
55
import type {
66
BackgroundRenderModeType,
7+
CameraState,
78
CameraType,
89
MaterialMode,
910
UpDirection
@@ -20,7 +21,7 @@ interface Load3dViewerState {
2021
cameraType: CameraType
2122
fov: number
2223
lightIntensity: number
23-
cameraState: any
24+
cameraState: CameraState | null
2425
backgroundImage: string
2526
backgroundRenderMode: BackgroundRenderModeType
2627
upDirection: UpDirection
@@ -183,9 +184,19 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
183184
sourceLoad3d = source
184185

185186
try {
187+
const width = node.widgets?.find((w) => w.name === 'width')
188+
const height = node.widgets?.find((w) => w.name === 'height')
189+
186190
load3d = new Load3d(containerRef, {
187-
node: node,
188-
disablePreview: true,
191+
width: width ? (toRaw(width).value as number) : undefined,
192+
height: height ? (toRaw(height).value as number) : undefined,
193+
getDimensions:
194+
width && height
195+
? () => ({
196+
width: width.value as number,
197+
height: height.value as number
198+
})
199+
: undefined,
189200
isViewerMode: true
190201
})
191202

@@ -253,16 +264,6 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
253264
upDirection: upDirection.value,
254265
materialMode: materialMode.value
255266
}
256-
257-
const width = node.widgets?.find((w) => w.name === 'width')
258-
const height = node.widgets?.find((w) => w.name === 'height')
259-
260-
if (width && height) {
261-
load3d.setTargetSize(
262-
toRaw(width).value as number,
263-
toRaw(height).value as number
264-
)
265-
}
266267
} catch (error) {
267268
console.error('Error initializing Load3d viewer:', error)
268269
useToastStore().addAlert(
@@ -283,19 +284,9 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
283284
try {
284285
isStandaloneMode.value = true
285286

286-
const mockNode = {
287-
widgets: [
288-
{ name: 'width', value: 800 },
289-
{ name: 'height', value: 600 }
290-
],
291-
properties: {},
292-
graph: null,
293-
type: 'AssetPreview'
294-
} as unknown as LGraphNode
295-
296287
load3d = new Load3d(containerRef, {
297-
node: mockNode,
298-
disablePreview: true,
288+
width: 800,
289+
height: 600,
299290
isViewerMode: true
300291
})
301292

src/extensions/core/load3d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ useExtensionService().registerExtension({
317317
const cameraConfig = node.properties['Camera Config'] as any
318318
const cameraState = cameraConfig?.state
319319

320-
const config = new Load3DConfiguration(load3d)
320+
const config = new Load3DConfiguration(load3d, node.properties)
321321

322322
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
323323
const width = node.widgets?.find((w) => w.name === 'width')
@@ -444,7 +444,7 @@ useExtensionService().registerExtension({
444444
const onExecuted = node.onExecuted
445445

446446
useLoad3d(node).waitForLoad3d((load3d) => {
447-
const config = new Load3DConfiguration(load3d)
447+
const config = new Load3DConfiguration(load3d, node.properties)
448448

449449
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
450450

src/extensions/core/load3d/CameraManager.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {
55
type CameraManagerInterface,
66
type CameraState,
77
type CameraType,
8-
type EventManagerInterface,
9-
type NodeStorageInterface
8+
type EventManagerInterface
109
} from './interfaces'
1110

1211
export class CameraManager implements CameraManagerInterface {
@@ -17,7 +16,6 @@ export class CameraManager implements CameraManagerInterface {
1716
// @ts-expect-error unused variable
1817
private renderer: THREE.WebGLRenderer
1918
private eventManager: EventManagerInterface
20-
private nodeStorage: NodeStorageInterface
2119

2220
private controls: OrbitControls | null = null
2321

@@ -45,12 +43,10 @@ export class CameraManager implements CameraManagerInterface {
4543

4644
constructor(
4745
renderer: THREE.WebGLRenderer,
48-
eventManager: EventManagerInterface,
49-
nodeStorage: NodeStorageInterface
46+
eventManager: EventManagerInterface
5047
) {
5148
this.renderer = renderer
5249
this.eventManager = eventManager
53-
this.nodeStorage = nodeStorage
5450

5551
this.perspectiveCamera = new THREE.PerspectiveCamera(
5652
this.DEFAULT_PERSPECTIVE_CAMERA.fov,
@@ -82,17 +78,7 @@ export class CameraManager implements CameraManagerInterface {
8278

8379
if (this.controls) {
8480
this.controls.addEventListener('end', () => {
85-
const cameraState = this.getCameraState()
86-
87-
const cameraConfig = this.nodeStorage.loadNodeProperty(
88-
'Camera Config',
89-
{
90-
cameraType: this.getCurrentCameraType(),
91-
fov: this.perspectiveCamera.fov
92-
}
93-
)
94-
cameraConfig.state = cameraState
95-
this.nodeStorage.storeNodeProperty('Camera Config', cameraConfig)
81+
this.eventManager.emitEvent('cameraChanged', this.getCameraState())
9682
})
9783
}
9884
}

src/extensions/core/load3d/ControlsManager.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,20 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
33

44
import {
55
type ControlsManagerInterface,
6-
type EventManagerInterface,
7-
type NodeStorageInterface
6+
type EventManagerInterface
87
} from './interfaces'
98

109
export class ControlsManager implements ControlsManagerInterface {
1110
controls: OrbitControls
12-
// @ts-expect-error unused variable
1311
private eventManager: EventManagerInterface
14-
private nodeStorage: NodeStorageInterface
1512
private camera: THREE.Camera
1613

1714
constructor(
1815
renderer: THREE.WebGLRenderer,
1916
camera: THREE.Camera,
20-
eventManager: EventManagerInterface,
21-
nodeStorage: NodeStorageInterface
17+
eventManager: EventManagerInterface
2218
) {
2319
this.eventManager = eventManager
24-
this.nodeStorage = nodeStorage
2520
this.camera = camera
2621

2722
const container = renderer.domElement.parentElement || renderer.domElement
@@ -44,15 +39,7 @@ export class ControlsManager implements ControlsManagerInterface {
4439
: 'orthographic'
4540
}
4641

47-
const cameraConfig = this.nodeStorage.loadNodeProperty('Camera Config', {
48-
cameraType: cameraState.cameraType,
49-
fov:
50-
this.camera instanceof THREE.PerspectiveCamera
51-
? (this.camera as THREE.PerspectiveCamera).fov
52-
: 75
53-
})
54-
cameraConfig.state = cameraState
55-
this.nodeStorage.storeNodeProperty('Camera Config', cameraConfig)
42+
this.eventManager.emitEvent('cameraChanged', cameraState)
5643
})
5744
}
5845

0 commit comments

Comments
 (0)