mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 22:39:39 +00:00
[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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
`
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
])
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user