Skip to content

Commit 35ff882

Browse files
authored
[3d] better solution to support reading extra resource/texture (#4209)
1 parent f57f97c commit 35ff882

File tree

8 files changed

+205
-641
lines changed

8 files changed

+205
-641
lines changed

src/components/load3d/Load3D.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,11 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
206206
return
207207
}
208208
209-
backgroundImage.value = await Load3dUtils.uploadFile(file)
209+
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
210+
211+
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
212+
213+
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
210214
211215
node.properties['Background Image'] = backgroundImage.value
212216
}
@@ -218,7 +222,14 @@ const handleUploadTexture = async (file: File) => {
218222
}
219223
220224
try {
221-
const texturePath = await Load3dUtils.uploadFile(file)
225+
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
226+
227+
const subfolder = resourceFolder.trim()
228+
? `3d/${resourceFolder.trim()}`
229+
: '3d'
230+
231+
const texturePath = await Load3dUtils.uploadFile(file, subfolder)
232+
222233
await load3DSceneRef.value.load3d.applyTexture(texturePath)
223234
224235
node.properties['Texture'] = texturePath

src/components/load3d/Load3DAnimation.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,11 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
238238
return
239239
}
240240
241-
backgroundImage.value = await Load3dUtils.uploadFile(file)
241+
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
242+
243+
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
244+
245+
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
242246
243247
node.properties['Background Image'] = backgroundImage.value
244248
}

src/extensions/core/load3d.ts

Lines changed: 116 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import type {
2-
IComboWidget,
3-
IStringWidget
4-
} from '@comfyorg/litegraph/dist/types/widgets'
1+
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
52
import { nextTick } from 'vue'
63

74
import Load3D from '@/components/load3d/Load3D.vue'
@@ -17,6 +14,80 @@ import { useExtensionService } from '@/services/extensionService'
1714
import { useLoad3dService } from '@/services/load3dService'
1815
import { useToastStore } from '@/stores/toastStore'
1916

