diff --git a/src/extensions/core/load3d.ts b/src/extensions/core/load3d.ts
index 5a00648e5..5be50af48 100644
--- a/src/extensions/core/load3d.ts
+++ b/src/extensions/core/load3d.ts
@@ -2,6 +2,7 @@
import { IWidget } 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'
@@ -115,8 +116,7 @@ class Load3d {
stlLoader: STLLoader
currentModel: THREE.Object3D | null = null
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
- node: any
- private animationFrameId: number | null = null
+ animationFrameId: number | null = null
gridHelper: THREE.GridHelper
lights: THREE.Light[] = []
clock: THREE.Clock
@@ -131,6 +131,10 @@ class Load3d {
currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' =
'original'
originalRotation: THREE.Euler | null = null
+ viewHelper: ViewHelper
+ viewHelperContainer: HTMLDivElement
+ cameraSwitcherContainer: HTMLDivElement
+ gridSwitcherContainer: HTMLDivElement
constructor(container: Element | HTMLElement) {
this.scene = new THREE.Scene()
@@ -157,6 +161,7 @@ class Load3d {
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
this.renderer.setSize(300, 300)
this.renderer.setClearColor(0x282828)
+ this.renderer.autoClear = false
const rendererDomElement: HTMLCanvasElement = this.renderer.domElement
@@ -203,13 +208,143 @@ class Load3d {
this.standardMaterial = this.createSTLMaterial()
- this.animate()
+ this.createViewHelper(container)
+
+ this.createGridSwitcher(container)
+
+ this.createCameraSwitcher(container)
this.handleResize()
this.startAnimation()
}
+ 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
+ }
+
+ createGridSwitcher(container: Element | HTMLElement) {
+ this.gridSwitcherContainer = document.createElement('div')
+ this.gridSwitcherContainer.style.position = 'absolute'
+ this.gridSwitcherContainer.style.top = '28px' // 修改这里,让按钮在相机按钮下方
+ this.gridSwitcherContainer.style.left = '3px' // 与相机按钮左对齐
+ this.gridSwitcherContainer.style.width = '20px'
+ this.gridSwitcherContainer.style.height = '20px'
+ this.gridSwitcherContainer.style.cursor = 'pointer'
+ this.gridSwitcherContainer.style.alignItems = 'center'
+ this.gridSwitcherContainer.style.justifyContent = 'center'
+ this.gridSwitcherContainer.style.transition = 'background-color 0.2s'
+
+ const gridIcon = document.createElement('div')
+ gridIcon.innerHTML = `
+
+ `
+
+ const updateButtonState = () => {
+ if (this.gridHelper.visible) {
+ this.gridSwitcherContainer.style.backgroundColor =
+ 'rgba(255, 255, 255, 0.2)'
+ } else {
+ this.gridSwitcherContainer.style.backgroundColor = 'transparent'
+ }
+ }
+
+ updateButtonState()
+
+ this.gridSwitcherContainer.addEventListener('mouseenter', () => {
+ if (!this.gridHelper.visible) {
+ this.gridSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
+ }
+ })
+
+ this.gridSwitcherContainer.addEventListener('mouseleave', () => {
+ if (!this.gridHelper.visible) {
+ this.gridSwitcherContainer.style.backgroundColor = 'transparent'
+ }
+ })
+
+ this.gridSwitcherContainer.title = 'Toggle Grid'
+
+ this.gridSwitcherContainer.addEventListener('click', (event) => {
+ event.stopPropagation()
+ this.toggleGrid(!this.gridHelper.visible)
+ updateButtonState()
+ })
+
+ this.gridSwitcherContainer.appendChild(gridIcon)
+ container.appendChild(this.gridSwitcherContainer)
+ }
+
+ createCameraSwitcher(container: Element | HTMLElement) {
+ this.cameraSwitcherContainer = document.createElement('div')
+ this.cameraSwitcherContainer.style.position = 'absolute'
+ this.cameraSwitcherContainer.style.top = '3px'
+ this.cameraSwitcherContainer.style.left = '3px'
+ this.cameraSwitcherContainer.style.width = '20px'
+ this.cameraSwitcherContainer.style.height = '20px'
+ this.cameraSwitcherContainer.style.cursor = 'pointer'
+ this.cameraSwitcherContainer.style.alignItems = 'center'
+ this.cameraSwitcherContainer.style.justifyContent = 'center'
+
+ const cameraIcon = document.createElement('div')
+ cameraIcon.innerHTML = `
+
+ `
+ this.cameraSwitcherContainer.addEventListener('mouseenter', () => {
+ this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
+ })
+
+ this.cameraSwitcherContainer.addEventListener('mouseleave', () => {
+ this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
+ })
+
+ this.cameraSwitcherContainer.title =
+ 'Switch Camera (Perspective/Orthographic)'
+
+ this.cameraSwitcherContainer.addEventListener('click', (event) => {
+ event.stopPropagation()
+ this.toggleCamera()
+ })
+
+ this.cameraSwitcherContainer.appendChild(cameraIcon)
+
+ container.appendChild(this.cameraSwitcherContainer)
+ }
+
setFOV(fov: number) {
if (this.activeCamera === this.perspectiveCamera) {
this.perspectiveCamera.fov = fov
@@ -465,6 +600,13 @@ class Load3d {
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()
}
@@ -501,8 +643,16 @@ class Load3d {
startAnimation() {
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
+ const delta = this.clock.getDelta()
+
+ if (this.viewHelper.animating) {
+ this.viewHelper.update(delta)
+ }
+
+ this.renderer.clear()
this.controls.update()
this.renderer.render(this.scene, this.activeCamera)
+ this.viewHelper.render(this.renderer)
}
animate()
}
@@ -588,6 +738,7 @@ class Load3d {
}
this.controls.dispose()
+ this.viewHelper.dispose()
this.renderer.dispose()
this.renderer.domElement.remove()
this.scene.clear()
@@ -818,10 +969,12 @@ class Load3d {
this.orthographicCamera.updateProjectionMatrix()
}
+ this.renderer.clear()
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')
@@ -846,44 +999,6 @@ class Load3d {
})
}
- setViewPosition(position: 'front' | 'top' | 'right' | 'isometric') {
- if (!this.currentModel) {
- return
- }
-
- const box = new THREE.Box3()
- let center = new THREE.Vector3()
- let size = new THREE.Vector3()
-
- if (this.currentModel) {
- box.setFromObject(this.currentModel)
- box.getCenter(center)
- box.getSize(size)
- }
-
- const maxDim = Math.max(size.x, size.y, size.z)
- const distance = maxDim * 2
-
- switch (position) {
- case 'front':
- this.activeCamera.position.set(0, 0, distance)
- break
- case 'top':
- this.activeCamera.position.set(0, distance, 0)
- break
- case 'right':
- this.activeCamera.position.set(distance, 0, 0)
- break
- case 'isometric':
- this.activeCamera.position.set(distance, distance, distance)
- break
- }
-
- this.activeCamera.lookAt(center)
- this.controls.target.copy(center)
- this.controls.update()
- }
-
setBackgroundColor(color: string) {
this.renderer.setClearColor(new THREE.Color(color))
this.renderer.render(this.scene, this.activeCamera)
@@ -1020,16 +1135,28 @@ class Load3dAnimation extends Load3d {
})
}
- animate = () => {
- requestAnimationFrame(this.animate)
-
- if (this.currentAnimation && this.isAnimationPlaying) {
+ startAnimation() {
+ const animate = () => {
+ this.animationFrameId = requestAnimationFrame(animate)
const delta = this.clock.getDelta()
- this.currentAnimation.update(delta)
- }
- this.controls.update()
- this.renderer.render(this.scene, this.activeCamera)
+ if (this.currentAnimation && this.isAnimationPlaying) {
+ this.currentAnimation.update(delta)
+ }
+
+ this.controls.update()
+
+ this.renderer.clear()
+
+ this.renderer.render(this.scene, this.activeCamera)
+
+ if (this.viewHelper.animating) {
+ this.viewHelper.update(delta)
+ }
+
+ this.viewHelper.render(this.renderer)
+ }
+ animate()
}
}
@@ -1076,9 +1203,6 @@ function configureLoad3D(
load3d: Load3d,
loadFolder: 'input' | 'output',
modelWidget: IWidget,
- showGrid: IWidget,
- cameraType: IWidget,
- view: IWidget,
material: IWidget,
bgColor: IWidget,
lightIntensity: IWidget,
@@ -1138,22 +1262,6 @@ function configureLoad3D(
modelWidget.callback = onModelWidgetUpdate
- load3d.toggleGrid(showGrid.value as boolean)
-
- showGrid.callback = (value: boolean) => {
- load3d.toggleGrid(value)
- }
-
- load3d.toggleCamera(cameraType.value as 'perspective' | 'orthographic')
-
- cameraType.callback = (value: 'perspective' | 'orthographic') => {
- load3d.toggleCamera(value)
- }
-
- view.callback = (value: 'front' | 'top' | 'right' | 'isometric') => {
- load3d.setViewPosition(value)
- }
-
material.callback = (value: 'original' | 'normal' | 'wireframe') => {
load3d.setMaterialMode(value)
}
@@ -1312,14 +1420,6 @@ app.registerExtension({
(w: IWidget) => w.name === 'model_file'
)
- const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
-
- const cameraType = node.widgets.find(
- (w: IWidget) => w.name === 'camera_type'
- )
-
- const view = node.widgets.find((w: IWidget) => w.name === 'view')
-
const material = node.widgets.find((w: IWidget) => w.name === 'material')
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
@@ -1353,9 +1453,6 @@ app.registerExtension({
load3d,
'input',
modelWidget,
- showGrid,
- cameraType,
- view,
material,
bgColor,
lightIntensity,
@@ -1569,14 +1666,6 @@ app.registerExtension({
(w: IWidget) => w.name === 'model_file'
)
- const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
-
- const cameraType = node.widgets.find(
- (w: IWidget) => w.name === 'camera_type'
- )
-
- const view = node.widgets.find((w: IWidget) => w.name === 'view')
-
const material = node.widgets.find((w: IWidget) => w.name === 'material')
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
@@ -1621,9 +1710,6 @@ app.registerExtension({
load3d,
'input',
modelWidget,
- showGrid,
- cameraType,
- view,
material,
bgColor,
lightIntensity,
@@ -1652,6 +1738,8 @@ app.registerExtension({
sceneWidget.serializeValue = async () => {
node.properties['Camera Info'] = JSON.stringify(load3d.getCameraState())
+ load3d.toggleAnimation(false)
+
const { scene: imageData, mask: maskData } = await load3d.captureScene(
w.value,
h.value
@@ -1758,14 +1846,6 @@ app.registerExtension({
(w: IWidget) => w.name === 'model_file'
)
- const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
-
- const cameraType = node.widgets.find(
- (w: IWidget) => w.name === 'camera_type'
- )
-
- const view = node.widgets.find((w: IWidget) => w.name === 'view')
-
const material = node.widgets.find((w: IWidget) => w.name === 'material')
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
@@ -1801,9 +1881,6 @@ app.registerExtension({
load3d,
'output',
modelWidget,
- showGrid,
- cameraType,
- view,
material,
bgColor,
lightIntensity,