diff --git a/src/components/load3d/Load3DAnimation.vue b/src/components/load3d/Load3DAnimation.vue
index e96ea8508..753226df3 100644
--- a/src/components/load3d/Load3DAnimation.vue
+++ b/src/components/load3d/Load3DAnimation.vue
@@ -63,8 +63,8 @@ import { computed, ref } from 'vue'
import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
import Load3DAnimationScene from '@/components/load3d/Load3DAnimationScene.vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
-import type { AnimationItem } from '@/extensions/core/load3d/Load3dAnimation'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
+import { AnimationItem } from '@/extensions/core/load3d/interfaces'
const props = defineProps<{
node: any
diff --git a/src/components/load3d/Load3DScene.vue b/src/components/load3d/Load3DScene.vue
index c45ad59bf..589494b4e 100644
--- a/src/components/load3d/Load3DScene.vue
+++ b/src/components/load3d/Load3DScene.vue
@@ -1,11 +1,14 @@
-
+
+
+
+
+
diff --git a/src/extensions/core/load3d.ts b/src/extensions/core/load3d.ts
index d9720db5f..25895ff6f 100644
--- a/src/extensions/core/load3d.ts
+++ b/src/extensions/core/load3d.ts
@@ -174,6 +174,8 @@ app.registerExtension({
Load3dUtils.uploadTempImage(maskData, 'scene_mask')
])
+ load3d.handleResize()
+
return {
image: `threed/${data.name} [temp]`,
mask: `threed/${dataMask.name} [temp]`
@@ -345,6 +347,8 @@ app.registerExtension({
Load3dUtils.uploadTempImage(maskData, 'scene_mask')
])
+ load3d.handleResize()
+
return {
image: `threed/${data.name} [temp]`,
mask: `threed/${dataMask.name} [temp]`
diff --git a/src/extensions/core/load3d/AnimationManager.ts b/src/extensions/core/load3d/AnimationManager.ts
new file mode 100644
index 000000000..f3efd220e
--- /dev/null
+++ b/src/extensions/core/load3d/AnimationManager.ts
@@ -0,0 +1,161 @@
+import * as THREE from 'three'
+
+import {
+ AnimationItem,
+ AnimationManagerInterface,
+ EventManagerInterface
+} from '@/extensions/core/load3d/interfaces'
+
+export class AnimationManager implements AnimationManagerInterface {
+ currentAnimation: THREE.AnimationMixer | null = null
+ animationActions: THREE.AnimationAction[] = []
+ animationClips: THREE.AnimationClip[] = []
+ selectedAnimationIndex: number = 0
+ isAnimationPlaying: boolean = false
+ animationSpeed: number = 1.0
+
+ private eventManager: EventManagerInterface
+ private getCurrentModel: () => THREE.Object3D | null
+
+ constructor(
+ eventManager: EventManagerInterface,
+ getCurrentModel: () => THREE.Object3D | null
+ ) {
+ this.eventManager = eventManager
+ this.getCurrentModel = getCurrentModel
+ }
+
+ init(): void {}
+
+ dispose(): void {
+ if (this.currentAnimation) {
+ this.animationActions.forEach((action) => {
+ action.stop()
+ })
+ this.currentAnimation = null
+ }
+ this.animationActions = []
+ this.animationClips = []
+ this.selectedAnimationIndex = 0
+ this.isAnimationPlaying = false
+ this.animationSpeed = 1.0
+
+ this.eventManager.emitEvent('animationListChange', [])
+ }
+
+ setupModelAnimations(model: THREE.Object3D, originalModel: any): void {
+ if (this.currentAnimation) {
+ this.currentAnimation.stopAllAction()
+ this.animationActions = []
+ }
+
+ let animations: THREE.AnimationClip[] = []
+ if (model.animations?.length > 0) {
+ animations = model.animations
+ } else if (originalModel && 'animations' in originalModel) {
+ animations = originalModel.animations
+ }
+
+ if (animations.length > 0) {
+ this.animationClips = animations
+ if (model.type === 'Scene') {
+ this.currentAnimation = new THREE.AnimationMixer(model)
+ } else {
+ this.currentAnimation = new THREE.AnimationMixer(
+ this.getCurrentModel()!
+ )
+ }
+
+ if (this.animationClips.length > 0) {
+ this.updateSelectedAnimation(0)
+ }
+ }
+
+ this.updateAnimationList()
+ }
+
+ updateAnimationList(): void {
+ let updatedAnimationList: AnimationItem[] = []
+
+ if (this.animationClips.length > 0) {
+ updatedAnimationList = this.animationClips.map((clip, index) => ({
+ name: clip.name || `Animation ${index + 1}`,
+ index
+ }))
+ }
+
+ this.eventManager.emitEvent('animationListChange', updatedAnimationList)
+ }
+
+ setAnimationSpeed(speed: number): void {
+ this.animationSpeed = speed
+ this.animationActions.forEach((action) => {
+ action.setEffectiveTimeScale(speed)
+ })
+ }
+
+ updateSelectedAnimation(index: number): void {
+ if (
+ !this.currentAnimation ||
+ !this.animationClips ||
+ index >= this.animationClips.length
+ ) {
+ console.warn('Invalid animation update request')
+ return
+ }
+
+ this.animationActions.forEach((action) => {
+ action.stop()
+ })
+ this.currentAnimation.stopAllAction()
+ this.animationActions = []
+
+ this.selectedAnimationIndex = index
+ const clip = this.animationClips[index]
+
+ const action = this.currentAnimation.clipAction(clip)
+
+ action.setEffectiveTimeScale(this.animationSpeed)
+
+ action.reset()
+ action.clampWhenFinished = false
+ action.loop = THREE.LoopRepeat
+
+ if (this.isAnimationPlaying) {
+ action.play()
+ } else {
+ action.play()
+ action.paused = true
+ }
+
+ this.animationActions = [action]
+ }
+
+ toggleAnimation(play?: boolean): void {
+ if (!this.currentAnimation || this.animationActions.length === 0) {
+ console.warn('No animation to toggle')
+ return
+ }
+
+ this.isAnimationPlaying = play ?? !this.isAnimationPlaying
+
+ this.animationActions.forEach((action) => {
+ if (this.isAnimationPlaying) {
+ action.paused = false
+ if (action.time === 0 || action.time === action.getClip().duration) {
+ action.reset()
+ }
+ } else {
+ action.paused = true
+ }
+ })
+ }
+
+ update(delta: number): void {
+ if (this.currentAnimation && this.isAnimationPlaying) {
+ this.currentAnimation.update(delta)
+ }
+ }
+
+ reset(): void {}
+}
diff --git a/src/extensions/core/load3d/CameraManager.ts b/src/extensions/core/load3d/CameraManager.ts
new file mode 100644
index 000000000..e876102c5
--- /dev/null
+++ b/src/extensions/core/load3d/CameraManager.ts
@@ -0,0 +1,234 @@
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+
+import {
+ CameraManagerInterface,
+ CameraState,
+ CameraType,
+ EventManagerInterface,
+ NodeStorageInterface
+} from './interfaces'
+
+export class CameraManager implements CameraManagerInterface {
+ perspectiveCamera: THREE.PerspectiveCamera
+ orthographicCamera: THREE.OrthographicCamera
+ activeCamera: THREE.Camera
+
+ private renderer: THREE.WebGLRenderer
+ private eventManager: EventManagerInterface
+ private nodeStorage: NodeStorageInterface
+
+ private controls: OrbitControls | null = null
+
+ DEFAULT_DISTANCE = 10
+ DEFAULT_LOOK_AT = 0
+
+ DEFAULT_CAMERA = {
+ near: 0.01,
+ far: 10000
+ }
+
+ DEFAULT_PERSPECTIVE_CAMERA = {
+ fov: 75,
+ aspect: 1
+ }
+
+ DEFAULT_FRUSTUM_SIZE = 10
+
+ DEFAULT_ORTHOGRAPHIC_CAMERA = {
+ left: -this.DEFAULT_FRUSTUM_SIZE / 2,
+ right: this.DEFAULT_FRUSTUM_SIZE / 2,
+ top: this.DEFAULT_FRUSTUM_SIZE / 2,
+ bottom: -this.DEFAULT_FRUSTUM_SIZE / 2
+ }
+
+ constructor(
+ renderer: THREE.WebGLRenderer,
+ eventManager: EventManagerInterface,
+ nodeStorage: NodeStorageInterface
+ ) {
+ this.renderer = renderer
+ this.eventManager = eventManager
+ this.nodeStorage = nodeStorage
+
+ this.perspectiveCamera = new THREE.PerspectiveCamera(
+ this.DEFAULT_PERSPECTIVE_CAMERA.fov,
+ this.DEFAULT_PERSPECTIVE_CAMERA.aspect,
+ this.DEFAULT_CAMERA.near,
+ this.DEFAULT_CAMERA.far
+ )
+
+ this.orthographicCamera = new THREE.OrthographicCamera(
+ this.DEFAULT_ORTHOGRAPHIC_CAMERA.left,
+ this.DEFAULT_ORTHOGRAPHIC_CAMERA.right,
+ this.DEFAULT_ORTHOGRAPHIC_CAMERA.top,
+ this.DEFAULT_ORTHOGRAPHIC_CAMERA.bottom,
+ this.DEFAULT_CAMERA.near,
+ this.DEFAULT_CAMERA.far
+ )
+
+ this.reset()
+
+ this.activeCamera = this.perspectiveCamera
+ }
+
+ init(): void {}
+
+ dispose(): void {}
+
+ setControls(controls: OrbitControls): void {
+ this.controls = controls
+
+ if (this.controls) {
+ this.controls.addEventListener('end', () => {
+ this.nodeStorage.storeNodeProperty('Camera Info', this.getCameraState())
+ })
+ }
+ }
+
+ getCurrentCameraType(): CameraType {
+ return this.activeCamera === this.perspectiveCamera
+ ? 'perspective'
+ : 'orthographic'
+ }
+
+ toggleCamera(cameraType?: CameraType): void {
+ const oldCamera = this.activeCamera
+
+ const position = oldCamera.position.clone()
+ const rotation = oldCamera.rotation.clone()
+ const target = this.controls?.target.clone() || new THREE.Vector3()
+
+ if (!cameraType) {
+ this.activeCamera =
+ oldCamera === this.perspectiveCamera
+ ? this.orthographicCamera
+ : this.perspectiveCamera
+ } else {
+ this.activeCamera =
+ cameraType === 'perspective'
+ ? this.perspectiveCamera
+ : this.orthographicCamera
+
+ if (oldCamera === this.activeCamera) {
+ return
+ }
+ }
+
+ this.activeCamera.position.copy(position)
+ this.activeCamera.rotation.copy(rotation)
+
+ if (this.controls) {
+ this.controls.object = this.activeCamera
+ this.controls.target.copy(target)
+ this.controls.update()
+ }
+
+ this.eventManager.emitEvent('cameraTypeChange', cameraType)
+ }
+
+ setFOV(fov: number): void {
+ if (this.activeCamera === this.perspectiveCamera) {
+ this.perspectiveCamera.fov = fov
+ this.perspectiveCamera.updateProjectionMatrix()
+ }
+
+ this.eventManager.emitEvent('fovChange', fov)
+ }
+
+ getCameraState(): CameraState {
+ return {
+ position: this.activeCamera.position.clone(),
+ target: this.controls?.target.clone() || new THREE.Vector3(),
+ zoom:
+ this.activeCamera instanceof THREE.OrthographicCamera
+ ? this.activeCamera.zoom
+ : (this.activeCamera as THREE.PerspectiveCamera).zoom,
+ cameraType: this.getCurrentCameraType()
+ }
+ }
+
+ setCameraState(state: CameraState): void {
+ this.activeCamera.position.copy(state.position)
+
+ this.controls?.target.copy(state.target)
+
+ if (this.activeCamera instanceof THREE.OrthographicCamera) {
+ this.activeCamera.zoom = state.zoom
+ this.activeCamera.updateProjectionMatrix()
+ } else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
+ this.activeCamera.zoom = state.zoom
+ this.activeCamera.updateProjectionMatrix()
+ }
+
+ this.controls?.update()
+ }
+
+ handleResize(width: number, height: number): void {
+ if (this.activeCamera === this.perspectiveCamera) {
+ this.perspectiveCamera.aspect = width / height
+ this.perspectiveCamera.updateProjectionMatrix()
+ } else {
+ const frustumSize = 10
+ const aspect = width / height
+ this.orthographicCamera.left = (-frustumSize * aspect) / 2
+ this.orthographicCamera.right = (frustumSize * aspect) / 2
+ this.orthographicCamera.top = frustumSize / 2
+ this.orthographicCamera.bottom = -frustumSize / 2
+ this.orthographicCamera.updateProjectionMatrix()
+ }
+ }
+
+ setupForModel(size: THREE.Vector3): void {
+ const distance = Math.max(size.x, size.z) * 2
+ const height = size.y * 2
+
+ this.perspectiveCamera.position.set(distance, height, distance)
+ this.orthographicCamera.position.set(distance, height, distance)
+
+ if (this.activeCamera === this.perspectiveCamera) {
+ this.perspectiveCamera.lookAt(0, size.y / 2, 0)
+ this.perspectiveCamera.updateProjectionMatrix()
+ } else {
+ const frustumSize = Math.max(size.x, size.y, size.z) * 2
+ const aspect = this.perspectiveCamera.aspect
+ this.orthographicCamera.left = (-frustumSize * aspect) / 2
+ this.orthographicCamera.right = (frustumSize * aspect) / 2
+ this.orthographicCamera.top = frustumSize / 2
+ this.orthographicCamera.bottom = -frustumSize / 2
+ this.orthographicCamera.lookAt(0, size.y / 2, 0)
+ this.orthographicCamera.updateProjectionMatrix()
+ }
+
+ this.controls?.target.set(0, size.y / 2, 0)
+ this.controls?.update()
+ }
+
+ reset(): void {
+ this.perspectiveCamera.position.set(
+ this.DEFAULT_DISTANCE,
+ this.DEFAULT_DISTANCE,
+ this.DEFAULT_DISTANCE
+ )
+
+ this.orthographicCamera.position.set(
+ this.DEFAULT_DISTANCE,
+ this.DEFAULT_DISTANCE,
+ this.DEFAULT_DISTANCE
+ )
+
+ this.perspectiveCamera.lookAt(
+ this.DEFAULT_LOOK_AT,
+ this.DEFAULT_LOOK_AT,
+ this.DEFAULT_LOOK_AT
+ )
+ this.orthographicCamera.lookAt(
+ this.DEFAULT_LOOK_AT,
+ this.DEFAULT_LOOK_AT,
+ this.DEFAULT_LOOK_AT
+ )
+
+ this.perspectiveCamera.updateProjectionMatrix()
+ this.orthographicCamera.updateProjectionMatrix()
+ }
+}
diff --git a/src/extensions/core/load3d/ControlsManager.ts b/src/extensions/core/load3d/ControlsManager.ts
new file mode 100644
index 000000000..79c5d1167
--- /dev/null
+++ b/src/extensions/core/load3d/ControlsManager.ts
@@ -0,0 +1,72 @@
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+
+import {
+ ControlsManagerInterface,
+ EventManagerInterface,
+ NodeStorageInterface
+} from './interfaces'
+
+export class ControlsManager implements ControlsManagerInterface {
+ controls: OrbitControls
+
+ private eventManager: EventManagerInterface
+ private nodeStorage: NodeStorageInterface
+ private camera: THREE.Camera
+
+ constructor(
+ renderer: THREE.WebGLRenderer,
+ camera: THREE.Camera,
+ eventManager: EventManagerInterface,
+ nodeStorage: NodeStorageInterface
+ ) {
+ this.eventManager = eventManager
+ this.nodeStorage = nodeStorage
+ this.camera = camera
+
+ this.controls = new OrbitControls(camera, renderer.domElement)
+ this.controls.enableDamping = true
+ }
+
+ init(): void {
+ this.controls.addEventListener('end', () => {
+ this.nodeStorage.storeNodeProperty('Camera Info', {
+ position: this.camera.position.clone(),
+ target: this.controls.target.clone(),
+ zoom:
+ this.camera instanceof THREE.OrthographicCamera
+ ? (this.camera as THREE.OrthographicCamera).zoom
+ : (this.camera as THREE.PerspectiveCamera).zoom,
+ cameraType:
+ this.camera instanceof THREE.PerspectiveCamera
+ ? 'perspective'
+ : 'orthographic'
+ })
+ })
+ }
+
+ dispose(): void {
+ this.controls.dispose()
+ }
+
+ handleResize(): void {}
+
+ update(): void {
+ this.controls.update()
+ }
+
+ updateCamera(camera: THREE.Camera): void {
+ const position = this.controls.object.position.clone()
+ const target = this.controls.target.clone()
+
+ this.camera = camera
+ this.controls.object = camera
+ this.controls.target = target
+ camera.position.copy(position)
+ this.controls.update()
+ }
+ reset(): void {
+ this.controls.target.set(0, 0, 0)
+ this.controls.update()
+ }
+}
diff --git a/src/extensions/core/load3d/EventManager.ts b/src/extensions/core/load3d/EventManager.ts
new file mode 100644
index 000000000..e06669ce2
--- /dev/null
+++ b/src/extensions/core/load3d/EventManager.ts
@@ -0,0 +1,26 @@
+import { EventCallback, EventManagerInterface } from './interfaces'
+
+export class EventManager implements EventManagerInterface {
+ private listeners: { [key: string]: EventCallback[] } = {}
+
+ addEventListener(event: string, callback: EventCallback): void {
+ if (!this.listeners[event]) {
+ this.listeners[event] = []
+ }
+ this.listeners[event].push(callback)
+ }
+
+ removeEventListener(event: string, callback: EventCallback): void {
+ if (this.listeners[event]) {
+ this.listeners[event] = this.listeners[event].filter(
+ (cb) => cb !== callback
+ )
+ }
+ }
+
+ emitEvent(event: string, data?: any): void {
+ if (this.listeners[event]) {
+ this.listeners[event].forEach((callback) => callback(data))
+ }
+ }
+}
diff --git a/src/extensions/core/load3d/LightingManager.ts b/src/extensions/core/load3d/LightingManager.ts
new file mode 100644
index 000000000..20212dbdc
--- /dev/null
+++ b/src/extensions/core/load3d/LightingManager.ts
@@ -0,0 +1,78 @@
+import * as THREE from 'three'
+
+import { EventManagerInterface, LightingManagerInterface } from './interfaces'
+
+export class LightingManager implements LightingManagerInterface {
+ lights: THREE.Light[] = []
+ private scene: THREE.Scene
+ private eventManager: EventManagerInterface
+
+ constructor(scene: THREE.Scene, eventManager: EventManagerInterface) {
+ this.scene = scene
+ this.eventManager = eventManager
+ }
+
+ init(): void {
+ this.setupLights()
+ }
+
+ dispose(): void {
+ this.lights.forEach((light) => {
+ this.scene.remove(light)
+ })
+ this.lights = []
+ }
+
+ setupLights(): void {
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
+ this.scene.add(ambientLight)
+ this.lights.push(ambientLight)
+
+ const mainLight = new THREE.DirectionalLight(0xffffff, 0.8)
+ mainLight.position.set(0, 10, 10)
+ this.scene.add(mainLight)
+ this.lights.push(mainLight)
+
+ const backLight = new THREE.DirectionalLight(0xffffff, 0.5)
+ backLight.position.set(0, 10, -10)
+ this.scene.add(backLight)
+ this.lights.push(backLight)
+
+ const leftFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
+ leftFillLight.position.set(-10, 0, 0)
+ this.scene.add(leftFillLight)
+ this.lights.push(leftFillLight)
+
+ const rightFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
+ rightFillLight.position.set(10, 0, 0)
+ this.scene.add(rightFillLight)
+ this.lights.push(rightFillLight)
+
+ const bottomLight = new THREE.DirectionalLight(0xffffff, 0.2)
+ bottomLight.position.set(0, -10, 0)
+ this.scene.add(bottomLight)
+ this.lights.push(bottomLight)
+ }
+
+ setLightIntensity(intensity: number): void {
+ this.lights.forEach((light) => {
+ if (light instanceof THREE.DirectionalLight) {
+ if (light === this.lights[1]) {
+ light.intensity = intensity * 0.8
+ } else if (light === this.lights[2]) {
+ light.intensity = intensity * 0.5
+ } else if (light === this.lights[5]) {
+ light.intensity = intensity * 0.2
+ } else {
+ light.intensity = intensity * 0.3
+ }
+ } else if (light instanceof THREE.AmbientLight) {
+ light.intensity = intensity * 0.5
+ }
+ })
+
+ this.eventManager.emitEvent('lightIntensityChange', intensity)
+ }
+
+ reset(): void {}
+}
diff --git a/src/extensions/core/load3d/Load3d.ts b/src/extensions/core/load3d/Load3d.ts
index f27e65dbc..112479e7c 100644
--- a/src/extensions/core/load3d/Load3d.ts
+++ b/src/extensions/core/load3d/Load3d.ts
@@ -1,71 +1,40 @@
import { LGraphNode } from '@comfyorg/litegraph'
import * as THREE from 'three'
-import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
-import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
-import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
-import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
-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 Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
-import { useToastStore } from '@/stores/toastStore'
-
-interface Load3DOptions {
- createPreview?: boolean
- node?: LGraphNode
-}
+import { CameraManager } from './CameraManager'
+import { ControlsManager } from './ControlsManager'
+import { EventManager } from './EventManager'
+import { LightingManager } from './LightingManager'
+import { LoaderManager } from './LoaderManager'
+import { ModelManager } from './ModelManager'
+import { NodeStorage } from './NodeStorage'
+import { PreviewManager } from './PreviewManager'
+import { SceneManager } from './SceneManager'
+import { ViewHelperManager } from './ViewHelperManager'
+import {
+ CameraState,
+ CaptureResult,
+ Load3DOptions,
+ MaterialMode,
+ UpDirection
+} from './interfaces'
class Load3d {
- scene: THREE.Scene
- perspectiveCamera: THREE.PerspectiveCamera
- orthographicCamera: THREE.OrthographicCamera
- activeCamera: THREE.Camera
renderer: THREE.WebGLRenderer
- controls: OrbitControls
- gltfLoader: GLTFLoader
- objLoader: OBJLoader
- mtlLoader: MTLLoader
- fbxLoader: FBXLoader
- stlLoader: STLLoader
- currentModel: THREE.Object3D | null = null
- originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
- animationFrameId: number | null = null
- gridHelper: THREE.GridHelper
- lights: THREE.Light[] = []
- clock: THREE.Clock
- normalMaterial: THREE.MeshNormalMaterial
- standardMaterial: THREE.MeshStandardMaterial
- wireframeMaterial: THREE.MeshBasicMaterial
- depthMaterial: THREE.MeshDepthMaterial
- originalMaterials: WeakMap =
- new WeakMap()
+ protected clock: THREE.Clock
+ protected animationFrameId: number | null = null
+ protected node: LGraphNode
- materialMode: 'original' | 'normal' | 'wireframe' | 'depth' = 'original'
- currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' =
- 'original'
- originalRotation: THREE.Euler | null = null
- viewHelper: ViewHelper = {} as ViewHelper
- viewHelperContainer: HTMLDivElement = {} as HTMLDivElement
- previewRenderer: THREE.WebGLRenderer | null = null
- previewCamera: THREE.Camera | null = null
- previewContainer: HTMLDivElement = {} as HTMLDivElement
- targetWidth: number = 1024
- targetHeight: number = 1024
- showPreview: boolean = true
- previewWidth: number = 120
- node: LGraphNode = {} as LGraphNode
- listeners: { [key: string]: Function[] } = {}
-
- backgroundScene: THREE.Scene
- backgroundCamera: THREE.OrthographicCamera
- backgroundMesh: THREE.Mesh | null = null
- backgroundTexture: THREE.Texture | null = null
-
- previewBackgroundScene: THREE.Scene
- previewBackgroundCamera: THREE.OrthographicCamera
- previewBackgroundMesh: THREE.Mesh | null = null
- previewBackgroundTexture: THREE.Texture | null = null
+ protected eventManager: EventManager
+ protected nodeStorage: NodeStorage
+ protected sceneManager: SceneManager
+ protected cameraManager: CameraManager
+ protected controlsManager: ControlsManager
+ protected lightingManager: LightingManager
+ protected viewHelperManager: ViewHelperManager
+ protected previewManager: PreviewManager
+ protected loaderManager: LoaderManager
+ protected modelManager: ModelManager
constructor(
container: Element | HTMLElement,
@@ -74,1115 +43,248 @@ class Load3d {
}
) {
this.node = options.node || ({} as LGraphNode)
- this.scene = new THREE.Scene()
-
- this.perspectiveCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
- this.perspectiveCamera.position.set(5, 5, 5)
-
- const frustumSize = 10
- this.orthographicCamera = new THREE.OrthographicCamera(
- -frustumSize / 2,
- frustumSize / 2,
- frustumSize / 2,
- -frustumSize / 2,
- 0.01,
- 10000
- )
- this.orthographicCamera.position.set(5, 5, 5)
-
- this.activeCamera = this.perspectiveCamera
-
- this.perspectiveCamera.lookAt(0, 0, 0)
- this.orthographicCamera.lookAt(0, 0, 0)
+ this.clock = new THREE.Clock()
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
this.renderer.setSize(300, 300)
this.renderer.setClearColor(0x282828)
this.renderer.autoClear = false
-
this.renderer.outputColorSpace = THREE.SRGBColorSpace
+ container.appendChild(this.renderer.domElement)
- const rendererDomElement: HTMLCanvasElement = this.renderer.domElement
+ this.eventManager = new EventManager()
+ this.nodeStorage = new NodeStorage(this.node)
- container.appendChild(rendererDomElement)
-
- this.controls = new OrbitControls(
- this.activeCamera,
- this.renderer.domElement
+ this.sceneManager = new SceneManager(
+ this.renderer,
+ this.getActiveCamera.bind(this),
+ this.getControls.bind(this),
+ this.eventManager
)
- this.controls.enableDamping = true
- this.controls.addEventListener('end', () => {
- this.storeNodeProperty('Camera Info', this.getCameraState())
- })
+ this.cameraManager = new CameraManager(
+ this.renderer,
+ this.eventManager,
+ this.nodeStorage
+ )
- this.gltfLoader = new GLTFLoader()
- this.objLoader = new OBJLoader()
- this.mtlLoader = new MTLLoader()
- this.fbxLoader = new FBXLoader()
- this.stlLoader = new STLLoader()
- this.clock = new THREE.Clock()
+ this.controlsManager = new ControlsManager(
+ this.renderer,
+ this.cameraManager.activeCamera,
+ this.eventManager,
+ this.nodeStorage
+ )
- this.setupLights()
+ this.cameraManager.setControls(this.controlsManager.controls)
- this.gridHelper = new THREE.GridHelper(10, 10)
- this.gridHelper.position.set(0, 0, 0)
- this.scene.add(this.gridHelper)
+ this.lightingManager = new LightingManager(
+ this.sceneManager.scene,
+ this.eventManager
+ )
- this.normalMaterial = new THREE.MeshNormalMaterial({
- flatShading: false,
- side: THREE.DoubleSide,
- normalScale: new THREE.Vector2(1, 1),
- transparent: false,
- opacity: 1.0
- })
+ this.viewHelperManager = new ViewHelperManager(
+ this.renderer,
+ this.getActiveCamera.bind(this),
+ this.getControls.bind(this),
+ this.nodeStorage
+ )
- this.wireframeMaterial = new THREE.MeshBasicMaterial({
- color: 0xffffff,
- wireframe: true,
- transparent: false,
- opacity: 1.0
- })
+ this.previewManager = new PreviewManager(
+ this.sceneManager.scene,
+ this.getActiveCamera.bind(this),
+ this.getControls.bind(this),
+ () => this.renderer,
+ this.eventManager,
+ this.sceneManager.backgroundScene,
+ this.sceneManager.backgroundCamera
+ )
- this.depthMaterial = new THREE.MeshDepthMaterial({
- depthPacking: THREE.BasicDepthPacking,
- side: THREE.DoubleSide
- })
+ this.modelManager = new ModelManager(
+ this.sceneManager.scene,
+ this.renderer,
+ this.eventManager,
+ this.getActiveCamera.bind(this),
+ this.setupCamera.bind(this)
+ )
- this.standardMaterial = this.createSTLMaterial()
+ this.loaderManager = new LoaderManager(this.modelManager, this.eventManager)
- this.createViewHelper(container)
+ this.sceneManager.init()
+ this.cameraManager.init()
+ this.controlsManager.init()
+ this.lightingManager.init()
+ this.loaderManager.init()
+ this.loaderManager.init()
+
+ this.viewHelperManager.createViewHelper(container)
+ this.viewHelperManager.init()
if (options && options.createPreview) {
- this.createCapturePreview(container)
+ this.previewManager.createCapturePreview(container)
+ this.previewManager.init()
}
- this.backgroundScene = new THREE.Scene()
- this.backgroundCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1)
- this.previewBackgroundScene = this.backgroundScene.clone()
- this.previewBackgroundCamera = this.backgroundCamera.clone()
-
- const planeGeometry = new THREE.PlaneGeometry(2, 2)
- const planeMaterial = new THREE.MeshBasicMaterial({
- transparent: true,
- depthWrite: false,
- depthTest: false,
- side: THREE.DoubleSide
- })
-
- this.backgroundMesh = new THREE.Mesh(planeGeometry, planeMaterial)
- this.backgroundMesh.position.set(0, 0, 0)
-
- this.previewBackgroundMesh = this.backgroundMesh.clone()
-
- this.backgroundScene.add(this.backgroundMesh)
- this.previewBackgroundScene.add(this.previewBackgroundMesh)
-
this.handleResize()
-
this.startAnimation()
}
- updateBackgroundSize(
- backgroundTexture: THREE.Texture | null,
- backgroundMesh: THREE.Mesh | null,
- targetWidth: number,
- targetHeight: number
- ) {
- if (!backgroundTexture || !backgroundMesh) return
-
- const material = backgroundMesh.material as THREE.MeshBasicMaterial
-
- if (!material.map) return
-
- const imageAspect =
- backgroundTexture.image.width / backgroundTexture.image.height
- const targetAspect = targetWidth / targetHeight
-
- if (imageAspect > targetAspect) {
- backgroundMesh.scale.set(imageAspect / targetAspect, 1, 1)
- } else {
- backgroundMesh.scale.set(1, targetAspect / imageAspect, 1)
- }
-
- material.needsUpdate = true
+ private getActiveCamera(): THREE.Camera {
+ return this.cameraManager.activeCamera
}
- async setBackgroundImage(uploadPath: string) {
- if (uploadPath === '') {
- this.removeBackgroundImage()
- return
- }
-
- let imageUrl = Load3dUtils.getResourceURL(
- ...Load3dUtils.splitFilePath(uploadPath)
- )
-
- if (!imageUrl.startsWith('/api')) {
- imageUrl = '/api' + imageUrl
- }
-
- try {
- const textureLoader = new THREE.TextureLoader()
- const texture = await new Promise((resolve, reject) => {
- textureLoader.load(imageUrl, resolve, undefined, reject)
- })
-
- if (this.backgroundTexture) {
- this.backgroundTexture.dispose()
- }
-
- if (this.previewBackgroundTexture) {
- this.previewBackgroundTexture.dispose()
- }
-
- texture.colorSpace = THREE.SRGBColorSpace
-
- this.backgroundTexture = texture
- this.previewBackgroundTexture = texture
-
- const material = this.backgroundMesh?.material as THREE.MeshBasicMaterial
- material.map = texture
- material.needsUpdate = true
-
- const material2 = this.previewBackgroundMesh
- ?.material as THREE.MeshBasicMaterial
- material2.map = texture
- material2.needsUpdate = true
-
- this.backgroundMesh?.position.set(0, 0, 0)
- this.previewBackgroundMesh?.position.set(0, 0, 0)
-
- this.updateBackgroundSize(
- this.previewBackgroundTexture,
- this.previewBackgroundMesh,
- this.targetWidth,
- this.targetHeight
- )
-
- this.emitEvent('backgroundImageChange', uploadPath)
- } catch (error) {
- console.error('Error loading background image:', error)
- }
+ private getControls() {
+ return this.controlsManager.controls
}
- removeBackgroundImage() {
- if (this.backgroundMesh) {
- const material = this.backgroundMesh.material as THREE.MeshBasicMaterial
- material.map = null
- material.needsUpdate = true
- }
-
- if (this.previewBackgroundMesh) {
- const material2 = this.previewBackgroundMesh
- .material as THREE.MeshBasicMaterial
- material2.map = null
- material2.needsUpdate = true
- }
-
- if (this.backgroundTexture) {
- this.backgroundTexture.dispose()
- this.backgroundTexture = null
- }
-
- if (this.previewBackgroundTexture) {
- this.previewBackgroundTexture.dispose()
- this.previewBackgroundTexture = null
- }
-
- this.renderer.render(this.scene, this.activeCamera)
- if (this.previewRenderer && this.previewCamera) {
- this.previewRenderer.render(this.scene, this.previewCamera)
- }
+ private setupCamera(size: THREE.Vector3): void {
+ this.cameraManager.setupForModel(size)
}
- addEventListener(event: string, callback: Function) {
- if (!this.listeners[event]) {
- this.listeners[event] = []
- }
- this.listeners[event].push(callback)
- }
-
- removeEventListener(event: string, callback: Function) {
- if (this.listeners[event]) {
- this.listeners[event] = this.listeners[event].filter(
- (cb) => cb !== callback
- )
- }
- }
-
- emitEvent(event: string, data?: any) {
- if (this.listeners[event]) {
- this.listeners[event].forEach((callback) => callback(data))
- }
- }
-
- storeNodeProperty(name: string, value: any) {
- if (this.node) {
- this.node.properties[name] = value
- }
- }
-
- loadNodeProperty(name: string, defaultValue: any) {
- if (
- !this.node ||
- !this.node.properties ||
- !(name in this.node.properties)
- ) {
- return defaultValue
- }
- return this.node.properties[name]
- }
-
- createCapturePreview(container: Element | HTMLElement) {
- this.previewRenderer = new THREE.WebGLRenderer({
- alpha: true,
- antialias: true,
- preserveDrawingBuffer: true
- })
- this.previewRenderer.setSize(this.targetWidth, this.targetHeight)
- this.previewRenderer.setClearColor(0x282828)
- this.previewRenderer.autoClear = false
-
- this.previewRenderer.outputColorSpace = THREE.SRGBColorSpace
-
- this.previewContainer = document.createElement('div')
- this.previewContainer.style.cssText = `
- position: absolute;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.2);
- display: block;
- transition: border-color 0.1s ease;
- `
- this.previewContainer.appendChild(this.previewRenderer.domElement)
-
- const MIN_PREVIEW_WIDTH = 120
- const MAX_PREVIEW_WIDTH = 240
-
- this.previewContainer.addEventListener('wheel', (event) => {
- event.preventDefault()
- event.stopPropagation()
-
- const delta = event.deltaY
- const oldWidth = this.previewWidth
-
- if (delta > 0) {
- this.previewWidth = Math.max(MIN_PREVIEW_WIDTH, this.previewWidth - 10)
- } else {
- this.previewWidth = Math.min(MAX_PREVIEW_WIDTH, this.previewWidth + 10)
- }
-
- if (
- oldWidth !== this.previewWidth &&
- (this.previewWidth === MIN_PREVIEW_WIDTH ||
- this.previewWidth === MAX_PREVIEW_WIDTH)
- ) {
- this.flashPreviewBorder()
- }
-
- this.updatePreviewSize()
- this.updatePreviewRender()
- })
-
- this.previewContainer.style.display = this.showPreview ? 'block' : 'none'
-
- container.appendChild(this.previewContainer)
- }
-
- flashPreviewBorder() {
- const originalBorder = this.previewContainer.style.border
- const originalBoxShadow = this.previewContainer.style.boxShadow
-
- this.previewContainer.style.border = '2px solid rgba(255, 255, 255, 0.8)'
- this.previewContainer.style.boxShadow = '0 0 8px rgba(255, 255, 255, 0.5)'
-
- setTimeout(() => {
- this.previewContainer.style.border = originalBorder
- this.previewContainer.style.boxShadow = originalBoxShadow
- }, 100)
- }
-
- updatePreviewRender() {
- if (!this.previewRenderer || !this.previewContainer || !this.showPreview)
- return
-
- if (
- !this.previewCamera ||
- (this.activeCamera instanceof THREE.PerspectiveCamera &&
- !(this.previewCamera instanceof THREE.PerspectiveCamera)) ||
- (this.activeCamera instanceof THREE.OrthographicCamera &&
- !(this.previewCamera instanceof THREE.OrthographicCamera))
- ) {
- this.previewCamera = this.activeCamera.clone()
- }
-
- this.previewCamera.position.copy(this.activeCamera.position)
- this.previewCamera.rotation.copy(this.activeCamera.rotation)
-
- const aspect = this.targetWidth / this.targetHeight
-
- if (this.activeCamera instanceof THREE.OrthographicCamera) {
- const activeOrtho = this.activeCamera as THREE.OrthographicCamera
- const previewOrtho = this.previewCamera as THREE.OrthographicCamera
-
- const frustumHeight =
- (activeOrtho.top - activeOrtho.bottom) / activeOrtho.zoom
-
- const frustumWidth = frustumHeight * aspect
-
- previewOrtho.top = frustumHeight / 2
- previewOrtho.left = -frustumWidth / 2
- previewOrtho.right = frustumWidth / 2
- previewOrtho.bottom = -frustumHeight / 2
- previewOrtho.zoom = 1
-
- previewOrtho.updateProjectionMatrix()
- } else {
- ;(this.previewCamera as THREE.PerspectiveCamera).aspect = aspect
- ;(this.previewCamera as THREE.PerspectiveCamera).fov = (
- this.activeCamera as THREE.PerspectiveCamera
- ).fov
- ;(this.previewCamera as THREE.PerspectiveCamera).updateProjectionMatrix()
- }
-
- this.previewCamera.lookAt(this.controls.target)
-
- const previewHeight =
- (this.previewWidth * this.targetHeight) / this.targetWidth
- this.previewRenderer.setSize(this.previewWidth, previewHeight, false)
-
- this.previewRenderer.outputColorSpace = THREE.SRGBColorSpace
-
- this.previewRenderer.clear()
-
- if (this.previewBackgroundMesh && this.previewBackgroundTexture) {
- const material = this.previewBackgroundMesh
- .material as THREE.MeshBasicMaterial
- if (material.map) {
- const currentToneMapping = this.previewRenderer.toneMapping
- const currentExposure = this.previewRenderer.toneMappingExposure
-
- this.previewRenderer.toneMapping = THREE.NoToneMapping
-
- this.previewRenderer.render(
- this.previewBackgroundScene,
- this.previewBackgroundCamera
- )
-
- this.previewRenderer.toneMapping = currentToneMapping
- this.previewRenderer.toneMappingExposure = currentExposure
- }
- }
-
- this.previewRenderer.render(this.scene, this.previewCamera)
- }
-
- updatePreviewSize() {
- if (!this.previewContainer) return
-
- const previewHeight =
- (this.previewWidth * this.targetHeight) / this.targetWidth
-
- this.previewRenderer?.setSize(this.previewWidth, previewHeight, false)
- }
-
- setTargetSize(width: number, height: number) {
- const oldAspect = this.targetWidth / this.targetHeight
-
- this.targetWidth = width
- this.targetHeight = height
-
- this.updatePreviewSize()
-
- const newAspect = width / height
- if (Math.abs(oldAspect - newAspect) > 0.001) {
- this.updateBackgroundSize(
- this.previewBackgroundTexture,
- this.previewBackgroundMesh,
- width,
- height
- )
- }
-
- if (this.previewRenderer && this.previewCamera) {
- if (this.previewCamera instanceof THREE.PerspectiveCamera) {
- this.previewCamera.aspect = width / height
- this.previewCamera.updateProjectionMatrix()
- } else if (this.previewCamera instanceof THREE.OrthographicCamera) {
- const frustumSize = 10
- const aspect = width / height
- this.previewCamera.left = (-frustumSize * aspect) / 2
- this.previewCamera.right = (frustumSize * aspect) / 2
- this.previewCamera.updateProjectionMatrix()
- }
- }
- }
-
- createViewHelper(container: Element | HTMLElement) {
- 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.activeCamera,
- this.viewHelperContainer
- )
-
- this.viewHelper.center = this.controls.target
- }
-
- setFOV(fov: number) {
- if (this.activeCamera === this.perspectiveCamera) {
- this.perspectiveCamera.fov = fov
- this.perspectiveCamera.updateProjectionMatrix()
- this.renderer.render(this.scene, this.activeCamera)
- }
-
- if (
- this.previewRenderer &&
- this.previewCamera instanceof THREE.PerspectiveCamera
- ) {
- this.previewCamera.fov = fov
- this.previewCamera.updateProjectionMatrix()
- this.previewRenderer.render(this.scene, this.previewCamera)
- }
-
- this.emitEvent('fovChange', fov)
- }
-
- getCameraState() {
- const currentType = this.getCurrentCameraType()
- return {
- position: this.activeCamera.position.clone(),
- target: this.controls.target.clone(),
- zoom:
- this.activeCamera instanceof THREE.OrthographicCamera
- ? this.activeCamera.zoom
- : (this.activeCamera as THREE.PerspectiveCamera).zoom,
- cameraType: currentType
- }
- }
-
- setCameraState(state: {
- position: THREE.Vector3
- target: THREE.Vector3
- zoom: number
- cameraType: 'perspective' | 'orthographic'
- }) {
- this.activeCamera.position.copy(state.position)
-
- this.controls.target.copy(state.target)
-
- if (this.activeCamera instanceof THREE.OrthographicCamera) {
- this.activeCamera.zoom = state.zoom
- this.activeCamera.updateProjectionMatrix()
- } else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
- this.activeCamera.zoom = state.zoom
- this.activeCamera.updateProjectionMatrix()
- }
-
- this.controls.update()
- }
-
- setUpDirection(
- direction: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
- ) {
- if (!this.currentModel) return
-
- if (!this.originalRotation && this.currentModel.rotation) {
- this.originalRotation = this.currentModel.rotation.clone()
- }
-
- this.currentUpDirection = direction
-
- if (this.originalRotation) {
- this.currentModel.rotation.copy(this.originalRotation)
- }
-
- switch (direction) {
- case 'original':
- break
- case '-x':
- this.currentModel.rotation.z = Math.PI / 2
- break
- case '+x':
- this.currentModel.rotation.z = -Math.PI / 2
- break
- case '-y':
- this.currentModel.rotation.x = Math.PI
- break
- case '+y':
- break
- case '-z':
- this.currentModel.rotation.x = Math.PI / 2
- break
- case '+z':
- this.currentModel.rotation.x = -Math.PI / 2
- break
- }
-
- this.renderer.render(this.scene, this.activeCamera)
- }
-
- setMaterialMode(mode: 'original' | 'normal' | 'wireframe' | 'depth') {
- this.materialMode = mode
-
- if (this.currentModel) {
- if (mode === 'depth') {
- this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
- } else {
- this.renderer.outputColorSpace = THREE.SRGBColorSpace
- }
-
- this.currentModel.traverse((child) => {
- if (child instanceof THREE.Mesh) {
- switch (mode) {
- case 'depth':
- if (!this.originalMaterials.has(child)) {
- this.originalMaterials.set(child, child.material)
- }
- const depthMat = new THREE.MeshDepthMaterial({
- depthPacking: THREE.BasicDepthPacking,
- side: THREE.DoubleSide
- })
-
- depthMat.onBeforeCompile = (shader) => {
- shader.uniforms.cameraType = {
- value:
- this.activeCamera instanceof THREE.OrthographicCamera
- ? 1.0
- : 0.0
- }
-
- shader.fragmentShader = `
- uniform float cameraType;
- ${shader.fragmentShader}
- `
-
- shader.fragmentShader = shader.fragmentShader.replace(
- /gl_FragColor\s*=\s*vec4\(\s*vec3\(\s*1.0\s*-\s*fragCoordZ\s*\)\s*,\s*opacity\s*\)\s*;/,
- `
- float depth = 1.0 - fragCoordZ;
- if (cameraType > 0.5) {
- depth = pow(depth, 400.0);
- } else {
- depth = pow(depth, 0.6);
- }
- gl_FragColor = vec4(vec3(depth), opacity);
- `
- )
- }
-
- depthMat.customProgramCacheKey = () => {
- return this.activeCamera instanceof THREE.OrthographicCamera
- ? 'ortho'
- : 'persp'
- }
-
- child.material = depthMat
- break
- case 'normal':
- if (!this.originalMaterials.has(child)) {
- this.originalMaterials.set(child, child.material)
- }
- child.material = new THREE.MeshNormalMaterial({
- flatShading: false,
- side: THREE.DoubleSide,
- normalScale: new THREE.Vector2(1, 1),
- transparent: false,
- opacity: 1.0
- })
- child.geometry.computeVertexNormals()
- break
-
- case 'wireframe':
- if (!this.originalMaterials.has(child)) {
- this.originalMaterials.set(child, child.material)
- }
- child.material = new THREE.MeshBasicMaterial({
- color: 0xffffff,
- wireframe: true,
- transparent: false,
- opacity: 1.0
- })
- break
-
- case 'original':
- const originalMaterial = this.originalMaterials.get(child)
- if (originalMaterial) {
- child.material = originalMaterial
- } else {
- child.material = this.standardMaterial
- }
- break
- }
- }
- })
-
- this.renderer.render(this.scene, this.activeCamera)
-
- this.emitEvent('materialModeChange', mode)
- }
- }
-
- setupLights() {
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
- this.scene.add(ambientLight)
- this.lights.push(ambientLight)
-
- const mainLight = new THREE.DirectionalLight(0xffffff, 0.8)
- mainLight.position.set(0, 10, 10)
- this.scene.add(mainLight)
- this.lights.push(mainLight)
-
- const backLight = new THREE.DirectionalLight(0xffffff, 0.5)
- backLight.position.set(0, 10, -10)
- this.scene.add(backLight)
- this.lights.push(backLight)
-
- const leftFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
- leftFillLight.position.set(-10, 0, 0)
- this.scene.add(leftFillLight)
- this.lights.push(leftFillLight)
-
- const rightFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
- rightFillLight.position.set(10, 0, 0)
- this.scene.add(rightFillLight)
- this.lights.push(rightFillLight)
-
- const bottomLight = new THREE.DirectionalLight(0xffffff, 0.2)
- bottomLight.position.set(0, -10, 0)
- this.scene.add(bottomLight)
- this.lights.push(bottomLight)
- }
-
- toggleCamera(cameraType?: 'perspective' | 'orthographic') {
- const oldCamera = this.activeCamera
-
- const position = oldCamera.position.clone()
- const rotation = oldCamera.rotation.clone()
- const target = this.controls.target.clone()
-
- if (!cameraType) {
- this.activeCamera =
- oldCamera === this.perspectiveCamera
- ? this.orthographicCamera
- : this.perspectiveCamera
- } else {
- this.activeCamera =
- cameraType === 'perspective'
- ? this.perspectiveCamera
- : this.orthographicCamera
-
- if (oldCamera === this.activeCamera) {
- return
- }
- }
-
- if (this.previewCamera) {
- this.previewCamera = null
- }
- this.previewCamera = this.activeCamera.clone()
-
- this.activeCamera.position.copy(position)
- this.activeCamera.rotation.copy(rotation)
-
- if (this.materialMode === 'depth' && oldCamera !== this.activeCamera) {
- this.setMaterialMode('depth')
- }
-
- this.controls.object = this.activeCamera
- this.controls.target.copy(target)
- this.controls.update()
-
- this.viewHelper.dispose()
- this.viewHelper = new ViewHelper(
- this.activeCamera,
- this.viewHelperContainer
- )
- this.viewHelper.center = this.controls.target
-
- this.handleResize()
- this.updatePreviewRender()
-
- this.emitEvent('cameraTypeChange', cameraType)
- }
-
- getCurrentCameraType(): 'perspective' | 'orthographic' {
- return this.activeCamera === this.perspectiveCamera
- ? 'perspective'
- : 'orthographic'
- }
-
- toggleGrid(showGrid: boolean) {
- if (this.gridHelper) {
- this.gridHelper.visible = showGrid
- }
-
- this.emitEvent('showGridChange', showGrid)
- }
-
- togglePreview(showPreview: boolean) {
- if (this.previewRenderer) {
- this.showPreview = showPreview
-
- this.previewContainer.style.display = this.showPreview ? 'block' : 'none'
- }
-
- this.emitEvent('showPreviewChange', showPreview)
- }
-
- setLightIntensity(intensity: number) {
- this.lights.forEach((light) => {
- if (light instanceof THREE.DirectionalLight) {
- if (light === this.lights[1]) {
- light.intensity = intensity * 0.8
- } else if (light === this.lights[2]) {
- light.intensity = intensity * 0.5
- } else if (light === this.lights[5]) {
- light.intensity = intensity * 0.2
- } else {
- light.intensity = intensity * 0.3
- }
- } else if (light instanceof THREE.AmbientLight) {
- light.intensity = intensity * 0.5
- }
- })
-
- this.emitEvent('lightIntensityChange', intensity)
- }
-
- startAnimation() {
+ private startAnimation(): void {
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
- if (this.showPreview) {
- this.updatePreviewRender()
+ if (this.previewManager.showPreview) {
+ this.previewManager.updatePreviewRender()
}
const delta = this.clock.getDelta()
-
- if (this.viewHelper.animating) {
- this.viewHelper.update(delta)
-
- if (!this.viewHelper.animating) {
- this.storeNodeProperty('Camera Info', this.getCameraState())
- }
- }
+ this.viewHelperManager.update(delta)
+ this.controlsManager.update()
this.renderer.clear()
+ this.sceneManager.renderBackground()
+ this.renderer.render(
+ this.sceneManager.scene,
+ this.cameraManager.activeCamera
+ )
- if (this.backgroundMesh && this.backgroundTexture) {
- const material = this.backgroundMesh.material as THREE.MeshBasicMaterial
- if (material.map) {
- const currentToneMapping = this.renderer.toneMapping
- const currentExposure = this.renderer.toneMappingExposure
-
- this.renderer.toneMapping = THREE.NoToneMapping
-
- this.renderer.render(this.backgroundScene, this.backgroundCamera)
-
- this.renderer.toneMapping = currentToneMapping
- this.renderer.toneMappingExposure = currentExposure
- }
+ if (this.viewHelperManager.viewHelper.render) {
+ this.viewHelperManager.viewHelper.render(this.renderer)
}
-
- this.controls.update()
- this.renderer.render(this.scene, this.activeCamera)
- this.viewHelper.render(this.renderer)
}
+
animate()
}
- clearModel() {
- const objectsToRemove: THREE.Object3D[] = []
-
- this.scene.traverse((object) => {
- const isEnvironmentObject =
- object === this.gridHelper ||
- this.lights.includes(object as THREE.Light) ||
- object === this.perspectiveCamera ||
- object === this.orthographicCamera
-
- if (!isEnvironmentObject) {
- objectsToRemove.push(object)
- }
- })
-
- objectsToRemove.forEach((obj) => {
- if (obj.parent && obj.parent !== this.scene) {
- obj.parent.remove(obj)
- } else {
- this.scene.remove(obj)
- }
-
- if (obj instanceof THREE.Mesh) {
- obj.geometry?.dispose()
- if (Array.isArray(obj.material)) {
- obj.material.forEach((material) => material.dispose())
- } else {
- obj.material?.dispose()
- }
- }
- })
-
- this.resetScene()
- }
-
- protected resetScene() {
- this.currentModel = null
- this.originalRotation = null
-
- const defaultDistance = 10
- this.perspectiveCamera.position.set(
- defaultDistance,
- defaultDistance,
- defaultDistance
+ setBackgroundColor(color: string): void {
+ this.sceneManager.setBackgroundColor(color)
+ this.renderer.render(
+ this.sceneManager.scene,
+ this.cameraManager.activeCamera
)
- this.orthographicCamera.position.set(
- defaultDistance,
- defaultDistance,
- defaultDistance
- )
-
- this.perspectiveCamera.lookAt(0, 0, 0)
- this.orthographicCamera.lookAt(0, 0, 0)
-
- const frustumSize = 10
- const aspect =
- this.renderer.domElement.width / this.renderer.domElement.height
- this.orthographicCamera.left = (-frustumSize * aspect) / 2
- this.orthographicCamera.right = (frustumSize * aspect) / 2
- this.orthographicCamera.top = frustumSize / 2
- this.orthographicCamera.bottom = -frustumSize / 2
-
- this.perspectiveCamera.updateProjectionMatrix()
- this.orthographicCamera.updateProjectionMatrix()
-
- this.controls.target.set(0, 0, 0)
- this.controls.update()
-
- this.renderer.render(this.scene, this.activeCamera)
-
- this.materialMode = 'original'
- this.originalMaterials = new WeakMap()
- this.renderer.outputColorSpace = THREE.SRGBColorSpace
}
- remove() {
- if (this.animationFrameId !== null) {
- cancelAnimationFrame(this.animationFrameId)
- }
+ async setBackgroundImage(uploadPath: string): Promise {
+ await this.sceneManager.setBackgroundImage(uploadPath)
- if (this.backgroundTexture) {
- this.backgroundTexture.dispose()
- }
-
- if (this.previewBackgroundTexture) {
- this.previewBackgroundTexture.dispose()
- }
-
- if (this.backgroundMesh) {
- this.backgroundMesh.geometry.dispose()
- ;(this.backgroundMesh.material as THREE.Material).dispose()
- }
-
- if (this.previewBackgroundMesh) {
- this.previewBackgroundMesh.geometry.dispose()
- ;(this.previewBackgroundMesh.material as THREE.Material).dispose()
- }
-
- this.controls.dispose()
- this.viewHelper.dispose()
- this.renderer.dispose()
- this.renderer.domElement.remove()
- this.scene.clear()
- }
-
- protected async loadModelInternal(
- url: string,
- fileExtension: string
- ): Promise {
- let model: THREE.Object3D | null = null
-
- switch (fileExtension) {
- case 'stl':
- const geometry = await this.stlLoader.loadAsync(url)
-
- this.originalModel = geometry
-
- geometry.computeVertexNormals()
- const mesh = new THREE.Mesh(geometry, this.standardMaterial)
- const group = new THREE.Group()
- group.add(mesh)
- model = group
- break
-
- case 'fbx':
- const fbxModel = await this.fbxLoader.loadAsync(url)
-
- this.originalModel = fbxModel
-
- model = fbxModel
-
- fbxModel.traverse((child) => {
- if (child instanceof THREE.Mesh) {
- this.originalMaterials.set(child, child.material)
- }
- })
-
- break
-
- case 'obj':
- if (this.materialMode === 'original') {
- const mtlUrl = url.replace(/\.obj([^.]*$)/, '.mtl$1')
- try {
- const materials = await this.mtlLoader.loadAsync(mtlUrl)
- materials.preload()
- this.objLoader.setMaterials(materials)
- } catch (e) {
- console.log(
- 'No MTL file found or error loading it, continuing without materials'
- )
- }
- }
- model = await this.objLoader.loadAsync(url)
- model.traverse((child) => {
- if (child instanceof THREE.Mesh) {
- this.originalMaterials.set(child, child.material)
- }
- })
- break
-
- case 'gltf':
- case 'glb':
- const gltf = await this.gltfLoader.loadAsync(url)
-
- this.originalModel = gltf
-
- model = gltf.scene
- gltf.scene.traverse((child) => {
- if (child instanceof THREE.Mesh) {
- child.geometry.computeVertexNormals()
- this.originalMaterials.set(child, child.material)
- }
- })
- break
- }
-
- return model
- }
-
- async loadModel(url: string, originalFileName?: string) {
- try {
- this.clearModel()
-
- let fileExtension: string | undefined
- if (originalFileName) {
- fileExtension = originalFileName.split('.').pop()?.toLowerCase()
- } else {
- const filename = new URLSearchParams(url.split('?')[1]).get('filename')
- fileExtension = filename?.split('.').pop()?.toLowerCase()
- }
-
- if (!fileExtension) {
- useToastStore().addAlert('Could not determine file type')
- return
- }
-
- let model = await this.loadModelInternal(url, fileExtension)
-
- if (model) {
- this.currentModel = model
- await this.setupModel(model)
- }
- } catch (error) {
- console.error('Error loading model:', error)
+ if (this.previewManager.previewRenderer) {
+ this.previewManager.updateBackgroundTexture(
+ this.sceneManager.backgroundTexture
+ )
}
}
- protected async setupModel(model: THREE.Object3D) {
- const box = new THREE.Box3().setFromObject(model)
- const size = box.getSize(new THREE.Vector3())
- const center = box.getCenter(new THREE.Vector3())
+ removeBackgroundImage(): void {
+ this.sceneManager.removeBackgroundImage()
- const maxDim = Math.max(size.x, size.y, size.z)
- const targetSize = 5
- const scale = targetSize / maxDim
- model.scale.multiplyScalar(scale)
-
- box.setFromObject(model)
- box.getCenter(center)
- box.getSize(size)
-
- model.position.set(-center.x, -box.min.y, -center.z)
-
- this.scene.add(model)
-
- if (this.materialMode !== 'original') {
- this.setMaterialMode(this.materialMode)
+ if (
+ this.previewManager.previewRenderer &&
+ this.previewManager.previewCamera
+ ) {
+ this.previewManager.updateBackgroundTexture(null)
}
-
- if (this.currentUpDirection !== 'original') {
- this.setUpDirection(this.currentUpDirection)
- }
-
- await this.setupCamera(size)
}
- protected async setupCamera(size: THREE.Vector3) {
- const distance = Math.max(size.x, size.z) * 2
- const height = size.y * 2
+ toggleGrid(showGrid: boolean): void {
+ this.sceneManager.toggleGrid(showGrid)
+ }
- this.perspectiveCamera.position.set(distance, height, distance)
- this.orthographicCamera.position.set(distance, height, distance)
+ toggleCamera(cameraType?: 'perspective' | 'orthographic'): void {
+ this.cameraManager.toggleCamera(cameraType)
- if (this.activeCamera === this.perspectiveCamera) {
- this.perspectiveCamera.lookAt(0, size.y / 2, 0)
- this.perspectiveCamera.updateProjectionMatrix()
- } else {
- const frustumSize = Math.max(size.x, size.y, size.z) * 2
- const aspect =
- this.renderer.domElement.width / this.renderer.domElement.height
- this.orthographicCamera.left = (-frustumSize * aspect) / 2
- this.orthographicCamera.right = (frustumSize * aspect) / 2
- this.orthographicCamera.top = frustumSize / 2
- this.orthographicCamera.bottom = -frustumSize / 2
- this.orthographicCamera.lookAt(0, size.y / 2, 0)
- this.orthographicCamera.updateProjectionMatrix()
- }
-
- this.controls.target.set(0, size.y / 2, 0)
- this.controls.update()
-
- this.renderer.outputColorSpace = THREE.SRGBColorSpace
- this.renderer.toneMapping = THREE.ACESFilmicToneMapping
- this.renderer.toneMappingExposure = 1
+ this.controlsManager.updateCamera(this.cameraManager.activeCamera)
+ this.viewHelperManager.recreateViewHelper()
this.handleResize()
}
- refreshViewport() {
+ getCurrentCameraType(): 'perspective' | 'orthographic' {
+ return this.cameraManager.getCurrentCameraType()
+ }
+
+ setCameraState(state: CameraState): void {
+ this.cameraManager.setCameraState(state)
+ }
+
+ getCameraState(): CameraState {
+ return this.cameraManager.getCameraState()
+ }
+
+ setFOV(fov: number): void {
+ this.cameraManager.setFOV(fov)
+ this.renderer.render(
+ this.sceneManager.scene,
+ this.cameraManager.activeCamera
+ )
+ }
+
+ setMaterialMode(mode: MaterialMode): void {
+ this.modelManager.setMaterialMode(mode)
+
+ this.renderer.render(
+ this.sceneManager.scene,
+ this.cameraManager.activeCamera
+ )
+ }
+
+ async loadModel(url: string, originalFileName?: string): Promise {
+ this.cameraManager.reset()
+ this.controlsManager.reset()
+ this.modelManager.reset()
+
+ await this.loaderManager.loadModel(url, originalFileName)
+
this.handleResize()
}
- handleResize() {
+ clearModel(): void {
+ this.modelManager.clearModel()
+ }
+
+ setUpDirection(direction: UpDirection): void {
+ this.modelManager.setUpDirection(direction)
+ this.renderer.render(
+ this.sceneManager.scene,
+ this.cameraManager.activeCamera
+ )
+ }
+
+ setLightIntensity(intensity: number): void {
+ this.lightingManager.setLightIntensity(intensity)
+ }
+
+ togglePreview(showPreview: boolean): void {
+ this.previewManager.togglePreview(showPreview)
+ }
+
+ setTargetSize(width: number, height: number): void {
+ this.previewManager.setTargetSize(width, height)
+ }
+
+ addEventListener(event: string, callback: (data?: any) => void): void {
+ this.eventManager.addEventListener(event, callback)
+ }
+
+ removeEventListener(event: string, callback: (data?: any) => void): void {
+ this.eventManager.removeEventListener(event, callback)
+ }
+
+ refreshViewport(): void {
+ this.handleResize()
+ }
+
+ handleResize(): void {
const parentElement = this.renderer?.domElement?.parentElement
if (!parentElement) {
@@ -1190,131 +292,41 @@ class Load3d {
return
}
- const width = parentElement?.clientWidth
- const height = parentElement?.clientHeight
+ const width = parentElement.clientWidth
+ const height = parentElement.clientHeight
- if (this.activeCamera === this.perspectiveCamera) {
- this.perspectiveCamera.aspect = width / height
- this.perspectiveCamera.updateProjectionMatrix()
- } else {
- const frustumSize = 10
- const aspect = width / height
- this.orthographicCamera.left = (-frustumSize * aspect) / 2
- this.orthographicCamera.right = (frustumSize * aspect) / 2
- this.orthographicCamera.top = frustumSize / 2
- this.orthographicCamera.bottom = -frustumSize / 2
- this.orthographicCamera.updateProjectionMatrix()
- }
+ this.cameraManager.handleResize(width, height)
+ this.sceneManager.handleResize(width, height)
this.renderer.setSize(width, height)
- if (this.backgroundTexture && this.backgroundMesh) {
- this.updateBackgroundSize(
- this.backgroundTexture,
- this.backgroundMesh,
- width,
- height
- )
+ this.previewManager.handleResize()
+ }
+
+ captureScene(width: number, height: number): Promise {
+ return this.sceneManager.captureScene(width, height)
+ }
+
+ loadNodeProperty(name: string, defaultValue: any) {
+ return this.nodeStorage.loadNodeProperty(name, defaultValue)
+ }
+
+ remove(): void {
+ if (this.animationFrameId !== null) {
+ cancelAnimationFrame(this.animationFrameId)
}
- this.setTargetSize(this.targetWidth, this.targetHeight)
- }
+ this.sceneManager.dispose()
+ this.cameraManager.dispose()
+ this.controlsManager.dispose()
+ this.lightingManager.dispose()
+ this.viewHelperManager.dispose()
+ this.previewManager.dispose()
+ this.loaderManager.dispose()
+ this.modelManager.dispose()
- animate = () => {
- requestAnimationFrame(this.animate)
-
- this.controls.update()
- this.renderer.render(this.scene, this.activeCamera)
- }
-
- captureScene(
- width: number,
- height: number
- ): Promise<{ scene: string; mask: string }> {
- return new Promise(async (resolve, reject) => {
- try {
- this.updatePreviewSize()
- const originalWidth = this.renderer.domElement.width
- const originalHeight = this.renderer.domElement.height
- const originalClearColor = this.renderer.getClearColor(
- new THREE.Color()
- )
- const originalClearAlpha = this.renderer.getClearAlpha()
- const originalToneMapping = this.renderer.toneMapping
- const originalExposure = this.renderer.toneMappingExposure
-
- this.renderer.setSize(width, height)
-
- if (this.activeCamera === this.perspectiveCamera) {
- this.perspectiveCamera.aspect = width / height
- this.perspectiveCamera.updateProjectionMatrix()
- } else {
- const frustumSize = 10
- const aspect = width / height
- this.orthographicCamera.left = (-frustumSize * aspect) / 2
- this.orthographicCamera.right = (frustumSize * aspect) / 2
- this.orthographicCamera.top = frustumSize / 2
- this.orthographicCamera.bottom = -frustumSize / 2
- this.orthographicCamera.updateProjectionMatrix()
- }
-
- if (this.backgroundTexture && this.backgroundMesh) {
- this.updateBackgroundSize(
- this.backgroundTexture,
- this.backgroundMesh,
- width,
- height
- )
- }
-
- this.renderer.clear()
-
- if (this.backgroundMesh && this.backgroundTexture) {
- const material = this.backgroundMesh
- .material as THREE.MeshBasicMaterial
-
- if (material.map) {
- this.renderer.toneMapping = THREE.NoToneMapping
- this.renderer.render(this.backgroundScene, this.backgroundCamera)
- this.renderer.toneMapping = originalToneMapping
- this.renderer.toneMappingExposure = originalExposure
- }
- }
-
- this.renderer.render(this.scene, this.activeCamera)
- const sceneData = this.renderer.domElement.toDataURL('image/png')
-
- this.renderer.setClearColor(0x000000, 0)
- this.renderer.clear()
- this.renderer.render(this.scene, this.activeCamera)
- const maskData = this.renderer.domElement.toDataURL('image/png')
-
- this.renderer.setClearColor(originalClearColor, originalClearAlpha)
- this.renderer.setSize(originalWidth, originalHeight)
- this.handleResize()
-
- resolve({ scene: sceneData, mask: maskData })
- } catch (error) {
- reject(error)
- }
- })
- }
-
- createSTLMaterial() {
- return new THREE.MeshStandardMaterial({
- color: 0x808080,
- metalness: 0.1,
- roughness: 0.8,
- flatShading: false,
- side: THREE.DoubleSide
- })
- }
-
- setBackgroundColor(color: string) {
- this.renderer.setClearColor(new THREE.Color(color))
- this.renderer.render(this.scene, this.activeCamera)
-
- this.emitEvent('backgroundColorChange', color)
+ this.renderer.dispose()
+ this.renderer.domElement.remove()
}
}
diff --git a/src/extensions/core/load3d/Load3dAnimation.ts b/src/extensions/core/load3d/Load3dAnimation.ts
index b358e2aaf..ecabc96e8 100644
--- a/src/extensions/core/load3d/Load3dAnimation.ts
+++ b/src/extensions/core/load3d/Load3dAnimation.ts
@@ -1,206 +1,130 @@
-import PrimeVue from 'primevue/config'
-import Tooltip from 'primevue/tooltip'
+import { LGraphNode } from '@comfyorg/litegraph'
import * as THREE from 'three'
-import { createApp } from 'vue'
-import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
-import Load3d from '@/extensions/core/load3d/Load3d'
-
-interface AnimationItem {
- name: string
- index: number
-}
+import { AnimationManager } from './AnimationManager'
+import Load3d from './Load3d'
+import { Load3DOptions } from './interfaces'
class Load3dAnimation extends Load3d {
- currentAnimation: THREE.AnimationMixer | null = null
- animationActions: THREE.AnimationAction[] = []
- animationClips: THREE.AnimationClip[] = []
- selectedAnimationIndex: number = 0
- isAnimationPlaying: boolean = false
-
- animationSpeed: number = 1.0
+ private animationManager: AnimationManager
constructor(
container: Element | HTMLElement,
- options: { createPreview?: boolean } = {}
+ options: Load3DOptions = {
+ node: {} as LGraphNode
+ }
) {
super(container, options)
+
+ this.animationManager = new AnimationManager(
+ this.eventManager,
+ this.getCurrentModel.bind(this)
+ )
+
+ this.animationManager.init()
+
+ this.overrideAnimationLoop()
}
- updateAnimationList() {
- let updatedAnimationList: AnimationItem[] = []
-
- if (this.animationClips.length > 0) {
- updatedAnimationList = this.animationClips.map((clip, index) => ({
- name: clip.name || `Animation ${index + 1}`,
- index
- }))
- }
-
- this.emitEvent('animationListChange', updatedAnimationList)
+ private getCurrentModel(): THREE.Object3D | null {
+ return this.modelManager.currentModel
}
- protected async setupModel(model: THREE.Object3D) {
- await super.setupModel(model)
-
- if (this.currentAnimation) {
- this.currentAnimation.stopAllAction()
- this.animationActions = []
+ private overrideAnimationLoop(): void {
+ if (this.animationFrameId !== null) {
+ cancelAnimationFrame(this.animationFrameId)
}
- let animations: THREE.AnimationClip[] = []
- if (model.animations?.length > 0) {
- animations = model.animations
- } else if (this.originalModel && 'animations' in this.originalModel) {
- animations = (
- this.originalModel as unknown as { animations: THREE.AnimationClip[] }
- ).animations
- }
-
- if (animations.length > 0) {
- this.animationClips = animations
- if (model.type === 'Scene') {
- this.currentAnimation = new THREE.AnimationMixer(model)
- } else {
- this.currentAnimation = new THREE.AnimationMixer(this.currentModel!)
- }
-
- if (this.animationClips.length > 0) {
- this.updateSelectedAnimation(0)
- }
- }
-
- this.updateAnimationList()
- }
-
- setAnimationSpeed(speed: number) {
- this.animationSpeed = speed
- this.animationActions.forEach((action) => {
- action.setEffectiveTimeScale(speed)
- })
- }
-
- updateSelectedAnimation(index: number) {
- if (
- !this.currentAnimation ||
- !this.animationClips ||
- index >= this.animationClips.length
- ) {
- console.warn('Invalid animation update request')
- return
- }
-
- this.animationActions.forEach((action) => {
- action.stop()
- })
- this.currentAnimation.stopAllAction()
- this.animationActions = []
-
- this.selectedAnimationIndex = index
- const clip = this.animationClips[index]
-
- const action = this.currentAnimation.clipAction(clip)
-
- action.setEffectiveTimeScale(this.animationSpeed)
-
- action.reset()
- action.clampWhenFinished = false
- action.loop = THREE.LoopRepeat
-
- if (this.isAnimationPlaying) {
- action.play()
- } else {
- action.play()
- action.paused = true
- }
-
- this.animationActions = [action]
- }
-
- clearModel() {
- if (this.currentAnimation) {
- this.animationActions.forEach((action) => {
- action.stop()
- })
- this.currentAnimation = null
- }
- this.animationActions = []
- this.animationClips = []
- this.selectedAnimationIndex = 0
- this.isAnimationPlaying = false
- this.animationSpeed = 1.0
-
- this.emitEvent('animationListChange', [])
-
- super.clearModel()
- }
-
- toggleAnimation(play?: boolean) {
- if (!this.currentAnimation || this.animationActions.length === 0) {
- console.warn('No animation to toggle')
- return
- }
-
- this.isAnimationPlaying = play ?? !this.isAnimationPlaying
-
- this.animationActions.forEach((action) => {
- if (this.isAnimationPlaying) {
- action.paused = false
- if (action.time === 0 || action.time === action.getClip().duration) {
- action.reset()
- }
- } else {
- action.paused = true
- }
- })
- }
-
- startAnimation() {
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
- if (this.showPreview) {
- this.updatePreviewRender()
+ if (this.previewManager.showPreview) {
+ this.previewManager.updatePreviewRender()
}
const delta = this.clock.getDelta()
- if (this.currentAnimation && this.isAnimationPlaying) {
- this.currentAnimation.update(delta)
- }
+ this.animationManager.update(delta)
- if (this.viewHelper.animating) {
- this.viewHelper.update(delta)
+ this.viewHelperManager.update(delta)
- if (!this.viewHelper.animating) {
- this.storeNodeProperty('Camera Info', this.getCameraState())
- }
- }
+ this.controlsManager.update()
this.renderer.clear()
+ this.sceneManager.renderBackground()
+ this.renderer.render(
+ this.sceneManager.scene,
+ this.cameraManager.activeCamera
+ )
- if (this.backgroundMesh && this.backgroundTexture) {
- const material = this.backgroundMesh.material as THREE.MeshBasicMaterial
- if (material.map) {
- const currentToneMapping = this.renderer.toneMapping
- const currentExposure = this.renderer.toneMappingExposure
-
- this.renderer.toneMapping = THREE.NoToneMapping
-
- this.renderer.render(this.backgroundScene, this.backgroundCamera)
-
- this.renderer.toneMapping = currentToneMapping
- this.renderer.toneMappingExposure = currentExposure
- }
+ if (this.viewHelperManager.viewHelper.render) {
+ this.viewHelperManager.viewHelper.render(this.renderer)
}
-
- this.controls.update()
- this.renderer.render(this.scene, this.activeCamera)
- this.viewHelper.render(this.renderer)
}
+
animate()
}
+
+ async loadModel(url: string, originalFileName?: string): Promise {
+ await super.loadModel(url, originalFileName)
+
+ if (this.modelManager.currentModel) {
+ this.animationManager.setupModelAnimations(
+ this.modelManager.currentModel,
+ this.modelManager.originalModel
+ )
+ }
+ }
+
+ clearModel(): void {
+ this.animationManager.dispose()
+ super.clearModel()
+ }
+
+ updateAnimationList(): void {
+ this.animationManager.updateAnimationList()
+ }
+
+ setAnimationSpeed(speed: number): void {
+ this.animationManager.setAnimationSpeed(speed)
+ }
+
+ updateSelectedAnimation(index: number): void {
+ this.animationManager.updateSelectedAnimation(index)
+ }
+
+ toggleAnimation(play?: boolean): void {
+ this.animationManager.toggleAnimation(play)
+ }
+
+ get isAnimationPlaying(): boolean {
+ return this.animationManager.isAnimationPlaying
+ }
+
+ get animationSpeed(): number {
+ return this.animationManager.animationSpeed
+ }
+
+ get selectedAnimationIndex(): number {
+ return this.animationManager.selectedAnimationIndex
+ }
+
+ get animationClips(): THREE.AnimationClip[] {
+ return this.animationManager.animationClips
+ }
+
+ get animationActions(): THREE.AnimationAction[] {
+ return this.animationManager.animationActions
+ }
+
+ get currentAnimation(): THREE.AnimationMixer | null {
+ return this.animationManager.currentAnimation
+ }
+
+ remove(): void {
+ this.animationManager.dispose()
+ super.remove()
+ }
}
-export { AnimationItem }
export default Load3dAnimation
diff --git a/src/extensions/core/load3d/Load3dUtils.ts b/src/extensions/core/load3d/Load3dUtils.ts
index e7823b4f7..a3e4d12ab 100644
--- a/src/extensions/core/load3d/Load3dUtils.ts
+++ b/src/extensions/core/load3d/Load3dUtils.ts
@@ -1,4 +1,3 @@
-import Load3d from '@/extensions/core/load3d/Load3d'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useToastStore } from '@/stores/toastStore'
diff --git a/src/extensions/core/load3d/LoaderManager.ts b/src/extensions/core/load3d/LoaderManager.ts
new file mode 100644
index 000000000..e7aabfcd4
--- /dev/null
+++ b/src/extensions/core/load3d/LoaderManager.ts
@@ -0,0 +1,150 @@
+import * as THREE from 'three'
+import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
+import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
+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 { useToastStore } from '@/stores/toastStore'
+
+import {
+ EventManagerInterface,
+ LoaderManagerInterface,
+ ModelManagerInterface
+} from './interfaces'
+
+export class LoaderManager implements LoaderManagerInterface {
+ gltfLoader: GLTFLoader
+ objLoader: OBJLoader
+ mtlLoader: MTLLoader
+ fbxLoader: FBXLoader
+ stlLoader: STLLoader
+
+ private modelManager: ModelManagerInterface
+ private eventManager: EventManagerInterface
+
+ constructor(
+ modelManager: ModelManagerInterface,
+ eventManager: EventManagerInterface
+ ) {
+ this.modelManager = modelManager
+ this.eventManager = eventManager
+
+ this.gltfLoader = new GLTFLoader()
+ this.objLoader = new OBJLoader()
+ this.mtlLoader = new MTLLoader()
+ this.fbxLoader = new FBXLoader()
+ this.stlLoader = new STLLoader()
+ }
+
+ init(): void {}
+
+ dispose(): void {}
+
+ async loadModel(url: string, originalFileName?: string): Promise {
+ try {
+ this.eventManager.emitEvent('modelLoadingStart', null)
+
+ this.modelManager.clearModel()
+
+ let fileExtension: string | undefined
+ if (originalFileName) {
+ fileExtension = originalFileName.split('.').pop()?.toLowerCase()
+ } else {
+ const filename = new URLSearchParams(url.split('?')[1]).get('filename')
+ fileExtension = filename?.split('.').pop()?.toLowerCase()
+ }
+
+ if (!fileExtension) {
+ useToastStore().addAlert('Could not determine file type')
+ return
+ }
+
+ let model = await this.loadModelInternal(url, fileExtension)
+
+ if (model) {
+ await this.modelManager.setupModel(model)
+ }
+
+ this.eventManager.emitEvent('modelLoadingEnd', null)
+ } catch (error) {
+ this.eventManager.emitEvent('modelLoadingEnd', null)
+ console.error('Error loading model:', error)
+ useToastStore().addAlert('Error loading model')
+ }
+ }
+
+ private async loadModelInternal(
+ url: string,
+ fileExtension: string
+ ): Promise {
+ let model: THREE.Object3D | null = null
+
+ switch (fileExtension) {
+ case 'stl':
+ const geometry = await this.stlLoader.loadAsync(url)
+ this.modelManager.setOriginalModel(geometry)
+ geometry.computeVertexNormals()
+
+ const mesh = new THREE.Mesh(
+ geometry,
+ this.modelManager.standardMaterial
+ )
+
+ const group = new THREE.Group()
+ group.add(mesh)
+ model = group
+ break
+
+ case 'fbx':
+ const fbxModel = await this.fbxLoader.loadAsync(url)
+ this.modelManager.setOriginalModel(fbxModel)
+ model = fbxModel
+
+ fbxModel.traverse((child) => {
+ if (child instanceof THREE.Mesh) {
+ this.modelManager.originalMaterials.set(child, child.material)
+ }
+ })
+ break
+
+ case 'obj':
+ if (this.modelManager.materialMode === 'original') {
+ const mtlUrl = url.replace(/\.obj([^.]*$)/, '.mtl$1')
+ try {
+ const materials = await this.mtlLoader.loadAsync(mtlUrl)
+ materials.preload()
+ this.objLoader.setMaterials(materials)
+ } catch (e) {
+ console.log(
+ 'No MTL file found or error loading it, continuing without materials'
+ )
+ }
+ }
+
+ model = await this.objLoader.loadAsync(url)
+ model.traverse((child) => {
+ if (child instanceof THREE.Mesh) {
+ this.modelManager.originalMaterials.set(child, child.material)
+ }
+ })
+ break
+
+ case 'gltf':
+ case 'glb':
+ const gltf = await this.gltfLoader.loadAsync(url)
+ this.modelManager.setOriginalModel(gltf)
+ model = gltf.scene
+
+ gltf.scene.traverse((child) => {
+ if (child instanceof THREE.Mesh) {
+ child.geometry.computeVertexNormals()
+ this.modelManager.originalMaterials.set(child, child.material)
+ }
+ })
+ break
+ }
+
+ return model
+ }
+}
diff --git a/src/extensions/core/load3d/ModelManager.ts b/src/extensions/core/load3d/ModelManager.ts
new file mode 100644
index 000000000..00189f512
--- /dev/null
+++ b/src/extensions/core/load3d/ModelManager.ts
@@ -0,0 +1,313 @@
+import * as THREE from 'three'
+import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
+
+import {
+ EventManagerInterface,
+ MaterialMode,
+ ModelManagerInterface,
+ UpDirection
+} from './interfaces'
+
+export class ModelManager implements ModelManagerInterface {
+ currentModel: THREE.Object3D | null = null
+ originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
+ originalRotation: THREE.Euler | null = null
+ currentUpDirection: UpDirection = 'original'
+ materialMode: MaterialMode = 'original'
+ originalMaterials: WeakMap =
+ new WeakMap()
+ normalMaterial: THREE.MeshNormalMaterial
+ standardMaterial: THREE.MeshStandardMaterial
+ wireframeMaterial: THREE.MeshBasicMaterial
+ depthMaterial: THREE.MeshDepthMaterial
+
+ private scene: THREE.Scene
+ private renderer: THREE.WebGLRenderer
+ private eventManager: EventManagerInterface
+ private activeCamera: THREE.Camera
+ private setupCamera: (size: THREE.Vector3) => void
+
+ constructor(
+ scene: THREE.Scene,
+ renderer: THREE.WebGLRenderer,
+ eventManager: EventManagerInterface,
+ getActiveCamera: () => THREE.Camera,
+ setupCamera: (size: THREE.Vector3) => void
+ ) {
+ this.scene = scene
+ this.renderer = renderer
+ this.eventManager = eventManager
+ this.activeCamera = getActiveCamera()
+ this.setupCamera = setupCamera
+
+ this.normalMaterial = new THREE.MeshNormalMaterial({
+ flatShading: false,
+ side: THREE.DoubleSide,
+ normalScale: new THREE.Vector2(1, 1),
+ transparent: false,
+ opacity: 1.0
+ })
+
+ this.wireframeMaterial = new THREE.MeshBasicMaterial({
+ color: 0xffffff,
+ wireframe: true,
+ transparent: false,
+ opacity: 1.0
+ })
+
+ this.depthMaterial = new THREE.MeshDepthMaterial({
+ depthPacking: THREE.BasicDepthPacking,
+ side: THREE.DoubleSide
+ })
+
+ this.standardMaterial = this.createSTLMaterial()
+ }
+
+ init(): void {}
+
+ dispose(): void {
+ this.clearModel()
+ this.normalMaterial.dispose()
+ this.standardMaterial.dispose()
+ this.wireframeMaterial.dispose()
+ this.depthMaterial.dispose()
+ }
+
+ createSTLMaterial(): THREE.MeshStandardMaterial {
+ return new THREE.MeshStandardMaterial({
+ color: 0x808080,
+ metalness: 0.1,
+ roughness: 0.8,
+ flatShading: false,
+ side: THREE.DoubleSide
+ })
+ }
+
+ setMaterialMode(mode: MaterialMode): void {
+ this.materialMode = mode
+
+ if (!this.currentModel) return
+
+ if (mode === 'depth') {
+ this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
+ } else {
+ this.renderer.outputColorSpace = THREE.SRGBColorSpace
+ }
+
+ this.currentModel.traverse((child) => {
+ if (child instanceof THREE.Mesh) {
+ switch (mode) {
+ case 'depth':
+ if (!this.originalMaterials.has(child)) {
+ this.originalMaterials.set(child, child.material)
+ }
+ const depthMat = new THREE.MeshDepthMaterial({
+ depthPacking: THREE.BasicDepthPacking,
+ side: THREE.DoubleSide
+ })
+
+ depthMat.onBeforeCompile = (shader) => {
+ shader.uniforms.cameraType = {
+ value:
+ this.activeCamera instanceof THREE.OrthographicCamera
+ ? 1.0
+ : 0.0
+ }
+
+ shader.fragmentShader = `
+ uniform float cameraType;
+ ${shader.fragmentShader}
+ `
+
+ shader.fragmentShader = shader.fragmentShader.replace(
+ /gl_FragColor\s*=\s*vec4\(\s*vec3\(\s*1.0\s*-\s*fragCoordZ\s*\)\s*,\s*opacity\s*\)\s*;/,
+ `
+ float depth = 1.0 - fragCoordZ;
+ if (cameraType > 0.5) {
+ depth = pow(depth, 400.0);
+ } else {
+ depth = pow(depth, 0.6);
+ }
+ gl_FragColor = vec4(vec3(depth), opacity);
+ `
+ )
+ }
+
+ depthMat.customProgramCacheKey = () => {
+ return this.activeCamera instanceof THREE.OrthographicCamera
+ ? 'ortho'
+ : 'persp'
+ }
+
+ child.material = depthMat
+ break
+ case 'normal':
+ if (!this.originalMaterials.has(child)) {
+ this.originalMaterials.set(child, child.material)
+ }
+ child.material = new THREE.MeshNormalMaterial({
+ flatShading: false,
+ side: THREE.DoubleSide,
+ normalScale: new THREE.Vector2(1, 1),
+ transparent: false,
+ opacity: 1.0
+ })
+ child.geometry.computeVertexNormals()
+ break
+
+ case 'wireframe':
+ if (!this.originalMaterials.has(child)) {
+ this.originalMaterials.set(child, child.material)
+ }
+ child.material = new THREE.MeshBasicMaterial({
+ color: 0xffffff,
+ wireframe: true,
+ transparent: false,
+ opacity: 1.0
+ })
+ break
+
+ case 'original':
+ const originalMaterial = this.originalMaterials.get(child)
+ if (originalMaterial) {
+ child.material = originalMaterial
+ } else {
+ child.material = this.standardMaterial
+ }
+ break
+ }
+ }
+ })
+
+ this.eventManager.emitEvent('materialModeChange', mode)
+ }
+
+ setupModelMaterials(model: THREE.Object3D): void {
+ model.traverse((child) => {
+ if (child instanceof THREE.Mesh) {
+ this.originalMaterials.set(child, child.material)
+ }
+ })
+
+ if (this.materialMode !== 'original') {
+ this.setMaterialMode(this.materialMode)
+ }
+ }
+
+ clearModel(): void {
+ const objectsToRemove: THREE.Object3D[] = []
+
+ this.scene.traverse((object) => {
+ const isEnvironmentObject =
+ object instanceof THREE.GridHelper ||
+ object instanceof THREE.Light ||
+ object instanceof THREE.Camera
+
+ if (!isEnvironmentObject) {
+ objectsToRemove.push(object)
+ }
+ })
+
+ objectsToRemove.forEach((obj) => {
+ if (obj.parent && obj.parent !== this.scene) {
+ obj.parent.remove(obj)
+ } else {
+ this.scene.remove(obj)
+ }
+
+ if (obj instanceof THREE.Mesh) {
+ obj.geometry?.dispose()
+ if (Array.isArray(obj.material)) {
+ obj.material.forEach((material) => material.dispose())
+ } else {
+ obj.material?.dispose()
+ }
+ }
+ })
+
+ this.reset()
+ }
+
+ reset(): void {
+ this.currentModel = null
+ this.originalRotation = null
+ this.currentUpDirection = 'original'
+ this.setMaterialMode('original')
+
+ this.originalMaterials = new WeakMap()
+ }
+
+ async setupModel(model: THREE.Object3D): Promise {
+ this.currentModel = model
+
+ const box = new THREE.Box3().setFromObject(model)
+ const size = box.getSize(new THREE.Vector3())
+ const center = box.getCenter(new THREE.Vector3())
+
+ const maxDim = Math.max(size.x, size.y, size.z)
+ const targetSize = 5
+ const scale = targetSize / maxDim
+ model.scale.multiplyScalar(scale)
+
+ box.setFromObject(model)
+ box.getCenter(center)
+ box.getSize(size)
+
+ model.position.set(-center.x, -box.min.y, -center.z)
+
+ this.scene.add(model)
+
+ if (this.materialMode !== 'original') {
+ this.setMaterialMode(this.materialMode)
+ }
+
+ if (this.currentUpDirection !== 'original') {
+ this.setUpDirection(this.currentUpDirection)
+ }
+ this.setupModelMaterials(model)
+
+ this.setupCamera(size)
+ }
+
+ setOriginalModel(model: THREE.Object3D | THREE.BufferGeometry | GLTF): void {
+ this.originalModel = model
+ }
+
+ setUpDirection(direction: UpDirection): void {
+ if (!this.currentModel) return
+
+ if (!this.originalRotation && this.currentModel.rotation) {
+ this.originalRotation = this.currentModel.rotation.clone()
+ }
+
+ this.currentUpDirection = direction
+
+ if (this.originalRotation) {
+ this.currentModel.rotation.copy(this.originalRotation)
+ }
+
+ switch (direction) {
+ case 'original':
+ break
+ case '-x':
+ this.currentModel.rotation.z = Math.PI / 2
+ break
+ case '+x':
+ this.currentModel.rotation.z = -Math.PI / 2
+ break
+ case '-y':
+ this.currentModel.rotation.x = Math.PI
+ break
+ case '+y':
+ break
+ case '-z':
+ this.currentModel.rotation.x = Math.PI / 2
+ break
+ case '+z':
+ this.currentModel.rotation.x = -Math.PI / 2
+ break
+ }
+
+ this.eventManager.emitEvent('upDirectionChange', direction)
+ }
+}
diff --git a/src/extensions/core/load3d/NodeStorage.ts b/src/extensions/core/load3d/NodeStorage.ts
new file mode 100644
index 000000000..7791a1fe2
--- /dev/null
+++ b/src/extensions/core/load3d/NodeStorage.ts
@@ -0,0 +1,32 @@
+import { LGraphNode } from '@comfyorg/litegraph'
+
+import { 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/PreviewManager.ts b/src/extensions/core/load3d/PreviewManager.ts
new file mode 100644
index 000000000..d63272fa6
--- /dev/null
+++ b/src/extensions/core/load3d/PreviewManager.ts
@@ -0,0 +1,324 @@
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+
+import { EventManagerInterface, PreviewManagerInterface } from './interfaces'
+
+export class PreviewManager implements PreviewManagerInterface {
+ previewRenderer: THREE.WebGLRenderer | null = null
+ previewCamera: THREE.Camera
+ previewContainer: HTMLDivElement = {} as HTMLDivElement
+ showPreview: boolean = true
+ previewWidth: number = 120
+
+ private targetWidth: number = 1024
+ private targetHeight: number = 1024
+ private scene: THREE.Scene
+ private getActiveCamera: () => THREE.Camera
+ private getControls: () => OrbitControls
+ private eventManager: EventManagerInterface
+
+ private getRenderer: () => THREE.WebGLRenderer
+
+ private previewBackgroundScene: THREE.Scene
+ private previewBackgroundCamera: THREE.OrthographicCamera
+ private previewBackgroundMesh: THREE.Mesh | null = null
+ private previewBackgroundTexture: THREE.Texture | null = null
+
+ constructor(
+ scene: THREE.Scene,
+ getActiveCamera: () => THREE.Camera,
+ getControls: () => OrbitControls,
+ getRenderer: () => THREE.WebGLRenderer,
+ eventManager: EventManagerInterface,
+ backgroundScene: THREE.Scene,
+ backgroundCamera: THREE.OrthographicCamera
+ ) {
+ this.scene = scene
+ this.getActiveCamera = getActiveCamera
+ this.getControls = getControls
+ this.getRenderer = getRenderer
+ this.eventManager = eventManager
+
+ this.previewCamera = this.getActiveCamera().clone()
+
+ this.previewBackgroundScene = backgroundScene.clone()
+ this.previewBackgroundCamera = backgroundCamera.clone()
+
+ const planeGeometry = new THREE.PlaneGeometry(2, 2)
+ const planeMaterial = new THREE.MeshBasicMaterial({
+ transparent: true,
+ depthWrite: false,
+ depthTest: false,
+ side: THREE.DoubleSide
+ })
+
+ this.previewBackgroundMesh = new THREE.Mesh(planeGeometry, planeMaterial)
+ this.previewBackgroundMesh.position.set(0, 0, 0)
+ this.previewBackgroundScene.add(this.previewBackgroundMesh)
+ }
+
+ init(): void {}
+
+ dispose(): void {
+ if (this.previewRenderer) {
+ this.previewRenderer.dispose()
+ }
+
+ if (this.previewBackgroundTexture) {
+ this.previewBackgroundTexture.dispose()
+ }
+
+ if (this.previewBackgroundMesh) {
+ this.previewBackgroundMesh.geometry.dispose()
+ ;(this.previewBackgroundMesh.material as THREE.Material).dispose()
+ }
+ }
+
+ createCapturePreview(container: Element | HTMLElement): void {
+ this.previewRenderer = new THREE.WebGLRenderer({
+ alpha: true,
+ antialias: true,
+ preserveDrawingBuffer: true
+ })
+
+ this.previewRenderer.setSize(this.targetWidth, this.targetHeight)
+ this.previewRenderer.setClearColor(0x282828)
+ this.previewRenderer.autoClear = false
+ this.previewRenderer.outputColorSpace = THREE.SRGBColorSpace
+
+ this.previewContainer = document.createElement('div')
+ this.previewContainer.style.cssText = `
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.2);
+ display: block;
+ transition: border-color 0.1s ease;
+ `
+ this.previewContainer.appendChild(this.previewRenderer.domElement)
+
+ const MIN_PREVIEW_WIDTH = 120
+ const MAX_PREVIEW_WIDTH = 240
+
+ this.previewContainer.addEventListener('wheel', (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+
+ const delta = event.deltaY
+ const oldWidth = this.previewWidth
+
+ if (delta > 0) {
+ this.previewWidth = Math.max(MIN_PREVIEW_WIDTH, this.previewWidth - 10)
+ } else {
+ this.previewWidth = Math.min(MAX_PREVIEW_WIDTH, this.previewWidth + 10)
+ }
+
+ if (
+ oldWidth !== this.previewWidth &&
+ (this.previewWidth === MIN_PREVIEW_WIDTH ||
+ this.previewWidth === MAX_PREVIEW_WIDTH)
+ ) {
+ this.flashPreviewBorder()
+ }
+
+ this.updatePreviewSize()
+ this.updatePreviewRender()
+ })
+
+ this.previewContainer.style.display = this.showPreview ? 'block' : 'none'
+
+ container.appendChild(this.previewContainer)
+
+ this.updatePreviewSize()
+ }
+
+ flashPreviewBorder(): void {
+ const originalBorder = this.previewContainer.style.border
+ const originalBoxShadow = this.previewContainer.style.boxShadow
+
+ this.previewContainer.style.border = '2px solid rgba(255, 255, 255, 0.8)'
+ this.previewContainer.style.boxShadow = '0 0 8px rgba(255, 255, 255, 0.5)'
+
+ setTimeout(() => {
+ this.previewContainer.style.border = originalBorder
+ this.previewContainer.style.boxShadow = originalBoxShadow
+ }, 100)
+ }
+
+ updatePreviewSize(): void {
+ if (!this.previewContainer) return
+
+ const previewHeight =
+ (this.previewWidth * this.targetHeight) / this.targetWidth
+ this.previewRenderer?.setSize(this.previewWidth, previewHeight, false)
+ }
+
+ updatePreviewRender(): void {
+ if (!this.previewRenderer || !this.previewContainer || !this.showPreview)
+ return
+
+ if (
+ !this.previewCamera ||
+ (this.getActiveCamera() instanceof THREE.PerspectiveCamera &&
+ !(this.previewCamera instanceof THREE.PerspectiveCamera)) ||
+ (this.getActiveCamera() instanceof THREE.OrthographicCamera &&
+ !(this.previewCamera instanceof THREE.OrthographicCamera))
+ ) {
+ this.previewCamera = this.getActiveCamera().clone()
+ }
+
+ this.previewCamera.position.copy(this.getActiveCamera().position)
+ this.previewCamera.rotation.copy(this.getActiveCamera().rotation)
+
+ const aspect = this.targetWidth / this.targetHeight
+
+ if (this.getActiveCamera() instanceof THREE.OrthographicCamera) {
+ const activeOrtho = this.getActiveCamera() as THREE.OrthographicCamera
+ const previewOrtho = this.previewCamera as THREE.OrthographicCamera
+
+ const frustumHeight =
+ (activeOrtho.top - activeOrtho.bottom) / activeOrtho.zoom
+
+ const frustumWidth = frustumHeight * aspect
+
+ previewOrtho.top = frustumHeight / 2
+ previewOrtho.left = -frustumWidth / 2
+ previewOrtho.right = frustumWidth / 2
+ previewOrtho.bottom = -frustumHeight / 2
+ previewOrtho.zoom = 1
+
+ previewOrtho.updateProjectionMatrix()
+ } else {
+ ;(this.previewCamera as THREE.PerspectiveCamera).aspect = aspect
+ ;(this.previewCamera as THREE.PerspectiveCamera).fov = (
+ this.getActiveCamera() as THREE.PerspectiveCamera
+ ).fov
+ ;(this.previewCamera as THREE.PerspectiveCamera).updateProjectionMatrix()
+ }
+
+ this.previewCamera.lookAt(this.getControls().target)
+
+ const previewHeight =
+ (this.previewWidth * this.targetHeight) / this.targetWidth
+ this.previewRenderer.setSize(this.previewWidth, previewHeight, false)
+ this.previewRenderer.outputColorSpace = THREE.SRGBColorSpace
+ this.previewRenderer.clear()
+
+ if (this.previewBackgroundMesh && this.previewBackgroundTexture) {
+ const material = this.previewBackgroundMesh
+ .material as THREE.MeshBasicMaterial
+ if (material.map) {
+ const currentToneMapping = this.previewRenderer.toneMapping
+ const currentExposure = this.previewRenderer.toneMappingExposure
+
+ this.previewRenderer.toneMapping = THREE.NoToneMapping
+ this.previewRenderer.render(
+ this.previewBackgroundScene,
+ this.previewBackgroundCamera
+ )
+
+ this.previewRenderer.toneMapping = currentToneMapping
+ this.previewRenderer.toneMappingExposure = currentExposure
+ }
+ }
+
+ this.previewRenderer.render(this.scene, this.previewCamera)
+ }
+
+ togglePreview(showPreview: boolean): void {
+ if (this.previewRenderer) {
+ this.showPreview = showPreview
+ this.previewContainer.style.display = this.showPreview ? 'block' : 'none'
+ }
+
+ this.eventManager.emitEvent('showPreviewChange', showPreview)
+ }
+
+ setTargetSize(width: number, height: number): void {
+ const oldAspect = this.targetWidth / this.targetHeight
+
+ this.targetWidth = width
+ this.targetHeight = height
+
+ this.updatePreviewSize()
+
+ const newAspect = width / height
+ if (Math.abs(oldAspect - newAspect) > 0.001) {
+ this.updateBackgroundSize(
+ this.previewBackgroundTexture,
+ this.previewBackgroundMesh,
+ width,
+ height
+ )
+ }
+
+ if (this.previewRenderer && this.previewCamera) {
+ if (this.previewCamera instanceof THREE.PerspectiveCamera) {
+ this.previewCamera.aspect = width / height
+ this.previewCamera.updateProjectionMatrix()
+ } else if (this.previewCamera instanceof THREE.OrthographicCamera) {
+ const frustumSize = 10
+ const aspect = width / height
+ this.previewCamera.left = (-frustumSize * aspect) / 2
+ this.previewCamera.right = (frustumSize * aspect) / 2
+ this.previewCamera.updateProjectionMatrix()
+ }
+ }
+ }
+
+ handleResize(): void {
+ this.updatePreviewSize()
+ this.updatePreviewRender()
+ }
+
+ updateBackgroundTexture(texture: THREE.Texture | null): void {
+ if (this.previewBackgroundTexture) {
+ this.previewBackgroundTexture.dispose()
+ }
+
+ this.previewBackgroundTexture = texture
+
+ if (texture && this.previewBackgroundMesh) {
+ const material2 = this.previewBackgroundMesh
+ .material as THREE.MeshBasicMaterial
+ material2.map = texture
+ material2.needsUpdate = true
+
+ this.previewBackgroundMesh.position.set(0, 0, 0)
+
+ this.updateBackgroundSize(
+ this.previewBackgroundTexture,
+ this.previewBackgroundMesh,
+ this.targetWidth,
+ this.targetHeight
+ )
+ }
+ }
+
+ private updateBackgroundSize(
+ backgroundTexture: THREE.Texture | null,
+ backgroundMesh: THREE.Mesh | null,
+ targetWidth: number,
+ targetHeight: number
+ ): void {
+ if (!backgroundTexture || !backgroundMesh) return
+
+ const material = backgroundMesh.material as THREE.MeshBasicMaterial
+
+ if (!material.map) return
+
+ const imageAspect =
+ backgroundTexture.image.width / backgroundTexture.image.height
+ const targetAspect = targetWidth / targetHeight
+
+ if (imageAspect > targetAspect) {
+ backgroundMesh.scale.set(imageAspect / targetAspect, 1, 1)
+ } else {
+ backgroundMesh.scale.set(1, targetAspect / imageAspect, 1)
+ }
+
+ material.needsUpdate = true
+ }
+
+ reset(): void {}
+}
diff --git a/src/extensions/core/load3d/SceneManager.ts b/src/extensions/core/load3d/SceneManager.ts
new file mode 100644
index 000000000..805677626
--- /dev/null
+++ b/src/extensions/core/load3d/SceneManager.ts
@@ -0,0 +1,277 @@
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+
+import Load3dUtils from './Load3dUtils'
+import { EventManagerInterface, SceneManagerInterface } from './interfaces'
+
+export class SceneManager implements SceneManagerInterface {
+ scene: THREE.Scene
+ gridHelper: THREE.GridHelper
+
+ backgroundScene: THREE.Scene
+ backgroundCamera: THREE.OrthographicCamera
+ backgroundMesh: THREE.Mesh | null = null
+ backgroundTexture: THREE.Texture | null = null
+
+ private eventManager: EventManagerInterface
+ private renderer: THREE.WebGLRenderer
+
+ private getActiveCamera: () => THREE.Camera
+ private getControls: () => OrbitControls
+
+ constructor(
+ renderer: THREE.WebGLRenderer,
+ getActiveCamera: () => THREE.Camera,
+ getControls: () => OrbitControls,
+ eventManager: EventManagerInterface
+ ) {
+ this.renderer = renderer
+ this.eventManager = eventManager
+ this.scene = new THREE.Scene()
+
+ this.getActiveCamera = getActiveCamera
+ this.getControls = getControls
+
+ this.gridHelper = new THREE.GridHelper(10, 10)
+ this.gridHelper.position.set(0, 0, 0)
+ this.scene.add(this.gridHelper)
+
+ this.backgroundScene = new THREE.Scene()
+ this.backgroundCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1)
+
+ const planeGeometry = new THREE.PlaneGeometry(2, 2)
+ const planeMaterial = new THREE.MeshBasicMaterial({
+ transparent: true,
+ depthWrite: false,
+ depthTest: false,
+ side: THREE.DoubleSide
+ })
+
+ this.backgroundMesh = new THREE.Mesh(planeGeometry, planeMaterial)
+ this.backgroundMesh.position.set(0, 0, 0)
+ this.backgroundScene.add(this.backgroundMesh)
+ }
+
+ init(): void {}
+
+ dispose(): void {
+ if (this.backgroundTexture) {
+ this.backgroundTexture.dispose()
+ }
+
+ if (this.backgroundMesh) {
+ this.backgroundMesh.geometry.dispose()
+ ;(this.backgroundMesh.material as THREE.Material).dispose()
+ }
+
+ this.scene.clear()
+ }
+
+ toggleGrid(showGrid: boolean): void {
+ if (this.gridHelper) {
+ this.gridHelper.visible = showGrid
+ }
+
+ this.eventManager.emitEvent('showGridChange', showGrid)
+ }
+
+ setBackgroundColor(color: string): void {
+ this.renderer.setClearColor(new THREE.Color(color))
+ this.eventManager.emitEvent('backgroundColorChange', color)
+ }
+
+ async setBackgroundImage(uploadPath: string): Promise {
+ if (uploadPath === '') {
+ this.removeBackgroundImage()
+ return
+ }
+
+ let imageUrl = Load3dUtils.getResourceURL(
+ ...Load3dUtils.splitFilePath(uploadPath)
+ )
+
+ if (!imageUrl.startsWith('/api')) {
+ imageUrl = '/api' + imageUrl
+ }
+
+ try {
+ const textureLoader = new THREE.TextureLoader()
+ const texture = await new Promise((resolve, reject) => {
+ textureLoader.load(imageUrl, resolve, undefined, reject)
+ })
+
+ if (this.backgroundTexture) {
+ this.backgroundTexture.dispose()
+ }
+
+ texture.colorSpace = THREE.SRGBColorSpace
+
+ this.backgroundTexture = texture
+
+ const material = this.backgroundMesh?.material as THREE.MeshBasicMaterial
+ material.map = texture
+ material.needsUpdate = true
+
+ this.backgroundMesh?.position.set(0, 0, 0)
+
+ this.updateBackgroundSize(
+ this.backgroundTexture,
+ this.backgroundMesh,
+ this.renderer.domElement.width,
+ this.renderer.domElement.height
+ )
+
+ this.eventManager.emitEvent('backgroundImageChange', uploadPath)
+ } catch (error) {
+ console.error('Error loading background image:', error)
+ }
+ }
+
+ removeBackgroundImage(): void {
+ if (this.backgroundMesh) {
+ const material = this.backgroundMesh.material as THREE.MeshBasicMaterial
+ material.map = null
+ material.needsUpdate = true
+ }
+
+ if (this.backgroundTexture) {
+ this.backgroundTexture.dispose()
+ this.backgroundTexture = null
+ }
+ }
+
+ updateBackgroundSize(
+ backgroundTexture: THREE.Texture | null,
+ backgroundMesh: THREE.Mesh | null,
+ targetWidth: number,
+ targetHeight: number
+ ): void {
+ if (!backgroundTexture || !backgroundMesh) return
+
+ const material = backgroundMesh.material as THREE.MeshBasicMaterial
+
+ if (!material.map) return
+
+ const imageAspect =
+ backgroundTexture.image.width / backgroundTexture.image.height
+ const targetAspect = targetWidth / targetHeight
+
+ if (imageAspect > targetAspect) {
+ backgroundMesh.scale.set(imageAspect / targetAspect, 1, 1)
+ } else {
+ backgroundMesh.scale.set(1, targetAspect / imageAspect, 1)
+ }
+
+ material.needsUpdate = true
+ }
+
+ handleResize(width: number, height: number): void {
+ if (this.backgroundTexture && this.backgroundMesh) {
+ this.updateBackgroundSize(
+ this.backgroundTexture,
+ this.backgroundMesh,
+ width,
+ height
+ )
+ }
+ }
+
+ renderBackground(): void {
+ if (this.backgroundMesh && this.backgroundTexture) {
+ const material = this.backgroundMesh.material as THREE.MeshBasicMaterial
+ if (material.map) {
+ const currentToneMapping = this.renderer.toneMapping
+ const currentExposure = this.renderer.toneMappingExposure
+
+ this.renderer.toneMapping = THREE.NoToneMapping
+ this.renderer.render(this.backgroundScene, this.backgroundCamera)
+
+ this.renderer.toneMapping = currentToneMapping
+ this.renderer.toneMappingExposure = currentExposure
+ }
+ }
+ }
+
+ captureScene(
+ width: number,
+ height: number
+ ): Promise<{ scene: string; mask: string }> {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const originalWidth = this.renderer.domElement.width
+ const originalHeight = this.renderer.domElement.height
+ const originalClearColor = this.renderer.getClearColor(
+ new THREE.Color()
+ )
+ const originalClearAlpha = this.renderer.getClearAlpha()
+ const originalToneMapping = this.renderer.toneMapping
+ const originalExposure = this.renderer.toneMappingExposure
+
+ this.renderer.setSize(width, height)
+
+ if (this.getActiveCamera() instanceof THREE.PerspectiveCamera) {
+ const perspectiveCamera =
+ this.getActiveCamera() as THREE.PerspectiveCamera
+
+ perspectiveCamera.aspect = width / height
+ perspectiveCamera.updateProjectionMatrix()
+ } else {
+ const orthographicCamera =
+ this.getActiveCamera() as THREE.OrthographicCamera
+
+ const frustumSize = 10
+ const aspect = width / height
+
+ orthographicCamera.left = (-frustumSize * aspect) / 2
+ orthographicCamera.right = (frustumSize * aspect) / 2
+ orthographicCamera.top = frustumSize / 2
+ orthographicCamera.bottom = -frustumSize / 2
+
+ orthographicCamera.updateProjectionMatrix()
+ }
+
+ if (this.backgroundTexture && this.backgroundMesh) {
+ this.updateBackgroundSize(
+ this.backgroundTexture,
+ this.backgroundMesh,
+ width,
+ height
+ )
+ }
+
+ this.renderer.clear()
+
+ if (this.backgroundMesh && this.backgroundTexture) {
+ const material = this.backgroundMesh
+ .material as THREE.MeshBasicMaterial
+
+ if (material.map) {
+ this.renderer.toneMapping = THREE.NoToneMapping
+ this.renderer.render(this.backgroundScene, this.backgroundCamera)
+ this.renderer.toneMapping = originalToneMapping
+ this.renderer.toneMappingExposure = originalExposure
+ }
+ }
+
+ this.renderer.render(this.scene, this.getActiveCamera())
+ const sceneData = this.renderer.domElement.toDataURL('image/png')
+
+ this.renderer.setClearColor(0x000000, 0)
+ this.renderer.clear()
+ this.renderer.render(this.scene, this.getActiveCamera())
+ const maskData = this.renderer.domElement.toDataURL('image/png')
+
+ this.renderer.setClearColor(originalClearColor, originalClearAlpha)
+ this.renderer.setSize(originalWidth, originalHeight)
+
+ this.handleResize(width, height)
+
+ resolve({ scene: sceneData, mask: maskData })
+ } catch (error) {
+ reject(error)
+ }
+ })
+ }
+
+ reset(): void {}
+}
diff --git a/src/extensions/core/load3d/ViewHelperManager.ts b/src/extensions/core/load3d/ViewHelperManager.ts
new file mode 100644
index 000000000..3a95d394a
--- /dev/null
+++ b/src/extensions/core/load3d/ViewHelperManager.ts
@@ -0,0 +1,104 @@
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
+
+import { NodeStorageInterface, ViewHelperManagerInterface } from './interfaces'
+
+export class ViewHelperManager implements ViewHelperManagerInterface {
+ viewHelper: ViewHelper = {} as ViewHelper
+ viewHelperContainer: HTMLDivElement = {} as HTMLDivElement
+
+ private getActiveCamera: () => THREE.Camera
+ private getControls: () => OrbitControls
+ private nodeStorage: NodeStorageInterface
+ private renderer: THREE.WebGLRenderer
+
+ constructor(
+ renderer: THREE.WebGLRenderer,
+ getActiveCamera: () => THREE.Camera,
+ getControls: () => OrbitControls,
+ nodeStorage: NodeStorageInterface
+ ) {
+ this.renderer = renderer
+ this.getActiveCamera = getActiveCamera
+ this.getControls = getControls
+ this.nodeStorage = nodeStorage
+ }
+
+ 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) {
+ this.nodeStorage.storeNodeProperty('Camera Info', {
+ 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'
+ })
+ }
+ }
+ }
+
+ handleResize(): void {}
+
+ recreateViewHelper(): void {
+ if (this.viewHelper) {
+ this.viewHelper.dispose()
+ }
+
+ this.viewHelper = new ViewHelper(
+ this.getActiveCamera(),
+ this.viewHelperContainer
+ )
+ this.viewHelper.center = this.getControls().target
+ }
+
+ reset(): void {}
+}
diff --git a/src/extensions/core/load3d/interfaces.ts b/src/extensions/core/load3d/interfaces.ts
new file mode 100644
index 000000000..9ae277d56
--- /dev/null
+++ b/src/extensions/core/load3d/interfaces.ts
@@ -0,0 +1,164 @@
+import { LGraphNode } from '@comfyorg/litegraph'
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
+import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
+import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
+import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
+import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
+import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
+
+export type MaterialMode = 'original' | 'normal' | 'wireframe' | 'depth'
+export type UpDirection = 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
+export type CameraType = 'perspective' | 'orthographic'
+
+export interface CameraState {
+ position: THREE.Vector3
+ target: THREE.Vector3
+ zoom: number
+ cameraType: CameraType
+}
+
+export interface EventCallback {
+ (data?: any): void
+}
+
+export interface Load3DOptions {
+ createPreview?: boolean
+ node?: LGraphNode
+}
+
+export interface CaptureResult {
+ scene: string
+ mask: string
+}
+
+export interface BaseManager {
+ init(): void
+ dispose(): void
+ reset(): void
+}
+
+export interface AnimationItem {
+ name: string
+ index: number
+}
+
+export interface SceneManagerInterface extends BaseManager {
+ scene: THREE.Scene
+ gridHelper: THREE.GridHelper
+ toggleGrid(showGrid: boolean): void
+ setBackgroundColor(color: string): void
+ setBackgroundImage(uploadPath: string): Promise
+ removeBackgroundImage(): void
+ handleResize(width: number, height: number): void
+ captureScene(width: number, height: number): Promise
+}
+
+export interface CameraManagerInterface extends BaseManager {
+ activeCamera: THREE.Camera
+ perspectiveCamera: THREE.PerspectiveCamera
+ orthographicCamera: THREE.OrthographicCamera
+ getCurrentCameraType(): CameraType
+ toggleCamera(cameraType?: CameraType): void
+ setFOV(fov: number): void
+ setCameraState(state: CameraState): void
+ getCameraState(): CameraState
+ handleResize(width: number, height: number): void
+ setControls(controls: OrbitControls): void
+}
+
+export interface ControlsManagerInterface extends BaseManager {
+ controls: OrbitControls
+ handleResize(): void
+}
+
+export interface LightingManagerInterface extends BaseManager {
+ lights: THREE.Light[]
+ setLightIntensity(intensity: number): void
+}
+
+export interface ViewHelperManagerInterface extends BaseManager {
+ viewHelper: ViewHelper
+ viewHelperContainer: HTMLDivElement
+ createViewHelper(container: Element | HTMLElement): void
+ update(delta: number): void
+ handleResize(): void
+}
+
+export interface PreviewManagerInterface extends BaseManager {
+ previewRenderer: THREE.WebGLRenderer | null
+ previewCamera: THREE.Camera
+ previewContainer: HTMLDivElement
+ showPreview: boolean
+ previewWidth: number
+ createCapturePreview(container: Element | HTMLElement): void
+ updatePreviewSize(): void
+ updatePreviewRender(): void
+ togglePreview(showPreview: boolean): void
+ setTargetSize(width: number, height: number): void
+ handleResize(): void
+ updateBackgroundTexture(texture: THREE.Texture | null): void
+}
+
+export interface EventManagerInterface {
+ addEventListener(event: string, callback: EventCallback): void
+ removeEventListener(event: string, callback: EventCallback): void
+ 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[]
+ animationClips: THREE.AnimationClip[]
+ selectedAnimationIndex: number
+ isAnimationPlaying: boolean
+ animationSpeed: number
+
+ setupModelAnimations(model: THREE.Object3D, originalModel: any): void
+ updateAnimationList(): void
+ setAnimationSpeed(speed: number): void
+ updateSelectedAnimation(index: number): void
+ toggleAnimation(play?: boolean): void
+ update(delta: number): void
+}
+
+export interface ModelManagerInterface {
+ currentModel: THREE.Object3D | null
+ originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null
+ originalRotation: THREE.Euler | null
+ currentUpDirection: UpDirection
+
+ init(): void
+ dispose(): void
+ clearModel(): void
+ reset(): void
+ setupModel(model: THREE.Object3D): Promise
+ setOriginalModel(model: THREE.Object3D | THREE.BufferGeometry | GLTF): void
+ setUpDirection(direction: UpDirection): void
+ materialMode: MaterialMode
+ originalMaterials: WeakMap
+ normalMaterial: THREE.MeshNormalMaterial
+ standardMaterial: THREE.MeshStandardMaterial
+ wireframeMaterial: THREE.MeshBasicMaterial
+ depthMaterial: THREE.MeshDepthMaterial
+ setMaterialMode(mode: MaterialMode): void
+ setupModelMaterials(model: THREE.Object3D): void
+}
+
+export interface LoaderManagerInterface {
+ gltfLoader: GLTFLoader
+ objLoader: OBJLoader
+ mtlLoader: MTLLoader
+ fbxLoader: FBXLoader
+ stlLoader: STLLoader
+
+ init(): void
+ dispose(): void
+ loadModel(url: string, originalFileName?: string): Promise
+}
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index caed96120..c4695818e 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -847,6 +847,7 @@
"fov": "FOV",
"previewOutput": "Preview Output",
"uploadBackgroundImage": "Upload Background Image",
- "removeBackgroundImage": "Remove Background Image"
+ "removeBackgroundImage": "Remove Background Image",
+ "loadingModel": "Loading 3D Model..."
}
}
\ No newline at end of file
diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json
index 86b5f2afa..1cad7f14f 100644
--- a/src/locales/fr/main.json
+++ b/src/locales/fr/main.json
@@ -321,6 +321,7 @@
"backgroundColor": "Couleur de fond",
"fov": "FOV",
"lightIntensity": "Intensité de la lumière",
+ "loadingModel": "Chargement du modèle 3D...",
"previewOutput": "Aperçu de la sortie",
"removeBackgroundImage": "Supprimer l'image de fond",
"showGrid": "Afficher la grille",
diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json
index 1510b87d4..234d46846 100644
--- a/src/locales/ja/main.json
+++ b/src/locales/ja/main.json
@@ -321,6 +321,7 @@
"backgroundColor": "背景色",
"fov": "FOV",
"lightIntensity": "光の強度",
+ "loadingModel": "3Dモデルを読み込んでいます...",
"previewOutput": "出力のプレビュー",
"removeBackgroundImage": "背景画像を削除",
"showGrid": "グリッドを表示",
diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json
index 063d3a47f..0a716434f 100644
--- a/src/locales/ru/main.json
+++ b/src/locales/ru/main.json
@@ -321,6 +321,7 @@
"backgroundColor": "Цвет фона",
"fov": "Угол обзора",
"lightIntensity": "Интенсивность света",
+ "loadingModel": "Загрузка 3D модели...",
"previewOutput": "Предварительный просмотр",
"removeBackgroundImage": "Удалить фоновое изображение",
"showGrid": "Показать сетку",
diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json
index 0f71f874e..2e452a827 100644
--- a/src/locales/zh/main.json
+++ b/src/locales/zh/main.json
@@ -321,6 +321,7 @@
"backgroundColor": "背景颜色",
"fov": "视场",
"lightIntensity": "光照强度",
+ "loadingModel": "正在加载3D模型...",
"previewOutput": "预览输出",
"removeBackgroundImage": "移除背景图片",
"showGrid": "显示网格",