17+
async function handleModelUpload(files: FileList, node: any) {
18+
if (!files?.length) return
19+
20+
const modelWidget = node.widgets?.find(
21+
(w: any) => w.name === 'model_file'
22+
) as IStringWidget
23+
24+
node.properties['Texture'] = undefined
25+
26+
try {
27+
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
28+
29+
const subfolder = resourceFolder.trim()
30+
? `3d/${resourceFolder.trim()}`
31+
: '3d'
32+
33+
const uploadPath = await Load3dUtils.uploadFile(files[0], subfolder)
34+
35+
if (!uploadPath) {
36+
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
37+
return
38+
}
39+
40+
const modelUrl = api.apiURL(
41+
Load3dUtils.getResourceURL(
42+
...Load3dUtils.splitFilePath(uploadPath),
43+
'input'
44+
)
45+
)
46+
47+
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
48+
49+
if (uploadPath && modelWidget) {
50+
if (!modelWidget.options?.values?.includes(uploadPath)) {
51+
modelWidget.options?.values?.push(uploadPath)
52+
}
53+
54+
modelWidget.value = uploadPath
55+
}
56+
} catch (error) {
57+
console.error('Model upload failed:', error)
58+
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
59+
}
60+
}
61+
62+
async function handleResourcesUpload(files: FileList, node: any) {
63+
if (!files?.length) return
64+
65+
try {
66+
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
67+
68+
const subfolder = resourceFolder.trim()
69+
? `3d/${resourceFolder.trim()}`
70+
: '3d'
71+
72+
await Load3dUtils.uploadMultipleFiles(files, subfolder)
73+
} catch (error) {
74+
console.error('Extra resources upload failed:', error)
75+
useToastStore().addAlert(t('toastMessages.extraResourcesUploadFailed'))
76+
}
77+
}
78+
79+
function createFileInput(
80+
accept: string,
81+
multiple: boolean = false
82+
): HTMLInputElement {
83+
const input = document.createElement('input')
84+
input.type = 'file'
85+
input.accept = accept
86+
input.multiple = multiple
87+
input.style.display = 'none'
88+
return input
89+
}
90+
2091
useExtensionService().registerExtension({
2192
name: 'Comfy.Load3D',
2293
settings: [
@@ -110,49 +181,34 @@ useExtensionService().registerExtension({
110181
getCustomWidgets() {
111182
return {
112183
LOAD_3D(node) {
113-
const fileInput = document.createElement('input')
114-
fileInput.type = 'file'
115-
fileInput.accept = '.gltf,.glb,.obj,.fbx,.stl'
116-
fileInput.style.display = 'none'
184+
const fileInput = createFileInput('.gltf,.glb,.obj,.fbx,.stl', false)
117185

118-
fileInput.onchange = async () => {
119-
if (fileInput.files?.length) {
120-
const modelWidget = node.widgets?.find(
121-
(w) => w.name === 'model_file'
122-
) as IComboWidget & { options: { values: string[] } }
186+
node.properties['Resource Folder'] = ''
123187

124-
node.properties['Texture'] = undefined
125-
126-
const uploadPath = await Load3dUtils.uploadFile(
127-
fileInput.files[0]
128-
).catch((error) => {
129-
console.error('File upload failed:', error)
130-
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
131-
})
132-
133-
const modelUrl = api.apiURL(
134-
Load3dUtils.getResourceURL(
135-
...Load3dUtils.splitFilePath(uploadPath),
136-
'input'
137-
)
138-
)
139-
140-
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
141-
142-
if (uploadPath && modelWidget) {
143-
if (!modelWidget.options?.values?.includes(uploadPath)) {
144-
modelWidget.options?.values?.push(uploadPath)
145-
}
146-
147-
modelWidget.value = uploadPath
148-
}
149-
}
188+
fileInput.onchange = async () => {
189+
await handleModelUpload(fileInput.files!, node)
150190
}
151191

152192
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
153193
fileInput.click()
154194
})
155195

196+
const resourcesInput = createFileInput('*', true)
197+
198+
resourcesInput.onchange = async () => {
199+
await handleResourcesUpload(resourcesInput.files!, node)
200+
resourcesInput.value = ''
201+
}
202+
203+
node.addWidget(
204+
'button',
205+
'upload extra resources',
206+
'uploadExtraResources',
207+
() => {
208+
resourcesInput.click()
209+
}
210+
)
211+
156212
node.addWidget('button', 'clear', 'clear', () => {
157213
useLoad3dService().getLoad3d(node)?.clearModel()
158214

@@ -264,46 +320,34 @@ useExtensionService().registerExtension({
264320
getCustomWidgets() {
265321
return {
266322
LOAD_3D_ANIMATION(node) {
267-
const fileInput = document.createElement('input')
268-
fileInput.type = 'file'
269-
fileInput.accept = '.gltf,.glb,.fbx'
270-
fileInput.style.display = 'none'
323+
const fileInput = createFileInput('.gltf,.glb,.fbx', false)
324+
325+
node.properties['Resource Folder'] = ''
326+
271327
fileInput.onchange = async () => {
272-
if (fileInput.files?.length) {
273-
const modelWidget = node.widgets?.find(
274-
(w) => w.name === 'model_file'
275-
) as IStringWidget
276-
277-
const uploadPath = await Load3dUtils.uploadFile(
278-
fileInput.files[0]
279-
).catch((error) => {
280-
console.error('File upload failed:', error)
281-
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
282-
})
283-
284-
const modelUrl = api.apiURL(
285-
Load3dUtils.getResourceURL(
286-
...Load3dUtils.splitFilePath(uploadPath),
287-
'input'
288-
)
289-
)
290-
291-
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
292-
293-
if (uploadPath && modelWidget) {
294-
if (!modelWidget.options?.values?.includes(uploadPath)) {
295-
modelWidget.options?.values?.push(uploadPath)
296-
}
297-
298-
modelWidget.value = uploadPath
299-
}
300-
}
328+
await handleModelUpload(fileInput.files!, node)
301329
}
302330

303331
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
304332
fileInput.click()
305333
})
306334

335+
const resourcesInput = createFileInput('*', true)
336+
337+
resourcesInput.onchange = async () => {
338+
await handleResourcesUpload(resourcesInput.files!, node)
339+
resourcesInput.value = ''
340+
}
341+
342+
node.addWidget(
343+
'button',
344+
'upload extra resources',
345+
'uploadExtraResources',
346+
() => {
347+
resourcesInput.click()
348+
}
349+
)
350+
307351
node.addWidget('button', 'clear', 'clear', () => {
308352
useLoad3dService().getLoad3d(node)?.clearModel()
309353

src/extensions/core/load3d/Load3DConfiguration.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ class Load3DConfiguration {
128128
if (!value) return
129129

130130
const filename = value as string
131+
132+
this.setResourceFolder(filename)
133+
131134
const modelUrl = api.apiURL(
132135
Load3dUtils.getResourceURL(
133136
...Load3dUtils.splitFilePath(filename),
@@ -173,6 +176,21 @@ class Load3DConfiguration {
173176
}
174177
}
175178
}
179+
180+
private setResourceFolder(filename: string): void {
181+
const pathParts = filename.split('/').filter((part) => part.trim())
182+
183+
if (pathParts.length <= 2) {
184+
return
185+
}
186+
187+
const subfolderParts = pathParts.slice(1, -1)
188+
const subfolder = subfolderParts.join('/')
189+
190+
if (subfolder) {
191+
this.load3d.node.properties['Resource Folder'] = subfolder
192+
}
193+
}
176194
}
177195

178196
export default Load3DConfiguration

src/extensions/core/load3d/Load3d.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,7 @@ class Load3d {
118118
options
119119
)
120120

121-
this.loaderManager = new LoaderManager(
122-
this.modelManager,
123-
this.eventManager,
124-
options
125-
)
121+
this.loaderManager = new LoaderManager(this.modelManager, this.eventManager)
126122

127123
this.recordingManager = new RecordingManager(
128124
this.sceneManager.scene,

src/extensions/core/load3d/Load3dUtils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ class Load3dUtils {
3434
return await resp.json()
3535
}
3636

37-
static async uploadFile(file: File) {
37+
static async uploadFile(file: File, subfolder: string) {
3838
let uploadPath
3939

4040
try {
4141
const body = new FormData()
4242
body.append('image', file)
43-
body.append('subfolder', '3d')
43+
44+
body.append('subfolder', subfolder)
4445

4546
const resp = await api.fetchApi('/upload/image', {
4647
method: 'POST',
@@ -96,6 +97,14 @@ class Load3dUtils {
9697

9798
return `/view?${params}`
9899
}
100+
101+
static async uploadMultipleFiles(files: FileList, subfolder: string = '3d') {
102+
const uploadPromises = Array.from(files).map((file) =>
103+
this.uploadFile(file, subfolder)
104+
)
105+
106+
await Promise.all(uploadPromises)
107+
}
99108
}
100109

101110
export default Load3dUtils

0 commit comments

Comments
 (0)