[3d] add support to upload texture (#3224)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Terry Jia
2025-03-24 16:58:45 -04:00
committed by GitHub
parent a1cfb68116
commit abe65e58a0
14 changed files with 205 additions and 10 deletions

View File

@@ -54,6 +54,7 @@
@updateUpDirection="handleUpdateUpDirection"
@updateMaterialMode="handleUpdateMaterialMode"
@updateEdgeThreshold="handleUpdateEdgeThreshold"
@uploadTexture="handleUploadTexture"
@exportModel="handleExportModel"
/>
</div>
@@ -155,6 +156,23 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
node.properties['Background Image'] = backgroundImage.value
}
const handleUploadTexture = async (file: File) => {
if (!load3DSceneRef.value?.load3d) {
useToastStore().addAlert('No 3D scene to apply texture')
return
}
try {
const texturePath = await Load3dUtils.uploadFile(file)
await load3DSceneRef.value.load3d.applyTexture(texturePath)
node.properties['Texture'] = texturePath
} catch (error) {
console.error('Error applying texture:', error)
useToastStore().addAlert('Failed to apply texture')
}
}
const handleUpdateFOV = (value: number) => {
fov.value = value

View File

@@ -50,6 +50,7 @@
@updateUpDirection="handleUpdateUpDirection"
@updateMaterialMode="handleUpdateMaterialMode"
@updateEdgeThreshold="handleUpdateEdgeThreshold"
@uploadTexture="handleUploadTexture"
ref="modelControlsRef"
/>
@@ -182,6 +183,7 @@ const emit = defineEmits<{
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
(e: 'exportModel', format: string): void
(e: 'uploadTexture', file: File): void
}>()
const backgroundColor = ref(props.backgroundColor)
@@ -230,6 +232,10 @@ const handleUpdateEdgeThreshold = (value: number) => {
emit('updateEdgeThreshold', value)
}
const handleUploadTexture = (file: File) => {
emit('uploadTexture', file)
}
const handleUpdateLightIntensity = (value: number) => {
emit('updateLightIntensity', value)
}

View File

@@ -65,7 +65,10 @@ const eventConfig = {
},
exportLoadingEnd: () => {
loadingOverlayRef.value?.endLoading()
}
},
textureLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.applyingTexture')),
textureLoadingEnd: () => loadingOverlayRef.value?.endLoading()
} as const
watchEffect(() => {

View File

@@ -58,6 +58,33 @@
</div>
</div>
</div>
<div
v-if="
materialMode === 'original' &&
!props.inputSpec.isAnimation &&
!props.inputSpec.isPreview
"
class="relative show-texture-upload"
>
<Button class="p-button-rounded p-button-text" @click="openTextureUpload">
<i
class="pi pi-image text-white text-lg"
v-tooltip.right="{
value: t('load3d.uploadTexture'),
showDelay: 300
}"
></i>
<input
type="file"
ref="texturePickerRef"
accept="image/*"
@change="uploadTexture"
class="absolute opacity-0 w-0 h-0 p-0 m-0 pointer-events-none"
/>
</Button>
</div>
<div v-if="materialMode === 'lineart'" class="relative show-edge-threshold">
<Button
class="p-button-rounded p-button-text"
@@ -115,6 +142,7 @@ const emit = defineEmits<{
(e: 'updateUpDirection', direction: UpDirection): void
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
(e: 'uploadTexture', file: File): void
}>()
const upDirection = ref(props.upDirection || 'original')
@@ -123,6 +151,7 @@ const edgeThreshold = ref(props.edgeThreshold || 85)
const showUpDirection = ref(false)
const showMaterialMode = ref(false)
const showEdgeThreshold = ref(false)
const texturePickerRef = ref<HTMLInputElement | null>(null)
const upDirections: UpDirection[] = [
'original',
@@ -218,6 +247,18 @@ const updateEdgeThreshold = () => {
emit('updateEdgeThreshold', edgeThreshold.value)
}
const openTextureUpload = () => {
texturePickerRef.value?.click()
}
const uploadTexture = (event: Event) => {
const input = event.target as HTMLInputElement
if (input.files && input.files[0]) {
emit('uploadTexture', input.files[0])
}
}
const closeSceneSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement

View File

@@ -32,6 +32,8 @@ useExtensionService().registerExtension({
(w: IWidget) => w.name === 'model_file'
) as IStringWidget
node.properties['Texture'] = undefined
const uploadPath = await Load3dUtils.uploadFile(
fileInput.files[0]
).catch((error) => {
@@ -70,6 +72,8 @@ useExtensionService().registerExtension({
)
if (modelWidget) {
modelWidget.value = ''
node.properties['Texture'] = undefined
}
})

View File

@@ -61,7 +61,12 @@ class Load3DConfiguration {
if (modelWidget.value) {
onModelWidgetUpdate(modelWidget.value)
}
modelWidget.callback = onModelWidgetUpdate
modelWidget.callback = (value: string | number | boolean | object) => {
this.load3d.node.properties['Texture'] = undefined
onModelWidgetUpdate(value)
}
}
private setupDefaultProperties() {
@@ -136,6 +141,12 @@ class Load3DConfiguration {
this.load3d.setEdgeThreshold(edgeThreshold)
const texturePath = this.load3d.loadNodeProperty('Texture', null)
if (texturePath) {
await this.load3d.applyTexture(texturePath)
}
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
try {
this.load3d.setCameraState(cameraState)

View File

@@ -26,7 +26,7 @@ class Load3d {
renderer: THREE.WebGLRenderer
protected clock: THREE.Clock
protected animationFrameId: number | null = null
protected node: LGraphNode
node: LGraphNode
protected eventManager: EventManager
protected nodeStorage: NodeStorage
@@ -267,6 +267,23 @@ class Load3d {
}
}
async applyTexture(texturePath: string): Promise<void> {
if (!this.modelManager.currentModel) {
throw new Error('No model to apply texture to')
}
this.eventManager.emitEvent('textureLoadingStart', null)
try {
await this.modelManager.applyTexture(texturePath)
} catch (error) {
console.error('Error applying texture:', error)
throw error
} finally {
this.eventManager.emitEvent('textureLoadingEnd', null)
}
}
setBackgroundColor(color: string): void {
this.sceneManager.setBackgroundColor(color)
this.forceRender()

View File

@@ -5,6 +5,7 @@ import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeome
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils'
import Load3dUtils from './Load3dUtils'
import { ColoredShadowMaterial } from './conditional-lines/ColoredShadowMaterial'
import { ConditionalEdgesGeometry } from './conditional-lines/ConditionalEdgesGeometry'
import { ConditionalEdgesShader } from './conditional-lines/ConditionalEdgesShader.js'
@@ -37,6 +38,8 @@ export class ModelManager implements ModelManagerInterface {
depthMaterial: THREE.MeshDepthMaterial
originalFileName: string | null = null
originalURL: string | null = null
appliedTexture: THREE.Texture | null = null
textureLoader: THREE.TextureLoader
private scene: THREE.Scene
private renderer: THREE.WebGLRenderer
@@ -68,6 +71,7 @@ export class ModelManager implements ModelManagerInterface {
this.eventManager = eventManager
this.activeCamera = getActiveCamera()
this.setupCamera = setupCamera
this.textureLoader = new THREE.TextureLoader()
if (
options &&
@@ -113,6 +117,11 @@ export class ModelManager implements ModelManagerInterface {
this.wireframeMaterial.dispose()
this.depthMaterial.dispose()
if (this.appliedTexture) {
this.appliedTexture.dispose()
this.appliedTexture = null
}
this.disposeLineartModel()
}
@@ -126,6 +135,66 @@ export class ModelManager implements ModelManagerInterface {
})
}
async applyTexture(texturePath: string): Promise<void> {
if (!this.currentModel) {
throw new Error('No model available to apply texture to')
}
if (this.appliedTexture) {
this.appliedTexture.dispose()
}
try {
let imageUrl = Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(texturePath)
)
if (!imageUrl.startsWith('/api')) {
imageUrl = '/api' + imageUrl
}
this.appliedTexture = await new Promise<THREE.Texture>(
(resolve, reject) => {
this.textureLoader.load(
imageUrl,
(texture) => {
texture.colorSpace = THREE.SRGBColorSpace
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
resolve(texture)
},
undefined,
(error) => reject(error)
)
}
)
if (this.materialMode === 'original') {
this.currentModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
const material = new THREE.MeshStandardMaterial({
map: this.appliedTexture,
metalness: 0.1,
roughness: 0.8,
side: THREE.DoubleSide
})
if (!this.originalMaterials.has(child)) {
this.originalMaterials.set(child, child.material)
}
child.material = material
}
})
}
return Promise.resolve()
} catch (error) {
console.error('Error applying texture:', error)
return Promise.reject(error)
}
}
disposeLineartModel(): void {
this.disposeEdgesModel()
this.disposeShadowModel()
@@ -576,7 +645,16 @@ export class ModelManager implements ModelManagerInterface {
if (originalMaterial) {
child.material = originalMaterial
} else {
child.material = this.standardMaterial
if (this.appliedTexture) {
child.material = new THREE.MeshStandardMaterial({
map: this.appliedTexture,
metalness: 0.1,
roughness: 0.8,
side: THREE.DoubleSide
})
} else {
child.material = this.standardMaterial
}
}
break
}
@@ -638,6 +716,11 @@ export class ModelManager implements ModelManagerInterface {
this.originalFileName = null
this.originalURL = null
if (this.appliedTexture) {
this.appliedTexture.dispose()
this.appliedTexture = null
}
this.originalMaterials = new WeakMap()
}

View File

@@ -981,6 +981,8 @@
"edgeThreshold": "Edge Threshold",
"export": "Export",
"exportModel": "Export Model",
"exportingModel": "Exporting model..."
"exportingModel": "Exporting model...",
"uploadTexture": "Upload Texture",
"applyingTexture": "Applying Texture..."
}
}

View File

@@ -346,6 +346,7 @@
}
},
"load3d": {
"applyingTexture": "Application de la texture...",
"backgroundColor": "Couleur de fond",
"camera": "Caméra",
"edgeThreshold": "Seuil de Bordure",
@@ -365,7 +366,8 @@
"switchCamera": "Changer de caméra",
"switchingMaterialMode": "Changement de mode de matériau...",
"upDirection": "Direction Haut",
"uploadBackgroundImage": "Télécharger l'image de fond"
"uploadBackgroundImage": "Télécharger l'image de fond",
"uploadTexture": "Télécharger Texture"
},
"maintenance": {
"None": "Aucun",

View File

@@ -346,6 +346,7 @@
}
},
"load3d": {
"applyingTexture": "テクスチャを適用中...",
"backgroundColor": "背景色",
"camera": "カメラ",
"edgeThreshold": "エッジ閾値",
@@ -365,7 +366,8 @@
"switchCamera": "カメラを切り替える",
"switchingMaterialMode": "マテリアルモードの切り替え中...",
"upDirection": "上方向",
"uploadBackgroundImage": "背景画像をアップロード"
"uploadBackgroundImage": "背景画像をアップロード",
"uploadTexture": "テクスチャをアップロード"
},
"maintenance": {
"None": "なし",

View File

@@ -346,6 +346,7 @@
}
},
"load3d": {
"applyingTexture": "텍스처 적용 중...",
"backgroundColor": "배경색",
"camera": "카메라",
"edgeThreshold": "엣지 임계값",
@@ -365,7 +366,8 @@
"switchCamera": "카메라 전환",
"switchingMaterialMode": "재질 모드 전환 중...",
"upDirection": "위 방향",
"uploadBackgroundImage": "배경 이미지 업로드"
"uploadBackgroundImage": "배경 이미지 업로드",
"uploadTexture": "텍스처 업로드"
},
"maintenance": {
"None": "없음",

View File

@@ -346,6 +346,7 @@
}
},
"load3d": {
"applyingTexture": "Применение текстуры...",
"backgroundColor": "Цвет фона",
"camera": "Камера",
"edgeThreshold": "Пороговое значение края",
@@ -365,7 +366,8 @@
"switchCamera": "Переключить камеру",
"switchingMaterialMode": "Переключение режима материала...",
"upDirection": "Направление Вверх",
"uploadBackgroundImage": "Загрузить фоновое изображение"
"uploadBackgroundImage": "Загрузить фоновое изображение",
"uploadTexture": "Загрузить текстуру"
},
"maintenance": {
"None": "Нет",

View File

@@ -346,6 +346,7 @@
}
},
"load3d": {
"applyingTexture": "应用纹理中...",
"backgroundColor": "背景颜色",
"camera": "相机",
"edgeThreshold": "边缘阈值",
@@ -365,7 +366,8 @@
"switchCamera": "切换摄像头",
"switchingMaterialMode": "切换材质模式中...",
"upDirection": "向上方向",
"uploadBackgroundImage": "上传背景图片"
"uploadBackgroundImage": "上传背景图片",
"uploadTexture": "上传纹理"
},
"maintenance": {
"None": "无",