[refactor] refactor load3d (#5765)

Summary

Fully Refactored the Load3D module to improve architecture and
maintainability by consolidating functionality into a
centralized composable pattern and simplifying component structure. and
support VueNodes system

  Changes

- Architecture: Introduced new useLoad3d composable to centralize 3D
loading logic and state
  management
- Component Simplification: Removed redundant components
(Load3DAnimation.vue, Load3DAnimationScene.vue,
  PreviewManager.ts) 
- Support VueNodes
- improve config store
- remove lineart output due Animation doesnot support it, may add it
back later
- remove Preview screen and keep scene in fixed ratio in load3d (not
affect preview3d)
- improve record video feature which will already record video by same
ratio as scene
Need BE change https://github.com/comfyanonymous/ComfyUI/pull/10025


https://github.com/user-attachments/assets/9e038729-84a0-45ad-b0f2-11c57d7e0c9a



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5765-refactor-refactor-load3d-2796d73d365081728297cc486e2e9052)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2025-10-31 16:19:35 -04:00
committed by GitHub
parent 91b5a7de17
commit afa10f7a1e
51 changed files with 2784 additions and 4200 deletions

View File

@@ -1,11 +1,10 @@
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import Load3DAnimation from '@/components/load3d/Load3DAnimation.vue'
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
import { nodeToLoad3dMap, useLoad3d } from '@/composables/useLoad3d'
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
@@ -21,6 +20,18 @@ import { useLoad3dService } from '@/services/load3dService'
import { useDialogStore } from '@/stores/dialogStore'
import { isLoad3dNode } from '@/utils/litegraphUtil'
const inputSpecLoad3D: CustomInputSpec = {
name: 'image',
type: 'Load3D',
isPreview: false
}
const inputSpecPreview3D: CustomInputSpec = {
name: 'image',
type: 'Preview3D',
isPreview: true
}
async function handleModelUpload(files: FileList, node: any) {
if (!files?.length) return
@@ -49,7 +60,13 @@ async function handleModelUpload(files: FileList, node: any) {
)
)
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
useLoad3d(node).waitForLoad3d((load3d) => {
try {
load3d.loadModel(modelUrl)
} catch (error) {
useToastStore().addAlert(t('toastMessages.failedToLoadModel'))
}
})
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
@@ -106,16 +123,6 @@ useExtensionService().registerExtension({
defaultValue: true,
experimental: true
},
{
id: 'Comfy.Load3D.ShowPreview',
category: ['3D', 'Scene', 'Initial Preview Visibility'],
name: 'Initial Preview Visibility',
tooltip:
'Controls whether the preview screen is visible by default when a new 3D widget is created. This default can still be toggled individually for each widget after creation.',
type: 'boolean',
defaultValue: true,
experimental: true
},
{
id: 'Comfy.Load3D.BackgroundColor',
category: ['3D', 'Scene', 'Initial Background Color'],
@@ -260,7 +267,9 @@ useExtensionService().registerExtension({
)
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()
useLoad3d(node).waitForLoad3d((load3d) => {
load3d.clearModel()
})
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
if (modelWidget) {
@@ -268,21 +277,16 @@ useExtensionService().registerExtension({
}
})
const inputSpec: CustomInputSpec = {
name: 'image',
type: 'Load3D',
isAnimation: false,
isPreview: false
}
const widget = new ComponentWidgetImpl({
node,
name: inputSpec.name,
node: node,
name: 'image',
component: Load3D,
inputSpec,
inputSpec: inputSpecLoad3D,
options: {}
})
widget.type = 'load3D'
addWidget(node, widget)
return { widget }
@@ -309,8 +313,9 @@ useExtensionService().registerExtension({
await nextTick()
useLoad3dService().waitForLoad3d(node, (load3d) => {
let cameraState = node.properties['Camera Info']
useLoad3d(node).waitForLoad3d((load3d) => {
const cameraConfig = node.properties['Camera Config'] as any
const cameraState = cameraConfig?.state
const config = new Load3DConfiguration(load3d)
@@ -320,159 +325,36 @@ useExtensionService().registerExtension({
const sceneWidget = node.widgets?.find((w) => w.name === 'image')
if (modelWidget && width && height && sceneWidget) {
config.configure('input', modelWidget, cameraState, width, height)
const settings = {
loadFolder: 'input',
modelWidget: modelWidget,
cameraState: cameraState,
width: width,
height: height
}
config.configure(settings)
sceneWidget.serializeValue = async () => {
node.properties['Camera Info'] = load3d.getCameraState()
load3d.stopRecording()
const {
scene: imageData,
mask: maskData,
normal: normalData,
lineart: lineartData
} = await load3d.captureScene(
width.value as number,
height.value as number
)
const [data, dataMask, dataNormal, dataLineart] = await Promise.all([
Load3dUtils.uploadTempImage(imageData, 'scene'),
Load3dUtils.uploadTempImage(maskData, 'scene_mask'),
Load3dUtils.uploadTempImage(normalData, 'scene_normal'),
Load3dUtils.uploadTempImage(lineartData, 'scene_lineart')
])
load3d.handleResize()
const returnVal = {
image: `threed/${data.name} [temp]`,
mask: `threed/${dataMask.name} [temp]`,
normal: `threed/${dataNormal.name} [temp]`,
lineart: `threed/${dataLineart.name} [temp]`,
camera_info: node.properties['Camera Info'],
recording: ''
const currentLoad3d = nodeToLoad3dMap.get(node)
if (!currentLoad3d) {
console.error('No load3d instance found for node')
return null
}
const recordingData = load3d.getRecordingData()
if (recordingData) {
const [recording] = await Promise.all([
Load3dUtils.uploadTempImage(recordingData, 'recording', 'mp4')
])
returnVal['recording'] = `threed/${recording.name} [temp]`
const cameraConfig = (node.properties['Camera Config'] as any) || {
cameraType: currentLoad3d.getCurrentCameraType(),
fov: currentLoad3d.cameraManager.perspectiveCamera.fov
}
cameraConfig.state = currentLoad3d.getCameraState()
node.properties['Camera Config'] = cameraConfig
return returnVal
}
}
})
}
})
useExtensionService().registerExtension({
name: 'Comfy.Load3DAnimation',
getCustomWidgets() {
return {
LOAD_3D_ANIMATION(node) {
const fileInput = createFileInput('.gltf,.glb,.fbx', false)
node.properties['Resource Folder'] = ''
fileInput.onchange = async () => {
await handleModelUpload(fileInput.files!, node)
}
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
fileInput.click()
})
const resourcesInput = createFileInput('*', true)
resourcesInput.onchange = async () => {
await handleResourcesUpload(resourcesInput.files!, node)
resourcesInput.value = ''
}
node.addWidget(
'button',
'upload extra resources',
'uploadExtraResources',
() => {
resourcesInput.click()
}
)
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
if (modelWidget) {
modelWidget.value = ''
}
})
const inputSpec: CustomInputSpec = {
name: 'image',
type: 'Load3DAnimation',
isAnimation: true,
isPreview: false
}
const widget = new ComponentWidgetImpl({
node,
name: inputSpec.name,
component: Load3DAnimation,
inputSpec,
options: {}
})
addWidget(node, widget)
return { widget }
}
}
},
async nodeCreated(node) {
if (node.constructor.comfyClass !== 'Load3DAnimation') return
const [oldWidth, oldHeight] = node.size
node.setSize([Math.max(oldWidth, 400), Math.max(oldHeight, 700)])
await nextTick()
useLoad3dService().waitForLoad3d(node, (load3d) => {
const sceneWidget = node.widgets?.find((w) => w.name === 'image')
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
let cameraState = node.properties['Camera Info']
const width = node.widgets?.find((w) => w.name === 'width')
const height = node.widgets?.find((w) => w.name === 'height')
if (modelWidget && width && height && sceneWidget && load3d) {
const config = new Load3DConfiguration(load3d)
config.configure('input', modelWidget, cameraState, width, height)
sceneWidget.serializeValue = async () => {
node.properties['Camera Info'] = load3d.getCameraState()
const load3dAnimation = load3d as Load3dAnimation
load3dAnimation.toggleAnimation(false)
if (load3dAnimation.isRecording()) {
load3dAnimation.stopRecording()
}
currentLoad3d.stopRecording()
const {
scene: imageData,
mask: maskData,
normal: normalData
} = await load3dAnimation.captureScene(
} = await currentLoad3d.captureScene(
width.value as number,
height.value as number
)
@@ -483,17 +365,19 @@ useExtensionService().registerExtension({
Load3dUtils.uploadTempImage(normalData, 'scene_normal')
])
load3dAnimation.handleResize()
currentLoad3d.handleResize()
const returnVal = {
image: `threed/${data.name} [temp]`,
mask: `threed/${dataMask.name} [temp]`,
normal: `threed/${dataNormal.name} [temp]`,
camera_info: node.properties['Camera Info'],
camera_info:
(node.properties['Camera Config'] as any)?.state || null,
recording: ''
}
const recordingData = load3dAnimation.getRecordingData()
const recordingData = currentLoad3d.getRecordingData()
if (recordingData) {
const [recording] = await Promise.all([
Load3dUtils.uploadTempImage(recordingData, 'recording', 'mp4')
@@ -531,21 +415,16 @@ useExtensionService().registerExtension({
getCustomWidgets() {
return {
PREVIEW_3D(node) {
const inputSpec: CustomInputSpec = {
name: 'image',
type: 'Preview3D',
isAnimation: false,
isPreview: true
}
const widget = new ComponentWidgetImpl({
node,
name: inputSpec.name,
name: inputSpecPreview3D.name,
component: Load3D,
inputSpec,
inputSpec: inputSpecPreview3D,
options: {}
})
widget.type = 'load3D'
addWidget(node, widget)
return { widget }
@@ -564,7 +443,7 @@ useExtensionService().registerExtension({
const onExecuted = node.onExecuted
useLoad3dService().waitForLoad3d(node, (load3d) => {
useLoad3d(node).waitForLoad3d((load3d) => {
const config = new Load3DConfiguration(load3d)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
@@ -575,9 +454,16 @@ useExtensionService().registerExtension({
if (lastTimeModelFile) {
modelWidget.value = lastTimeModelFile
const cameraState = node.properties['Camera Info']
const cameraConfig = node.properties['Camera Config'] as any
const cameraState = cameraConfig?.state
config.configure('output', modelWidget, cameraState)
const settings = {
loadFolder: 'output',
modelWidget: modelWidget,
cameraState: cameraState
}
config.configure(settings)
}
node.onExecuted = function (message: any) {
@@ -592,98 +478,24 @@ useExtensionService().registerExtension({
}
let cameraState = message.result[1]
let bgImagePath = message.result[2]
modelWidget.value = filePath.replaceAll('\\', '/')
node.properties['Last Time Model File'] = modelWidget.value
config.configure('output', modelWidget, cameraState)
}
}
})
}
})
useExtensionService().registerExtension({
name: 'Comfy.Preview3DAnimation',
async beforeRegisterNodeDef(_nodeType, nodeData) {
if ('Preview3DAnimation' === nodeData.name) {
// @ts-expect-error InputSpec is not typed correctly
nodeData.input.required.image = ['PREVIEW_3D_ANIMATION']
}
},
getCustomWidgets() {
return {
PREVIEW_3D_ANIMATION(node) {
const inputSpec: CustomInputSpec = {
name: 'image',
type: 'Preview3DAnimation',
isAnimation: true,
isPreview: true
}
const widget = new ComponentWidgetImpl({
node,
name: inputSpec.name,
component: Load3DAnimation,
inputSpec,
options: {}
})
addWidget(node, widget)
return { widget }
}
}
},
async nodeCreated(node) {
if (node.constructor.comfyClass !== 'Preview3DAnimation') return
const [oldWidth, oldHeight] = node.size
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 550)])
await nextTick()
const onExecuted = node.onExecuted
useLoad3dService().waitForLoad3d(node, (load3d) => {
const config = new Load3DConfiguration(load3d)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
if (modelWidget) {
const lastTimeModelFile = node.properties['Last Time Model File']
if (lastTimeModelFile) {
modelWidget.value = lastTimeModelFile
const cameraState = node.properties['Camera Info']
config.configure('output', modelWidget, cameraState)
}
node.onExecuted = function (message: any) {
onExecuted?.apply(this, arguments as any)
let filePath = message.result[0]
if (!filePath) {
const msg = t('toastMessages.unableToGetModelFilePath')
console.error(msg)
useToastStore().addAlert(msg)
const settings = {
loadFolder: 'output',
modelWidget: modelWidget,
cameraState: cameraState,
bgImagePath: bgImagePath
}
let cameraState = message.result[1]
config.configure(settings)
modelWidget.value = filePath.replaceAll('\\', '/')
node.properties['Last Time Model File'] = modelWidget.value
config.configure('output', modelWidget, cameraState)
if (bgImagePath) {
load3d.setBackgroundImage(bgImagePath)
}
}
}
})

View File

@@ -15,14 +15,9 @@ export class AnimationManager implements AnimationManagerInterface {
animationSpeed: number = 1.0
private eventManager: EventManagerInterface
private getCurrentModel: () => THREE.Object3D | null
constructor(
eventManager: EventManagerInterface,
getCurrentModel: () => THREE.Object3D | null
) {
constructor(eventManager: EventManagerInterface) {
this.eventManager = eventManager
this.getCurrentModel = getCurrentModel
}
init(): void {}
@@ -52,23 +47,24 @@ export class AnimationManager implements AnimationManagerInterface {
let animations: THREE.AnimationClip[] = []
if (model.animations?.length > 0) {
animations = model.animations
} else if (originalModel && 'animations' in originalModel) {
} else if (
originalModel &&
'animations' in originalModel &&
Array.isArray(originalModel.animations)
) {
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()!
)
}
this.currentAnimation = new THREE.AnimationMixer(model)
if (this.animationClips.length > 0) {
this.updateSelectedAnimation(0)
}
} else {
this.animationClips = []
}
this.updateAnimationList()

View File

@@ -82,7 +82,17 @@ export class CameraManager implements CameraManagerInterface {
if (this.controls) {
this.controls.addEventListener('end', () => {
this.nodeStorage.storeNodeProperty('Camera Info', this.getCameraState())
const cameraState = this.getCameraState()
const cameraConfig = this.nodeStorage.loadNodeProperty(
'Camera Config',
{
cameraType: this.getCurrentCameraType(),
fov: this.perspectiveCamera.fov
}
)
cameraConfig.state = cameraState
this.nodeStorage.storeNodeProperty('Camera Config', cameraConfig)
})
}
}

View File

@@ -24,13 +24,14 @@ export class ControlsManager implements ControlsManagerInterface {
this.nodeStorage = nodeStorage
this.camera = camera
this.controls = new OrbitControls(camera, renderer.domElement)
const container = renderer.domElement.parentElement || renderer.domElement
this.controls = new OrbitControls(camera, container)
this.controls.enableDamping = true
}
init(): void {
this.controls.addEventListener('end', () => {
this.nodeStorage.storeNodeProperty('Camera Info', {
const cameraState = {
position: this.camera.position.clone(),
target: this.controls.target.clone(),
zoom:
@@ -41,7 +42,17 @@ export class ControlsManager implements ControlsManagerInterface {
this.camera instanceof THREE.PerspectiveCamera
? 'perspective'
: 'orthographic'
}
const cameraConfig = this.nodeStorage.loadNodeProperty('Camera Config', {
cameraType: cameraState.cameraType,
fov:
this.camera instanceof THREE.PerspectiveCamera
? (this.camera as THREE.PerspectiveCamera).fov
: 75
})
cameraConfig.state = cameraState
this.nodeStorage.storeNodeProperty('Camera Config', cameraConfig)
})
}

View File

@@ -7,6 +7,7 @@ import {
export class LightingManager implements LightingManagerInterface {
lights: THREE.Light[] = []
currentIntensity: number = 3
private scene: THREE.Scene
private eventManager: EventManagerInterface
@@ -58,6 +59,7 @@ export class LightingManager implements LightingManagerInterface {
}
setLightIntensity(intensity: number): void {
this.currentIntensity = intensity
this.lights.forEach((light) => {
if (light instanceof THREE.DirectionalLight) {
if (light === this.lights[1]) {

View File

@@ -1,9 +1,24 @@
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import type {
CameraConfig,
LightConfig,
ModelConfig,
SceneConfig
} from '@/extensions/core/load3d/interfaces'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
import { api } from '@/scripts/api'
type Load3DConfigurationSettings = {
loadFolder: string
modelWidget: IBaseWidget
cameraState?: any
width?: IBaseWidget
height?: IBaseWidget
bgImagePath?: string
}
class Load3DConfiguration {
constructor(private load3d: Load3d) {}
@@ -12,22 +27,17 @@ class Load3DConfiguration {
this.setupDefaultProperties()
}
configure(
loadFolder: 'input' | 'output',
modelWidget: IBaseWidget,
cameraState?: any,
width: IBaseWidget | null = null,
height: IBaseWidget | null = null
) {
this.setupModelHandling(modelWidget, loadFolder, cameraState)
this.setupTargetSize(width, height)
this.setupDefaultProperties()
configure(setting: Load3DConfigurationSettings) {
this.setupModelHandling(
setting.modelWidget,
setting.loadFolder,
setting.cameraState
)
this.setupTargetSize(setting.width, setting.height)
this.setupDefaultProperties(setting.bgImagePath)
}
private setupTargetSize(
width: IBaseWidget | null,
height: IBaseWidget | null
) {
private setupTargetSize(width?: IBaseWidget, height?: IBaseWidget) {
if (width && height) {
this.load3d.setTargetSize(width.value as number, height.value as number)
@@ -41,10 +51,7 @@ class Load3DConfiguration {
}
}
private setupModelHandlingForSaveMesh(
filePath: string,
loadFolder: 'input' | 'output'
) {
private setupModelHandlingForSaveMesh(filePath: string, loadFolder: string) {
const onModelWidgetUpdate = this.createModelUpdateHandler(loadFolder)
if (filePath) {
@@ -54,7 +61,7 @@ class Load3DConfiguration {
private setupModelHandling(
modelWidget: IBaseWidget,
loadFolder: 'input' | 'output',
loadFolder: string,
cameraState?: any
) {
const onModelWidgetUpdate = this.createModelUpdateHandler(
@@ -65,63 +72,119 @@ class Load3DConfiguration {
onModelWidgetUpdate(modelWidget.value)
}
modelWidget.callback = (value: string | number | boolean | object) => {
this.load3d.node.properties['Texture'] = undefined
const originalCallback = modelWidget.callback
let currentValue = modelWidget.value
Object.defineProperty(modelWidget, 'value', {
get() {
return currentValue
},
set(newValue) {
currentValue = newValue
if (modelWidget.callback && newValue !== undefined && newValue !== '') {
modelWidget.callback(newValue)
}
},
enumerable: true,
configurable: true
})
modelWidget.callback = (value: string | number | boolean | object) => {
onModelWidgetUpdate(value)
if (originalCallback) {
originalCallback(value)
}
}
}
private setupDefaultProperties() {
const cameraType = this.load3d.loadNodeProperty(
'Camera Type',
useSettingStore().get('Comfy.Load3D.CameraType')
)
this.load3d.toggleCamera(cameraType)
private setupDefaultProperties(bgImagePath?: string) {
const sceneConfig = this.loadSceneConfig()
this.applySceneConfig(sceneConfig, bgImagePath)
const showGrid = this.load3d.loadNodeProperty(
'Show Grid',
useSettingStore().get('Comfy.Load3D.ShowGrid')
)
const cameraConfig = this.loadCameraConfig()
this.applyCameraConfig(cameraConfig)
this.load3d.toggleGrid(showGrid)
const showPreview = this.load3d.loadNodeProperty(
'Show Preview',
useSettingStore().get('Comfy.Load3D.ShowPreview')
)
this.load3d.togglePreview(showPreview)
const bgColor = this.load3d.loadNodeProperty(
'Background Color',
'#' + useSettingStore().get('Comfy.Load3D.BackgroundColor')
)
this.load3d.setBackgroundColor(bgColor)
const lightIntensity: number = Number(
this.load3d.loadNodeProperty(
'Light Intensity',
useSettingStore().get('Comfy.Load3D.LightIntensity')
)
)
this.load3d.setLightIntensity(lightIntensity)
const fov: number = Number(this.load3d.loadNodeProperty('FOV', 35))
this.load3d.setFOV(fov)
const backgroundImage = this.load3d.loadNodeProperty('Background Image', '')
this.load3d.setBackgroundImage(backgroundImage)
const lightConfig = this.loadLightConfig()
this.applyLightConfig(lightConfig)
}
private createModelUpdateHandler(
loadFolder: 'input' | 'output',
cameraState?: any
) {
private loadSceneConfig(): SceneConfig {
const defaultConfig: SceneConfig = {
showGrid: useSettingStore().get('Comfy.Load3D.ShowGrid'),
backgroundColor:
'#' + useSettingStore().get('Comfy.Load3D.BackgroundColor'),
backgroundImage: ''
}
const config = this.load3d.loadNodeProperty('Scene Config', defaultConfig)
this.load3d.node.properties['Scene Config'] = config
return config
}
private loadCameraConfig(): CameraConfig {
const defaultConfig: CameraConfig = {
cameraType: useSettingStore().get('Comfy.Load3D.CameraType'),
fov: 35
}
const config = this.load3d.loadNodeProperty('Camera Config', defaultConfig)
this.load3d.node.properties['Camera Config'] = config
return config
}
private loadLightConfig(): LightConfig {
const defaultConfig: LightConfig = {
intensity: useSettingStore().get('Comfy.Load3D.LightIntensity')
}
const config = this.load3d.loadNodeProperty('Light Config', defaultConfig)
this.load3d.node.properties['Light Config'] = config
return config
}
private loadModelConfig(): ModelConfig {
const defaultConfig: ModelConfig = {
upDirection: 'original',
materialMode: 'original'
}
const config = this.load3d.loadNodeProperty('Model Config', defaultConfig)
this.load3d.node.properties['Model Config'] = config
return config
}
private applySceneConfig(config: SceneConfig, bgImagePath?: string) {
this.load3d.toggleGrid(config.showGrid)
this.load3d.setBackgroundColor(config.backgroundColor)
if (config.backgroundImage) {
if (bgImagePath && bgImagePath != config.backgroundImage) {
return
}
this.load3d.setBackgroundImage(config.backgroundImage)
}
}
private applyCameraConfig(config: CameraConfig) {
this.load3d.toggleCamera(config.cameraType)
this.load3d.setFOV(config.fov)
if (config.state) {
this.load3d.setCameraState(config.state)
}
}
private applyLightConfig(config: LightConfig) {
this.load3d.setLightIntensity(config.intensity)
}
private applyModelConfig(config: ModelConfig) {
this.load3d.setUpDirection(config.upDirection)
this.load3d.setMaterialMode(config.materialMode)
}
private createModelUpdateHandler(loadFolder: string, cameraState?: any) {
let isFirstLoad = true
return async (value: string | number | boolean | object) => {
if (!value) return
@@ -139,25 +202,8 @@ class Load3DConfiguration {
await this.load3d.loadModel(modelUrl, filename)
const upDirection = this.load3d.loadNodeProperty(
'Up Direction',
'original'
)
this.load3d.setUpDirection(upDirection)
const materialMode = this.load3d.loadNodeProperty(
'Material Mode',
'original'
)
this.load3d.setMaterialMode(materialMode)
const edgeThreshold: number = Number(
this.load3d.loadNodeProperty('Edge Threshold', 85)
)
this.load3d.setEdgeThreshold(edgeThreshold)
const modelConfig = this.loadModelConfig()
this.applyModelConfig(modelConfig)
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
try {

View File

@@ -1,8 +1,8 @@
import * as THREE from 'three'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { AnimationManager } from './AnimationManager'
import { CameraManager } from './CameraManager'
import { ControlsManager } from './ControlsManager'
import { EventManager } from './EventManager'
@@ -10,7 +10,6 @@ import { LightingManager } from './LightingManager'
import { LoaderManager } from './LoaderManager'
import { ModelExporter } from './ModelExporter'
import { NodeStorage } from './NodeStorage'
import { PreviewManager } from './PreviewManager'
import { RecordingManager } from './RecordingManager'
import { SceneManager } from './SceneManager'
import { SceneModelManager } from './SceneModelManager'
@@ -29,6 +28,7 @@ class Load3d {
protected clock: THREE.Clock
protected animationFrameId: number | null = null
node: LGraphNode
private loadingPromise: Promise<void> | null = null
eventManager: EventManager
nodeStorage: NodeStorage
@@ -37,10 +37,10 @@ class Load3d {
controlsManager: ControlsManager
lightingManager: LightingManager
viewHelperManager: ViewHelperManager
previewManager: PreviewManager
loaderManager: LoaderManager
modelManager: SceneModelManager
recordingManager: RecordingManager
animationManager: AnimationManager
STATUS_MOUSE_ON_NODE: boolean
STATUS_MOUSE_ON_SCENE: boolean
@@ -62,8 +62,7 @@ class Load3d {
constructor(
container: Element | HTMLElement,
options: Load3DOptions = {
node: {} as LGraphNode,
inputSpec: {} as CustomInputSpec
node: {} as LGraphNode
}
) {
this.node = options.node || ({} as LGraphNode)
@@ -124,27 +123,12 @@ class Load3d {
this.nodeStorage
)
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
)
if (options.disablePreview) {
this.previewManager.togglePreview(false)
}
this.modelManager = new SceneModelManager(
this.sceneManager.scene,
this.renderer,
this.eventManager,
this.getActiveCamera.bind(this),
this.setupCamera.bind(this),
options
this.setupCamera.bind(this)
)
this.loaderManager = new LoaderManager(this.modelManager, this.eventManager)
@@ -154,21 +138,18 @@ class Load3d {
this.renderer,
this.eventManager
)
this.animationManager = new AnimationManager(this.eventManager)
this.sceneManager.init()
this.cameraManager.init()
this.controlsManager.init()
this.lightingManager.init()
this.loaderManager.init()
this.loaderManager.init()
this.animationManager.init()
this.viewHelperManager.createViewHelper(container)
this.viewHelperManager.init()
if (options && !options.inputSpec?.isPreview) {
this.previewManager.createCapturePreview(container)
this.previewManager.init()
}
this.STATUS_MOUSE_ON_NODE = false
this.STATUS_MOUSE_ON_SCENE = false
this.STATUS_MOUSE_ON_VIEWER = false
@@ -253,9 +234,6 @@ class Load3d {
return this.eventManager
}
getNodeStorage(): NodeStorage {
return this.nodeStorage
}
getSceneManager(): SceneManager {
return this.sceneManager
}
@@ -271,9 +249,6 @@ class Load3d {
getViewHelperManager(): ViewHelperManager {
return this.viewHelperManager
}
getPreviewManager(): PreviewManager {
return this.previewManager
}
getLoaderManager(): LoaderManager {
return this.loaderManager
}
@@ -286,15 +261,12 @@ class Load3d {
forceRender(): void {
const delta = this.clock.getDelta()
this.animationManager.update(delta)
this.viewHelperManager.update(delta)
this.controlsManager.update()
this.renderMainScene()
if (this.previewManager.showPreview) {
this.previewManager.renderPreview()
}
this.resetViewport()
if (this.viewHelperManager.viewHelper.render) {
@@ -308,7 +280,18 @@ class Load3d {
const containerWidth = this.renderer.domElement.clientWidth
const containerHeight = this.renderer.domElement.clientHeight
if (this.isViewerMode) {
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
const shouldMaintainAspectRatio =
(widthWidget && heightWidget) || this.isViewerMode
if (shouldMaintainAspectRatio) {
if (widthWidget && heightWidget) {
this.targetWidth = widthWidget.value as number
this.targetHeight = heightWidget.value as number
this.targetAspectRatio = this.targetWidth / this.targetHeight
}
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
@@ -338,6 +321,7 @@ class Load3d {
const renderAspectRatio = renderWidth / renderHeight
this.cameraManager.updateAspectRatio(renderAspectRatio)
} else {
// Preview3D: fill the entire container
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
this.renderer.setScissor(0, 0, containerWidth, containerHeight)
this.renderer.setScissorTest(true)
@@ -380,15 +364,12 @@ class Load3d {
}
const delta = this.clock.getDelta()
this.animationManager.update(delta)
this.viewHelperManager.update(delta)
this.controlsManager.update()
this.renderMainScene()
if (this.previewManager.showPreview) {
this.previewManager.renderPreview()
}
this.resetViewport()
if (this.viewHelperManager.viewHelper.render) {
@@ -465,44 +446,54 @@ class Load3d {
setBackgroundColor(color: string): void {
this.sceneManager.setBackgroundColor(color)
this.previewManager.setPreviewBackgroundColor(color)
this.forceRender()
}
async setBackgroundImage(uploadPath: string): Promise<void> {
await this.sceneManager.setBackgroundImage(uploadPath)
this.previewManager.updateBackgroundTexture(
this.sceneManager.backgroundTexture
)
if (
this.isViewerMode &&
this.sceneManager.backgroundTexture &&
this.sceneManager.backgroundMesh
) {
const containerWidth = this.renderer.domElement.clientWidth
const containerHeight = this.renderer.domElement.clientHeight
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
let renderHeight: number
// Calculate the actual render area based on target aspect ratio
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
const shouldMaintainAspectRatio =
(widthWidget && heightWidget) || this.isViewerMode
if (containerAspectRatio > this.targetAspectRatio) {
renderHeight = containerHeight
renderWidth = renderHeight * this.targetAspectRatio
if (shouldMaintainAspectRatio) {
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
let renderHeight: number
if (containerAspectRatio > this.targetAspectRatio) {
renderHeight = containerHeight
renderWidth = renderHeight * this.targetAspectRatio
} else {
renderWidth = containerWidth
renderHeight = renderWidth / this.targetAspectRatio
}
this.sceneManager.updateBackgroundSize(
this.sceneManager.backgroundTexture,
this.sceneManager.backgroundMesh,
renderWidth,
renderHeight
)
} else {
renderWidth = containerWidth
renderHeight = renderWidth / this.targetAspectRatio
// For Preview3D mode without aspect ratio constraints
this.sceneManager.updateBackgroundSize(
this.sceneManager.backgroundTexture,
this.sceneManager.backgroundMesh,
containerWidth,
containerHeight
)
}
this.sceneManager.updateBackgroundSize(
this.sceneManager.backgroundTexture,
this.sceneManager.backgroundMesh,
renderWidth,
renderHeight
)
}
this.forceRender()
@@ -511,10 +502,6 @@ class Load3d {
removeBackgroundImage(): void {
this.sceneManager.removeBackgroundImage()
this.previewManager.setPreviewBackgroundColor(
this.sceneManager.currentBackgroundColor
)
this.forceRender()
}
@@ -556,28 +543,49 @@ class Load3d {
this.forceRender()
}
setEdgeThreshold(threshold: number): void {
this.modelManager.setEdgeThreshold(threshold)
this.forceRender()
}
setMaterialMode(mode: MaterialMode): void {
this.modelManager.setMaterialMode(mode)
this.forceRender()
}
async loadModel(url: string, originalFileName?: string): Promise<void> {
if (this.loadingPromise) {
try {
await this.loadingPromise
} catch (e) {}
}
this.loadingPromise = this._loadModelInternal(url, originalFileName)
return this.loadingPromise
}
private async _loadModelInternal(
url: string,
originalFileName?: string
): Promise<void> {
this.cameraManager.reset()
this.controlsManager.reset()
this.modelManager.reset()
this.modelManager.clearModel()
this.animationManager.dispose()
await this.loaderManager.loadModel(url, originalFileName)
// Auto-detect and setup animations if present
if (this.modelManager.currentModel) {
this.animationManager.setupModelAnimations(
this.modelManager.currentModel,
this.modelManager.originalModel
)
}
this.handleResize()
this.forceRender()
this.loadingPromise = null
}
clearModel(): void {
this.animationManager.dispose()
this.modelManager.clearModel()
this.forceRender()
}
@@ -592,16 +600,10 @@ class Load3d {
this.forceRender()
}
togglePreview(showPreview: boolean): void {
this.previewManager.togglePreview(showPreview)
this.forceRender()
}
setTargetSize(width: number, height: number): void {
this.targetWidth = width
this.targetHeight = height
this.targetAspectRatio = width / height
this.previewManager.setTargetSize(width, height)
this.forceRender()
}
@@ -619,7 +621,7 @@ class Load3d {
}
handleResize(): void {
const parentElement = this.renderer?.domElement?.parentElement
const parentElement = this.renderer?.domElement
if (!parentElement) {
console.warn('Parent element not found')
@@ -629,7 +631,20 @@ class Load3d {
const containerWidth = parentElement.clientWidth
const containerHeight = parentElement.clientHeight
if (this.isViewerMode) {
// Check if we have width/height widgets (Load3D nodes) or if it's viewer mode
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
const shouldMaintainAspectRatio =
(widthWidget && heightWidget) || this.isViewerMode
if (shouldMaintainAspectRatio) {
// Load3D or viewer mode: maintain aspect ratio
if (widthWidget && heightWidget) {
this.targetWidth = widthWidget.value as number
this.targetHeight = heightWidget.value as number
this.targetAspectRatio = this.targetWidth / this.targetHeight
}
const containerAspectRatio = containerWidth / containerHeight
let renderWidth: number
let renderHeight: number
@@ -642,16 +657,16 @@ class Load3d {
renderHeight = renderWidth / this.targetAspectRatio
}
this.renderer.setSize(containerWidth, containerHeight)
this.cameraManager.handleResize(renderWidth, renderHeight)
this.sceneManager.handleResize(renderWidth, renderHeight)
} else {
// Preview3D: use container dimensions directly
this.renderer.setSize(containerWidth, containerHeight)
this.cameraManager.handleResize(containerWidth, containerHeight)
this.sceneManager.handleResize(containerWidth, containerHeight)
}
this.renderer.setSize(containerWidth, containerHeight)
this.previewManager.handleResize()
this.forceRender()
}
@@ -666,7 +681,10 @@ class Load3d {
public async startRecording(): Promise<void> {
this.viewHelperManager.visibleViewHelper(false)
return this.recordingManager.startRecording()
return this.recordingManager.startRecording(
this.targetWidth,
this.targetHeight
)
}
public stopRecording(): void {
@@ -697,6 +715,23 @@ class Load3d {
this.recordingManager.clearRecording()
}
// Animation methods
public setAnimationSpeed(speed: number): void {
this.animationManager.setAnimationSpeed(speed)
}
public updateSelectedAnimation(index: number): void {
this.animationManager.updateSelectedAnimation(index)
}
public toggleAnimation(play?: boolean): void {
this.animationManager.toggleAnimation(play)
}
public hasAnimations(): boolean {
return this.animationManager.animationClips.length > 0
}
public remove(): void {
if (this.contextMenuAbortController) {
this.contextMenuAbortController.abort()
@@ -720,10 +755,10 @@ class Load3d {
this.controlsManager.dispose()
this.lightingManager.dispose()
this.viewHelperManager.dispose()
this.previewManager.dispose()
this.loaderManager.dispose()
this.modelManager.dispose()
this.recordingManager.dispose()
this.animationManager.dispose()
this.renderer.dispose()
this.renderer.domElement.remove()

View File

@@ -1,131 +0,0 @@
import * as THREE from 'three'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { AnimationManager } from './AnimationManager'
import Load3d from './Load3d'
import { type Load3DOptions } from './interfaces'
class Load3dAnimation extends Load3d {
private animationManager: AnimationManager
constructor(
container: Element | HTMLElement,
options: Load3DOptions = {
node: {} as LGraphNode
}
) {
super(container, options)
this.animationManager = new AnimationManager(
this.eventManager,
this.getCurrentModel.bind(this)
)
this.animationManager.init()
this.overrideAnimationLoop()
}
private overrideAnimationLoop(): void {
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId)
}
const animate = () => {
this.animationFrameId = requestAnimationFrame(animate)
if (!this.isActive()) {
return
}
const delta = this.clock.getDelta()
this.animationManager.update(delta)
this.viewHelperManager.update(delta)
this.controlsManager.update()
this.renderMainScene()
if (this.previewManager.showPreview) {
this.previewManager.renderPreview()
}
this.resetViewport()
if (this.viewHelperManager.viewHelper.render) {
this.viewHelperManager.viewHelper.render(this.renderer)
}
}
animate()
}
override async loadModel(
url: string,
originalFileName?: string
): Promise<void> {
await super.loadModel(url, originalFileName)
if (this.modelManager.currentModel) {
this.animationManager.setupModelAnimations(
this.modelManager.currentModel,
this.modelManager.originalModel
)
}
}
override 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
}
override remove(): void {
this.animationManager.dispose()
super.remove()
}
}
export default Load3dAnimation

View File

@@ -1,416 +0,0 @@
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {
type EventManagerInterface,
type PreviewManagerInterface
} from './interfaces'
export class PreviewManager implements PreviewManagerInterface {
previewCamera: THREE.Camera
previewContainer: HTMLDivElement = null!
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
private previewBackgroundColorMaterial: THREE.MeshBasicMaterial | null = null
private currentBackgroundColor: THREE.Color = new THREE.Color(0x282828)
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()
this.initPreviewBackgroundScene()
}
private initPreviewBackgroundScene(): void {
const planeGeometry = new THREE.PlaneGeometry(2, 2)
this.previewBackgroundColorMaterial = new THREE.MeshBasicMaterial({
color: this.currentBackgroundColor.clone(),
transparent: false,
depthWrite: false,
depthTest: false,
side: THREE.DoubleSide
})
this.previewBackgroundMesh = new THREE.Mesh(
planeGeometry,
this.previewBackgroundColorMaterial
)
this.previewBackgroundMesh.position.set(0, 0, 0)
this.previewBackgroundScene.add(this.previewBackgroundMesh)
}
init(): void {}
dispose(): void {
if (this.previewBackgroundTexture) {
this.previewBackgroundTexture.dispose()
}
if (this.previewBackgroundColorMaterial) {
this.previewBackgroundColorMaterial.dispose()
}
if (this.previewBackgroundMesh) {
this.previewBackgroundMesh.geometry.dispose()
if (this.previewBackgroundMesh.material instanceof THREE.Material) {
this.previewBackgroundMesh.material.dispose()
}
}
}
createCapturePreview(container: Element | HTMLElement): void {
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;
`
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.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.previewContainer.style.width = `${this.previewWidth}px`
this.previewContainer.style.height = `${previewHeight}px`
}
getPreviewViewport(): {
left: number
bottom: number
width: number
height: number
} | null {
if (!this.showPreview || !this.previewContainer) {
return null
}
const renderer = this.getRenderer()
const canvas = renderer.domElement
const containerRect = this.previewContainer.getBoundingClientRect()
const canvasRect = canvas.getBoundingClientRect()
if (
containerRect.bottom < canvasRect.top ||
containerRect.top > canvasRect.bottom ||
containerRect.right < canvasRect.left ||
containerRect.left > canvasRect.right
) {
return null
}
const width = parseFloat(this.previewContainer.style.width)
const height = parseFloat(this.previewContainer.style.height)
const left = this.getRenderer().domElement.clientWidth - width
const bottom = 0
return { left, bottom, width, height }
}
renderPreview(): void {
const viewport = this.getPreviewViewport()
if (!viewport) return
const renderer = this.getRenderer()
const originalClearColor = renderer.getClearColor(new THREE.Color())
const originalClearAlpha = renderer.getClearAlpha()
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 {
const activePerspective =
this.getActiveCamera() as THREE.PerspectiveCamera
const previewPerspective = this.previewCamera as THREE.PerspectiveCamera
previewPerspective.fov = activePerspective.fov
previewPerspective.zoom = activePerspective.zoom
previewPerspective.aspect = aspect
previewPerspective.updateProjectionMatrix()
}
this.previewCamera.lookAt(this.getControls().target)
renderer.setViewport(
viewport.left,
viewport.bottom,
viewport.width,
viewport.height
)
renderer.setScissor(
viewport.left,
viewport.bottom,
viewport.width,
viewport.height
)
renderer.setClearColor(0x000000, 0)
renderer.clear()
this.renderPreviewBackground(renderer)
renderer.render(this.scene, this.previewCamera)
renderer.setClearColor(originalClearColor, originalClearAlpha)
}
private renderPreviewBackground(renderer: THREE.WebGLRenderer): void {
if (this.previewBackgroundMesh) {
const currentToneMapping = renderer.toneMapping
const currentExposure = renderer.toneMappingExposure
renderer.toneMapping = THREE.NoToneMapping
renderer.render(this.previewBackgroundScene, this.previewBackgroundCamera)
renderer.toneMapping = currentToneMapping
renderer.toneMappingExposure = currentExposure
}
}
setPreviewBackgroundColor(color: string | number | THREE.Color): void {
this.currentBackgroundColor.set(color)
if (!this.previewBackgroundMesh || !this.previewBackgroundColorMaterial) {
this.initPreviewBackgroundScene()
}
this.previewBackgroundColorMaterial!.color.copy(this.currentBackgroundColor)
if (this.previewBackgroundMesh) {
this.previewBackgroundMesh.material = this.previewBackgroundColorMaterial!
}
if (this.previewBackgroundTexture) {
this.previewBackgroundTexture.dispose()
this.previewBackgroundTexture = null
}
}
togglePreview(showPreview: boolean): void {
this.showPreview = showPreview
if (this.previewContainer) {
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.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()
}
updateBackgroundTexture(texture: THREE.Texture | null): void {
if (texture) {
if (this.previewBackgroundTexture) {
this.previewBackgroundTexture.dispose()
}
this.previewBackgroundTexture = texture
if (this.previewBackgroundMesh) {
const imageMaterial = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
depthWrite: false,
depthTest: false,
side: THREE.DoubleSide
})
if (
this.previewBackgroundMesh.material instanceof THREE.Material &&
this.previewBackgroundMesh.material !==
this.previewBackgroundColorMaterial
) {
this.previewBackgroundMesh.material.dispose()
}
this.previewBackgroundMesh.material = imageMaterial
this.previewBackgroundMesh.position.set(0, 0, 0)
this.updateBackgroundSize(
this.previewBackgroundTexture,
this.previewBackgroundMesh,
this.targetWidth,
this.targetHeight
)
}
} else {
this.setPreviewBackgroundColor(this.currentBackgroundColor)
}
}
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 {}
}

View File

@@ -16,6 +16,8 @@ export class RecordingManager {
private recordingStartTime: number = 0
private recordingDuration: number = 0
private recordingCanvas: HTMLCanvasElement | null = null
private recordingContext: CanvasRenderingContext2D | null = null
private animationFrameId: number | null = null
constructor(
scene: THREE.Scene,
@@ -50,13 +52,70 @@ export class RecordingManager {
this.scene.add(this.recordingIndicator)
}
public async startRecording(): Promise<void> {
public async startRecording(
targetWidth?: number,
targetHeight?: number
): Promise<void> {
if (this.isRecording) {
return
}
try {
this.recordingCanvas = this.renderer.domElement
const sourceCanvas = this.renderer.domElement
const sourceWidth = sourceCanvas.width
const sourceHeight = sourceCanvas.height
const recordWidth = targetWidth || sourceWidth
const recordHeight = targetHeight || sourceHeight
this.recordingCanvas = document.createElement('canvas')
this.recordingCanvas.width = recordWidth
this.recordingCanvas.height = recordHeight
this.recordingContext = this.recordingCanvas.getContext('2d', {
alpha: false
})
if (!this.recordingContext) {
throw new Error('Failed to get 2D context for recording canvas')
}
const sourceAspectRatio = sourceWidth / sourceHeight
const targetAspectRatio = recordWidth / recordHeight
let sx = 0,
sy = 0,
sw = sourceWidth,
sh = sourceHeight
if (Math.abs(sourceAspectRatio - targetAspectRatio) > 0.01) {
if (sourceAspectRatio > targetAspectRatio) {
sw = sourceHeight * targetAspectRatio
sx = (sourceWidth - sw) / 2
} else {
sh = sourceWidth / targetAspectRatio
sy = (sourceHeight - sh) / 2
}
}
const captureFrame = () => {
if (!this.isRecording || !this.recordingContext) {
return
}
this.recordingContext.drawImage(
sourceCanvas,
sx,
sy,
sw,
sh,
0,
0,
recordWidth,
recordHeight
)
this.animationFrameId = requestAnimationFrame(captureFrame)
}
this.recordingStream = this.recordingCanvas.captureStream(30)
@@ -82,6 +141,11 @@ export class RecordingManager {
this.isRecording = false
this.recordingStream = null
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
this.eventManager.emitEvent('recordingStopped', {
duration: this.recordingDuration,
hasRecording: this.recordedChunks.length > 0
@@ -96,6 +160,8 @@ export class RecordingManager {
this.isRecording = true
this.recordingStartTime = Date.now()
captureFrame()
this.eventManager.emitEvent('recordingStarted', null)
} catch (error) {
console.error('Error starting recording:', error)
@@ -110,10 +176,18 @@ export class RecordingManager {
this.recordingDuration = (Date.now() - this.recordingStartTime) / 1000
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
this.mediaRecorder.stop()
if (this.recordingStream) {
this.recordingStream.getTracks().forEach((track) => track.stop())
}
this.recordingCanvas = null
this.recordingContext = null
}
public getIsRecording(): boolean {
@@ -167,9 +241,17 @@ export class RecordingManager {
}
public dispose(): void {
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
this.stopRecording()
this.clearRecording()
this.recordingCanvas = null
this.recordingContext = null
if (this.recordingIndicator) {
this.scene.remove(this.recordingIndicator)
;(this.recordingIndicator.material as THREE.SpriteMaterial).map?.dispose()

View File

@@ -134,9 +134,20 @@ export class SceneManager implements SceneManagerInterface {
this.eventManager.emitEvent('backgroundImageLoadingStart', null)
let imageUrl = Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(uploadPath)
)
let type = 'input'
let pathParts = Load3dUtils.splitFilePath(uploadPath)
let subfolder = pathParts[0]
let filename = pathParts[1]
if (subfolder === 'temp') {
type = 'temp'
pathParts = ['', filename]
} else if (subfolder === 'output') {
type = 'output'
pathParts = ['', filename]
}
let imageUrl = Load3dUtils.getResourceURL(...pathParts, type)
if (!imageUrl.startsWith('/api')) {
imageUrl = '/api' + imageUrl
@@ -184,8 +195,8 @@ export class SceneManager implements SceneManagerInterface {
this.updateBackgroundSize(
this.backgroundTexture,
this.backgroundMesh,
this.renderer.domElement.width,
this.renderer.domElement.height
this.renderer.domElement.clientWidth,
this.renderer.domElement.clientHeight
)
this.eventManager.emitEvent('backgroundImageChange', uploadPath)
@@ -268,7 +279,7 @@ export class SceneManager implements SceneManagerInterface {
captureScene(
width: number,
height: number
): Promise<{ scene: string; mask: string; normal: string; lineart: string }> {
): Promise<{ scene: string; mask: string; normal: string }> {
return new Promise(async (resolve, reject) => {
try {
const originalWidth = this.renderer.domElement.width
@@ -359,60 +370,9 @@ export class SceneManager implements SceneManagerInterface {
}
})
let lineartModel: THREE.Group | null = null
const originalSceneVisible: Map<THREE.Object3D, boolean> = new Map()
this.scene.traverse((child) => {
if (child instanceof THREE.Group && child.name === 'lineartModel') {
lineartModel = child as THREE.Group
}
if (
child instanceof THREE.Mesh &&
!(child.parent?.name === 'lineartModel')
) {
originalSceneVisible.set(child, child.visible)
child.visible = false
}
})
this.renderer.setClearColor(0xffffff, 1)
this.renderer.clear()
if (lineartModel !== null) {
lineartModel = lineartModel as THREE.Group
const originalLineartVisibleMap: Map<THREE.Object3D, boolean> =
new Map()
lineartModel.traverse((child: THREE.Object3D) => {
if (child instanceof THREE.Mesh) {
originalLineartVisibleMap.set(child, child.visible)
child.visible = true
}
})
const originalLineartVisible = lineartModel.visible
lineartModel.visible = true
this.renderer.render(this.scene, this.getActiveCamera())
lineartModel.visible = originalLineartVisible
originalLineartVisibleMap.forEach((visible, object) => {
object.visible = visible
})
}
const lineartData = this.renderer.domElement.toDataURL('image/png')
originalSceneVisible.forEach((visible, object) => {
object.visible = visible
})
this.gridHelper.visible = gridVisible
this.renderer.setClearColor(originalClearColor, originalClearAlpha)
@@ -424,8 +384,7 @@ export class SceneManager implements SceneManagerInterface {
resolve({
scene: sceneData,
mask: maskData,
normal: normalData,
lineart: lineartData
normal: normalData
})
} catch (error) {
reject(error)

View File

@@ -1,18 +1,8 @@
import * as THREE from 'three'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2'
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry'
import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils'
import { ColoredShadowMaterial } from './conditional-lines/ColoredShadowMaterial'
import { ConditionalEdgesGeometry } from './conditional-lines/ConditionalEdgesGeometry'
import { ConditionalEdgesShader } from './conditional-lines/ConditionalEdgesShader.js'
import { ConditionalLineMaterial } from './conditional-lines/Lines2/ConditionalLineMaterial'
import { ConditionalLineSegmentsGeometry } from './conditional-lines/Lines2/ConditionalLineSegmentsGeometry'
import {
type EventManagerInterface,
type Load3DOptions,
type MaterialMode,
type ModelManagerInterface,
type UpDirection
@@ -45,25 +35,13 @@ export class SceneModelManager implements ModelManagerInterface {
private eventManager: EventManagerInterface
private activeCamera: THREE.Camera
private setupCamera: (size: THREE.Vector3) => void
private lineartModel: THREE.Group
private createLineartModel: boolean = false
LIGHT_MODEL = 0xffffff
LIGHT_LINES = 0x455a64
conditionalModel: THREE.Object3D | null = null
edgesModel: THREE.Object3D | null = null
backgroundModel: THREE.Object3D | null = null
shadowModel: THREE.Object3D | null = null
depthModel: THREE.Object3D | null = null
constructor(
scene: THREE.Scene,
renderer: THREE.WebGLRenderer,
eventManager: EventManagerInterface,
getActiveCamera: () => THREE.Camera,
setupCamera: (size: THREE.Vector3) => void,
options: Load3DOptions
setupCamera: (size: THREE.Vector3) => void
) {
this.scene = scene
this.renderer = renderer
@@ -72,14 +50,6 @@ export class SceneModelManager implements ModelManagerInterface {
this.setupCamera = setupCamera
this.textureLoader = new THREE.TextureLoader()
if (
options &&
!options.inputSpec?.isPreview &&
!options.inputSpec?.isAnimation
) {
this.createLineartModel = true
}
this.normalMaterial = new THREE.MeshNormalMaterial({
flatShading: false,
side: THREE.DoubleSide,
@@ -101,10 +71,6 @@ export class SceneModelManager implements ModelManagerInterface {
})
this.standardMaterial = this.createSTLMaterial()
this.lineartModel = new THREE.Group()
this.lineartModel.name = 'lineartModel'
}
init(): void {}
@@ -120,8 +86,6 @@ export class SceneModelManager implements ModelManagerInterface {
this.appliedTexture.dispose()
this.appliedTexture = null
}
this.disposeLineartModel()
}
createSTLMaterial(): THREE.MeshStandardMaterial {
@@ -134,360 +98,6 @@ export class SceneModelManager implements ModelManagerInterface {
})
}
disposeLineartModel(): void {
this.disposeEdgesModel()
this.disposeShadowModel()
this.disposeBackgroundModel()
this.disposeDepthModel()
this.disposeConditionalModel()
}
disposeEdgesModel(): void {
if (this.edgesModel) {
if (this.edgesModel.parent) {
this.edgesModel.parent.remove(this.edgesModel)
}
this.edgesModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
if (Array.isArray(child.material)) {
child.material.forEach((m) => m.dispose())
} else {
child.material.dispose()
}
}
})
}
}
initEdgesModel() {
this.disposeEdgesModel()
if (!this.currentModel) {
return
}
this.edgesModel = this.currentModel.clone()
this.lineartModel.add(this.edgesModel)
const meshes: THREE.Mesh[] = []
this.edgesModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
meshes.push(child)
}
})
for (const key in meshes) {
const mesh = meshes[key]
const parent = mesh.parent
let lineGeom = new THREE.EdgesGeometry(mesh.geometry, 85)
const line = new THREE.LineSegments(
lineGeom,
new THREE.LineBasicMaterial({ color: this.LIGHT_LINES })
)
line.position.copy(mesh.position)
line.scale.copy(mesh.scale)
line.rotation.copy(mesh.rotation)
const thickLineGeom = new LineSegmentsGeometry().fromEdgesGeometry(
lineGeom
)
const thickLines = new LineSegments2(
thickLineGeom,
new LineMaterial({ color: this.LIGHT_LINES, linewidth: 13 })
)
thickLines.position.copy(mesh.position)
thickLines.scale.copy(mesh.scale)
thickLines.rotation.copy(mesh.rotation)
parent?.remove(mesh)
parent?.add(line)
parent?.add(thickLines)
}
this.edgesModel.traverse((child) => {
if (
child instanceof THREE.Mesh &&
child.material &&
child.material.resolution
) {
this.renderer.getSize(child.material.resolution)
child.material.resolution.multiplyScalar(window.devicePixelRatio)
child.material.linewidth = 1
}
})
}
setEdgeThreshold(threshold: number): void {
if (!this.edgesModel || !this.currentModel) {
return
}
const linesToRemove: THREE.Object3D[] = []
this.edgesModel.traverse((child) => {
if (
child instanceof THREE.LineSegments ||
child instanceof LineSegments2
) {
linesToRemove.push(child)
}
})
for (const line of linesToRemove) {
if (line.parent) {
line.parent.remove(line)
}
}
const meshes: THREE.Mesh[] = []
this.currentModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
meshes.push(child)
}
})
for (const mesh of meshes) {
const meshClone = mesh.clone()
let lineGeom = new THREE.EdgesGeometry(meshClone.geometry, threshold)
const line = new THREE.LineSegments(
lineGeom,
new THREE.LineBasicMaterial({ color: this.LIGHT_LINES })
)
line.position.copy(mesh.position)
line.scale.copy(mesh.scale)
line.rotation.copy(mesh.rotation)
const thickLineGeom = new LineSegmentsGeometry().fromEdgesGeometry(
lineGeom
)
const thickLines = new LineSegments2(
thickLineGeom,
new LineMaterial({ color: this.LIGHT_LINES, linewidth: 13 })
)
thickLines.position.copy(mesh.position)
thickLines.scale.copy(mesh.scale)
thickLines.rotation.copy(mesh.rotation)
this.edgesModel.add(line)
this.edgesModel.add(thickLines)
}
this.edgesModel.traverse((child) => {
if (
child instanceof THREE.Mesh &&
child.material &&
child.material.resolution
) {
this.renderer.getSize(child.material.resolution)
child.material.resolution.multiplyScalar(window.devicePixelRatio)
child.material.linewidth = 1
}
})
this.eventManager.emitEvent('edgeThresholdChange', threshold)
}
disposeBackgroundModel(): void {
if (this.backgroundModel) {
if (this.backgroundModel.parent) {
this.backgroundModel.parent.remove(this.backgroundModel)
}
this.backgroundModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.dispose()
}
})
}
}
disposeShadowModel(): void {
if (this.shadowModel) {
if (this.shadowModel.parent) {
this.shadowModel.parent.remove(this.shadowModel)
}
this.shadowModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.dispose()
}
})
}
}
disposeDepthModel(): void {
if (this.depthModel) {
if (this.depthModel.parent) {
this.depthModel.parent.remove(this.depthModel)
}
this.depthModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.dispose()
}
})
}
}
disposeConditionalModel(): void {
if (this.conditionalModel) {
if (this.conditionalModel.parent) {
this.conditionalModel.parent.remove(this.conditionalModel)
}
this.conditionalModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.dispose()
}
})
}
}
initBackgroundModel() {
this.disposeBackgroundModel()
this.disposeShadowModel()
this.disposeDepthModel()
if (!this.currentModel) {
return
}
this.backgroundModel = this.currentModel.clone()
this.backgroundModel.visible = true
this.backgroundModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshBasicMaterial({
color: this.LIGHT_MODEL
})
child.material.polygonOffset = true
child.material.polygonOffsetFactor = 1
child.material.polygonOffsetUnits = 1
child.renderOrder = 2
child.material.transparent = false
child.material.opacity = 0.25
}
})
this.lineartModel.add(this.backgroundModel)
this.shadowModel = this.currentModel.clone()
// TODO this has some error, need to fix later
this.shadowModel.visible = false
this.shadowModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = new ColoredShadowMaterial({
color: this.LIGHT_MODEL,
shininess: 1.0
})
child.material.polygonOffset = true
child.material.polygonOffsetFactor = 1
child.material.polygonOffsetUnits = 1
child.receiveShadow = true
child.renderOrder = 2
}
})
this.lineartModel.add(this.shadowModel)
this.depthModel = this.currentModel.clone()
this.depthModel.visible = true
this.depthModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshBasicMaterial({
color: this.LIGHT_MODEL
})
child.material.polygonOffset = true
child.material.polygonOffsetFactor = 1
child.material.polygonOffsetUnits = 1
child.material.colorWrite = false
child.renderOrder = 1
}
})
this.lineartModel.add(this.depthModel)
}
initConditionalModel() {
this.disposeConditionalModel()
if (!this.currentModel) {
return
}
this.conditionalModel = this.currentModel.clone()
this.lineartModel.add(this.conditionalModel)
this.conditionalModel.visible = true
const meshes: THREE.Mesh[] = []
this.conditionalModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
meshes.push(child)
}
})
for (const key in meshes) {
const mesh = meshes[key]
const parent = mesh.parent
const mergedGeom = mesh.geometry.clone()
for (const key in mergedGeom.attributes) {
if (key !== 'position') {
mergedGeom.deleteAttribute(key)
}
}
const lineGeom = new ConditionalEdgesGeometry(mergeVertices(mergedGeom))
const material = new THREE.ShaderMaterial(ConditionalEdgesShader)
material.uniforms.diffuse.value.set(this.LIGHT_LINES)
const line = new THREE.LineSegments(lineGeom, material)
line.position.copy(mesh.position)
line.scale.copy(mesh.scale)
line.rotation.copy(mesh.rotation)
const thickLineGeom =
new ConditionalLineSegmentsGeometry().fromConditionalEdgesGeometry(
lineGeom
)
const conditionalLineMaterial = new ConditionalLineMaterial({
color: this.LIGHT_LINES,
linewidth: 2
})
const thickLines = new LineSegments2(
thickLineGeom,
conditionalLineMaterial
)
thickLines.position.copy(mesh.position)
thickLines.scale.copy(mesh.scale)
thickLines.rotation.copy(mesh.rotation)
parent?.remove(mesh)
parent?.add(line)
parent?.add(thickLines)
}
this.conditionalModel.traverse((child) => {
if (
child instanceof THREE.Mesh &&
child.material &&
child.material.resolution
) {
this.renderer.getSize(child.material.resolution)
child.material.resolution.multiplyScalar(window.devicePixelRatio)
child.material.linewidth = 1
}
})
}
setMaterialMode(mode: MaterialMode): void {
if (!this.currentModel || mode === this.materialMode) {
return
@@ -502,11 +112,7 @@ export class SceneModelManager implements ModelManagerInterface {
}
if (this.currentModel) {
this.currentModel.visible = mode !== 'lineart'
}
if (this.lineartModel) {
this.lineartModel.visible = mode === 'lineart'
this.currentModel.visible = true
}
this.currentModel.traverse((child) => {
@@ -649,6 +255,7 @@ export class SceneModelManager implements ModelManagerInterface {
reset(): void {
this.currentModel = null
this.originalModel = null
this.originalRotation = null
this.currentUpDirection = 'original'
this.setMaterialMode('original')
@@ -699,20 +306,6 @@ export class SceneModelManager implements ModelManagerInterface {
this.setupModelMaterials(model)
this.setupCamera(size)
if (this.createLineartModel) {
this.setupLineartModel()
}
}
setupLineartModel(): void {
this.scene.add(this.lineartModel)
this.initEdgesModel()
this.initBackgroundModel()
this.initConditionalModel()
this.lineartModel.visible = false
}
setOriginalModel(model: THREE.Object3D | THREE.BufferGeometry | GLTF): void {
@@ -730,7 +323,6 @@ export class SceneModelManager implements ModelManagerInterface {
if (this.originalRotation) {
this.currentModel.rotation.copy(this.originalRotation)
this.lineartModel.rotation.copy(this.originalRotation)
}
switch (direction) {
@@ -755,8 +347,6 @@ export class SceneModelManager implements ModelManagerInterface {
break
}
this.lineartModel.rotation.copy(this.currentModel.rotation)
this.eventManager.emitEvent('upDirectionChange', direction)
}
}

View File

@@ -74,7 +74,7 @@ export class ViewHelperManager implements ViewHelperManagerInterface {
this.viewHelper.update(delta)
if (!this.viewHelper.animating) {
this.nodeStorage.storeNodeProperty('Camera Info', {
const cameraState = {
position: this.getActiveCamera().position.clone(),
target: this.getControls().target.clone(),
zoom:
@@ -85,7 +85,20 @@ export class ViewHelperManager implements ViewHelperManagerInterface {
this.getActiveCamera() instanceof THREE.PerspectiveCamera
? 'perspective'
: 'orthographic'
})
}
const cameraConfig = this.nodeStorage.loadNodeProperty(
'Camera Config',
{
cameraType: cameraState.cameraType,
fov:
this.getActiveCamera() instanceof THREE.PerspectiveCamera
? (this.getActiveCamera() as THREE.PerspectiveCamera).fov
: 75
}
)
cameraConfig.state = cameraState
this.nodeStorage.storeNodeProperty('Camera Config', cameraConfig)
}
}
}

View File

@@ -1,163 +0,0 @@
/*
Taken from: https://github.com/gkjohnson/threejs-sandbox/tree/master/conditional-lines
under MIT license
*/
import { Color, ShaderLib, ShaderMaterial, UniformsUtils } from 'three'
export class ColoredShadowMaterial extends ShaderMaterial {
get color() {
return this.uniforms.diffuse.value
}
get shadowColor() {
return this.uniforms.shadowColor.value
}
set shininess(v) {
this.uniforms.shininess.value = v
}
get shininess() {
return this.uniforms.shininess.value
}
constructor(options) {
super({
uniforms: UniformsUtils.merge([
ShaderLib.phong.uniforms,
{
shadowColor: {
value: new Color(0xff0000)
}
}
]),
vertexShader: `
#define PHONG
varying vec3 vViewPosition;
#ifndef FLAT_SHADED
varying vec3 vNormal;
#endif
#include <common>
#include <uv_pars_vertex>
#include <uv2_pars_vertex>
#include <displacementmap_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <shadowmap_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
#include <uv_vertex>
#include <uv2_vertex>
#include <color_vertex>
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinbase_vertex>
#include <skinnormal_vertex>
#include <defaultnormal_vertex>
#ifndef FLAT_SHADED
vNormal = normalize( transformedNormal );
#endif
#include <begin_vertex>
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <displacementmap_vertex>
#include <project_vertex>
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
vViewPosition = - mvPosition.xyz;
#include <worldpos_vertex>
#include <envmap_vertex>
#include <shadowmap_vertex>
#include <fog_vertex>
}
`,
fragmentShader: `
#define PHONG
uniform vec3 diffuse;
uniform vec3 emissive;
uniform vec3 specular;
uniform float shininess;
uniform float opacity;
uniform vec3 shadowColor;
#include <common>
#include <packing>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <uv2_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <emissivemap_pars_fragment>
#include <envmap_common_pars_fragment>
#include <envmap_pars_fragment>
#include <cube_uv_reflection_fragment>
#include <fog_pars_fragment>
#include <bsdfs>
#include <lights_pars_begin>
#include <lights_phong_pars_fragment>
#include <shadowmap_pars_fragment>
#include <bumpmap_pars_fragment>
#include <normalmap_pars_fragment>
#include <specularmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
void main() {
#include <clipping_planes_fragment>
vec4 diffuseColor = vec4( 1.0, 1.0, 1.0, opacity );
ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
vec3 totalEmissiveRadiance = emissive;
#include <logdepthbuf_fragment>
#include <map_fragment>
#include <color_fragment>
#include <alphamap_fragment>
#include <alphatest_fragment>
#include <specularmap_fragment>
#include <normal_fragment_begin>
#include <normal_fragment_maps>
#include <emissivemap_fragment>
#include <lights_phong_fragment>
#include <lights_fragment_begin>
#include <lights_fragment_maps>
#include <lights_fragment_end>
#include <aomap_fragment>
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
#include <envmap_fragment>
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
#include <tonemapping_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
#include <dithering_fragment>
gl_FragColor.rgb = mix(
shadowColor.rgb,
diffuse.rgb,
min( gl_FragColor.r, 1.0 )
);
}
`
})
Object.defineProperties(this, {
opacity: {
set(v) {
this.uniforms.opacity.value = v
},
get() {
return this.uniforms.opacity.value
}
}
})
this.setValues(options)
this.lights = true
}
}

View File

@@ -1,126 +0,0 @@
/*
Taken from: https://github.com/gkjohnson/threejs-sandbox/tree/master/conditional-lines
under MIT license
*/
import { BufferAttribute, BufferGeometry, Triangle, Vector3 } from 'three'
const vec0 = new Vector3()
const vec1 = new Vector3()
const vec2 = new Vector3()
const vec3 = new Vector3()
const vec4 = new Vector3()
const triangle0 = new Triangle()
const triangle1 = new Triangle()
const normal0 = new Vector3()
const normal1 = new Vector3()
export class ConditionalEdgesGeometry extends BufferGeometry {
constructor(geometry) {
super()
const edgeInfo = {}
const position = geometry.attributes.position
let index
if (geometry.index) {
index = geometry.index
} else {
const arr = new Array(position.count / 3).fill().map((_, i) => i)
index = new BufferAttribute(new Uint32Array(arr), 1, false)
}
for (let i = 0, l = index.count; i < l; i += 3) {
const indices = [index.getX(i + 0), index.getX(i + 1), index.getX(i + 2)]
for (let j = 0; j < 3; j++) {
const index0 = indices[j]
const index1 = indices[(j + 1) % 3]
const hash = `${index0}_${index1}`
const reverseHash = `${index1}_${index0}`
if (reverseHash in edgeInfo) {
edgeInfo[reverseHash].controlIndex1 = indices[(j + 2) % 3]
edgeInfo[reverseHash].tri1 = i / 3
} else {
edgeInfo[hash] = {
index0,
index1,
controlIndex0: indices[(j + 2) % 3],
controlIndex1: null,
tri0: i / 3,
tri1: null
}
}
}
}
const edgePositions = []
const edgeDirections = []
const edgeControl0 = []
const edgeControl1 = []
for (const key in edgeInfo) {
const { index0, index1, controlIndex0, controlIndex1, tri0, tri1 } =
edgeInfo[key]
if (controlIndex1 === null) {
continue
}
triangle0.a.fromBufferAttribute(position, index.getX(tri0 * 3 + 0))
triangle0.b.fromBufferAttribute(position, index.getX(tri0 * 3 + 1))
triangle0.c.fromBufferAttribute(position, index.getX(tri0 * 3 + 2))
triangle1.a.fromBufferAttribute(position, index.getX(tri1 * 3 + 0))
triangle1.b.fromBufferAttribute(position, index.getX(tri1 * 3 + 1))
triangle1.c.fromBufferAttribute(position, index.getX(tri1 * 3 + 2))
triangle0.getNormal(normal0).normalize()
triangle1.getNormal(normal1).normalize()
if (normal0.dot(normal1) < 0.01) {
continue
}
// positions
vec0.fromBufferAttribute(position, index0)
vec1.fromBufferAttribute(position, index1)
// direction
vec2.subVectors(vec0, vec1)
// control positions
vec3.fromBufferAttribute(position, controlIndex0)
vec4.fromBufferAttribute(position, controlIndex1)
// create arrays
edgePositions.push(vec0.x, vec0.y, vec0.z)
edgeDirections.push(vec2.x, vec2.y, vec2.z)
edgeControl0.push(vec3.x, vec3.y, vec3.z)
edgeControl1.push(vec4.x, vec4.y, vec4.z)
edgePositions.push(vec1.x, vec1.y, vec1.z)
edgeDirections.push(vec2.x, vec2.y, vec2.z)
edgeControl0.push(vec3.x, vec3.y, vec3.z)
edgeControl1.push(vec4.x, vec4.y, vec4.z)
}
this.setAttribute(
'position',
new BufferAttribute(new Float32Array(edgePositions), 3, false)
)
this.setAttribute(
'direction',
new BufferAttribute(new Float32Array(edgeDirections), 3, false)
)
this.setAttribute(
'control0',
new BufferAttribute(new Float32Array(edgeControl0), 3, false)
)
this.setAttribute(
'control1',
new BufferAttribute(new Float32Array(edgeControl1), 3, false)
)
}
}

View File

@@ -1,96 +0,0 @@
/*
Taken from: https://github.com/gkjohnson/threejs-sandbox/tree/master/conditional-lines
under MIT license
*/
import { Color } from 'three'
export const ConditionalEdgesShader = {
uniforms: {
diffuse: {
value: new Color()
},
opacity: {
value: 1.0
}
},
vertexShader: /* glsl */ `
attribute vec3 control0;
attribute vec3 control1;
attribute vec3 direction;
#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
#include <color_vertex>
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
// Transform the line segment ends and control points into camera clip space
vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );
c0 /= c0.w;
c1 /= c1.w;
p0 /= p0.w;
p1 /= p1.w;
// Get the direction of the segment and an orthogonal vector
vec2 dir = p1.xy - p0.xy;
vec2 norm = vec2( -dir.y, dir.x );
// Get control point directions from the line
vec2 c0dir = c0.xy - p1.xy;
vec2 c1dir = c1.xy - p1.xy;
// If the vectors to the controls points are pointed in different directions away
// from the line segment then the line should not be drawn.
float d0 = dot( normalize( norm ), normalize( c0dir ) );
float d1 = dot( normalize( norm ), normalize( c1dir ) );
float discardFlag = float( sign( d0 ) != sign( d1 ) );
gl_Position = discardFlag > 0.5 ? c0 : gl_Position;
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
#include <fog_vertex>
}
`,
fragmentShader: /* glsl */ `
uniform vec3 diffuse;
uniform float opacity;
#include <common>
#include <color_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
void main() {
#include <clipping_planes_fragment>
vec3 outgoingLight = vec3( 0.0 );
vec4 diffuseColor = vec4( diffuse, opacity );
#include <logdepthbuf_fragment>
#include <color_fragment>
outgoingLight = diffuseColor.rgb; // simple shader
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
#include <tonemapping_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
}
`
}

View File

@@ -1,379 +0,0 @@
/*
Taken from: https://github.com/gkjohnson/threejs-sandbox/tree/master/conditional-lines
under MIT license
*/
import { ShaderMaterial, UniformsLib, UniformsUtils, Vector2 } from 'three'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
/**
* parameters = {
* color: <hex>,
* linewidth: <float>,
* dashed: <boolean>,
* dashScale: <float>,
* dashSize: <float>,
* gapSize: <float>,
* resolution: <Vector2>, // to be set by renderer
* }
*/
const uniforms = {
linewidth: { value: 1 },
resolution: { value: new Vector2(1, 1) },
dashScale: { value: 1 },
dashSize: { value: 1 },
gapSize: { value: 1 }, // todo FIX - maybe change to totalSize
opacity: { value: 1 }
}
const shader = {
uniforms: UniformsUtils.merge([
UniformsLib.common,
UniformsLib.fog,
uniforms
]),
vertexShader: /* glsl */ `
#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
uniform float linewidth;
uniform vec2 resolution;
attribute vec3 control0;
attribute vec3 control1;
attribute vec3 direction;
attribute vec3 instanceStart;
attribute vec3 instanceEnd;
attribute vec3 instanceColorStart;
attribute vec3 instanceColorEnd;
varying vec2 vUv;
#ifdef USE_DASH
uniform float dashScale;
attribute float instanceDistanceStart;
attribute float instanceDistanceEnd;
varying float vLineDistance;
#endif
void trimSegment( const in vec4 start, inout vec4 end ) {
// trim end segment so it terminates between the camera plane and the near plane
// conservative estimate of the near plane
float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column
float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column
float nearEstimate = - 0.5 * b / a;
float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );
end.xyz = mix( start.xyz, end.xyz, alpha );
}
void main() {
#ifdef USE_COLOR
vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;
#endif
#ifdef USE_DASH
vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;
#endif
float aspect = resolution.x / resolution.y;
vUv = uv;
// camera space
vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );
vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );
// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
// perhaps there is a more elegant solution -- WestLangley
bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column
if ( perspective ) {
if ( start.z < 0.0 && end.z >= 0.0 ) {
trimSegment( start, end );
} else if ( end.z < 0.0 && start.z >= 0.0 ) {
trimSegment( end, start );
}
}
// clip space
vec4 clipStart = projectionMatrix * start;
vec4 clipEnd = projectionMatrix * end;
// ndc space
vec2 ndcStart = clipStart.xy / clipStart.w;
vec2 ndcEnd = clipEnd.xy / clipEnd.w;
// direction
vec2 dir = ndcEnd - ndcStart;
// account for clip-space aspect ratio
dir.x *= aspect;
dir = normalize( dir );
// perpendicular to dir
vec2 offset = vec2( dir.y, - dir.x );
// undo aspect ratio adjustment
dir.x /= aspect;
offset.x /= aspect;
// sign flip
if ( position.x < 0.0 ) offset *= - 1.0;
// endcaps
if ( position.y < 0.0 ) {
offset += - dir;
} else if ( position.y > 1.0 ) {
offset += dir;
}
// adjust for linewidth
offset *= linewidth;
// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
offset /= resolution.y;
// select end
vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;
// back to clip space
offset *= clip.w;
clip.xy += offset;
gl_Position = clip;
vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
#include <fog_vertex>
// conditional logic
// Transform the line segment ends and control points into camera clip space
vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
vec4 p0 = projectionMatrix * modelViewMatrix * vec4( instanceStart, 1.0 );
vec4 p1 = projectionMatrix * modelViewMatrix * vec4( instanceStart + direction, 1.0 );
c0 /= c0.w;
c1 /= c1.w;
p0 /= p0.w;
p1 /= p1.w;
// Get the direction of the segment and an orthogonal vector
vec2 segDir = p1.xy - p0.xy;
vec2 norm = vec2( - segDir.y, segDir.x );
// Get control point directions from the line
vec2 c0dir = c0.xy - p1.xy;
vec2 c1dir = c1.xy - p1.xy;
// If the vectors to the controls points are pointed in different directions away
// from the line segment then the line should not be drawn.
float d0 = dot( normalize( norm ), normalize( c0dir ) );
float d1 = dot( normalize( norm ), normalize( c1dir ) );
float discardFlag = float( sign( d0 ) != sign( d1 ) );
gl_Position = discardFlag > 0.5 ? c0 : gl_Position;
// end conditional line logic
}
`,
fragmentShader: /* glsl */ `
uniform vec3 diffuse;
uniform float opacity;
#ifdef USE_DASH
uniform float dashSize;
uniform float gapSize;
#endif
varying float vLineDistance;
#include <common>
#include <color_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
varying vec2 vUv;
void main() {
#include <clipping_planes_fragment>
#ifdef USE_DASH
if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps
if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX
#endif
if ( abs( vUv.y ) > 1.0 ) {
float a = vUv.x;
float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
float len2 = a * a + b * b;
if ( len2 > 1.0 ) discard;
}
vec4 diffuseColor = vec4( diffuse, opacity );
#include <logdepthbuf_fragment>
#include <color_fragment>
gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a );
#include <tonemapping_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
}
`
}
class ConditionalLineMaterial extends LineMaterial {
constructor(parameters) {
super({
type: 'ConditionalLineMaterial',
uniforms: UniformsUtils.clone(shader.uniforms),
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader,
clipping: true // required for clipping support
})
this.dashed = false
Object.defineProperties(this, {
color: {
enumerable: true,
get: function () {
return this.uniforms.diffuse.value
},
set: function (value) {
this.uniforms.diffuse.value = value
}
},
linewidth: {
enumerable: true,
get: function () {
return this.uniforms.linewidth.value
},
set: function (value) {
this.uniforms.linewidth.value = value
}
},
dashScale: {
enumerable: true,
get: function () {
return this.uniforms.dashScale.value
},
set: function (value) {
this.uniforms.dashScale.value = value
}
},
dashSize: {
enumerable: true,
get: function () {
return this.uniforms.dashSize.value
},
set: function (value) {
this.uniforms.dashSize.value = value
}
},
gapSize: {
enumerable: true,
get: function () {
return this.uniforms.gapSize.value
},
set: function (value) {
this.uniforms.gapSize.value = value
}
},
opacity: {
enumerable: true,
get: function () {
return this.uniforms.opacity.value
},
set: function (value) {
this.uniforms.opacity.value = value
}
},
resolution: {
enumerable: true,
get: function () {
return this.uniforms.resolution.value
},
set: function (value) {
this.uniforms.resolution.value.copy(value)
}
}
})
this.setValues(parameters)
}
}
ConditionalLineMaterial.prototype.isConditionalLineMaterial = true
export { ConditionalLineMaterial }

View File

@@ -1,43 +0,0 @@
/*
Taken from: https://github.com/gkjohnson/threejs-sandbox/tree/master/conditional-lines
under MIT license
*/
import * as THREE from 'three'
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'
export class ConditionalLineSegmentsGeometry extends LineSegmentsGeometry {
fromConditionalEdgesGeometry(geometry) {
super.fromEdgesGeometry(geometry)
const { direction, control0, control1 } = geometry.attributes
this.setAttribute(
'direction',
new THREE.InterleavedBufferAttribute(
new THREE.InstancedInterleavedBuffer(direction.array, 6, 1),
3,
0
)
)
this.setAttribute(
'control0',
new THREE.InterleavedBufferAttribute(
new THREE.InstancedInterleavedBuffer(control0.array, 6, 1),
3,
0
)
)
this.setAttribute(
'control1',
new THREE.InterleavedBufferAttribute(
new THREE.InstancedInterleavedBuffer(control1.array, 6, 1),
3,
0
)
)
return this
}
}

View File

@@ -10,16 +10,7 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
export type Load3DNodeType = 'Load3D' | 'Preview3D'
export type Load3DAnimationNodeType = 'Load3DAnimation' | 'Preview3DAnimation'
export type MaterialMode =
| 'original'
| 'normal'
| 'wireframe'
| 'depth'
| 'lineart'
export type MaterialMode = 'original' | 'normal' | 'wireframe' | 'depth'
export type UpDirection = 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
export type CameraType = 'perspective' | 'orthographic'
@@ -30,6 +21,27 @@ export interface CameraState {
cameraType: CameraType
}
export interface SceneConfig {
showGrid: boolean
backgroundColor: string
backgroundImage?: string
}
export interface ModelConfig {
upDirection: UpDirection
materialMode: MaterialMode
}
export interface CameraConfig {
cameraType: CameraType
fov: number
state?: CameraState
}
export interface LightConfig {
intensity: number
}
export interface EventCallback {
(data?: any): void
}
@@ -45,7 +57,6 @@ export interface CaptureResult {
scene: string
mask: string
normal: string
lineart: string
}
interface BaseManager {
@@ -101,26 +112,6 @@ export interface ViewHelperManagerInterface extends BaseManager {
handleResize(): void
}
export interface PreviewManagerInterface extends BaseManager {
previewCamera: THREE.Camera
previewContainer: HTMLDivElement
showPreview: boolean
previewWidth: number
createCapturePreview(container: Element | HTMLElement): void
updatePreviewSize(): void
togglePreview(showPreview: boolean): void
setTargetSize(width: number, height: number): void
handleResize(): void
updateBackgroundTexture(texture: THREE.Texture | null): void
getPreviewViewport(): {
left: number
bottom: number
width: number
height: number
} | null
renderPreview(): void
}
export interface EventManagerInterface {
addEventListener(event: string, callback: EventCallback): void
removeEventListener(event: string, callback: EventCallback): void
@@ -185,3 +176,11 @@ export interface LoaderManagerInterface {
dispose(): void
loadModel(url: string, originalFileName?: string): Promise<void>
}
export const SUPPORTED_EXTENSIONS = new Set([
'.gltf',
'.glb',
'.obj',
'.fbx',
'.stl'
])

View File

@@ -1,6 +1,7 @@
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import { useLoad3d } from '@/composables/useLoad3d'
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
@@ -10,6 +11,12 @@ import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { useExtensionService } from '@/services/extensionService'
import { useLoad3dService } from '@/services/load3dService'
const inputSpec: CustomInputSpec = {
name: 'image',
type: 'Preview3D',
isPreview: true
}
useExtensionService().registerExtension({
name: 'Comfy.SaveGLB',
@@ -23,13 +30,6 @@ useExtensionService().registerExtension({
getCustomWidgets() {
return {
PREVIEW_3D(node) {
const inputSpec: CustomInputSpec = {
name: 'image',
type: 'Preview3D',
isAnimation: false,
isPreview: true
}
const widget = new ComponentWidgetImpl({
node,
name: inputSpec.name,
@@ -38,6 +38,8 @@ useExtensionService().registerExtension({
options: {}
})
widget.type = 'load3D'
addWidget(node, widget)
return { widget }
@@ -71,19 +73,19 @@ useExtensionService().registerExtension({
const fileInfo = message['3d'][0]
const load3d = useLoad3dService().getLoad3d(node)
useLoad3d(node).waitForLoad3d((load3d) => {
const modelWidget = node.widgets?.find((w) => w.name === 'image')
const modelWidget = node.widgets?.find((w) => w.name === 'image')
if (load3d && modelWidget) {
const filePath = fileInfo['subfolder'] + '/' + fileInfo['filename']
if (load3d && modelWidget) {
const filePath = fileInfo['subfolder'] + '/' + fileInfo['filename']
modelWidget.value = filePath
modelWidget.value = filePath
const config = new Load3DConfiguration(load3d)
const config = new Load3DConfiguration(load3d)
config.configureForSaveMesh(fileInfo['type'], filePath)
}
config.configureForSaveMesh(fileInfo['type'], filePath)
}
})
}
}
})