22import folder_paths
33import os
44
5- from comfy . comfy_types import IO
6- from comfy_api .input_impl import VideoFromFile
5+ from typing_extensions import override
6+ from comfy_api .latest import IO , ComfyExtension , InputImpl , UI
77
88from pathlib import Path
99
10- from PIL import Image
11- import numpy as np
12-
13- import uuid
1410
1511def normalize_path (path ):
1612 return path .replace ('\\ ' , '/' )
1713
18- class Load3D ():
14+ class Load3D (IO . ComfyNode ):
1915 @classmethod
20- def INPUT_TYPES ( s ):
16+ def define_schema ( cls ):
2117 input_dir = os .path .join (folder_paths .get_input_directory (), "3d" )
2218
2319 os .makedirs (input_dir , exist_ok = True )
@@ -30,23 +26,29 @@ def INPUT_TYPES(s):
3026 for file_path in input_path .rglob ("*" )
3127 if file_path .suffix .lower () in {'.gltf' , '.glb' , '.obj' , '.fbx' , '.stl' }
3228 ]
29+ return IO .Schema (
30+ node_id = "Load3D" ,
31+ display_name = "Load 3D & Animation" ,
32+ category = "3d" ,
33+ is_experimental = True ,
34+ inputs = [
35+ IO .Combo .Input ("model_file" , options = sorted (files ), upload = IO .UploadType .model ),
36+ IO .Load3D .Input ("image" ),
37+ IO .Int .Input ("width" , default = 1024 , min = 1 , max = 4096 , step = 1 ),
38+ IO .Int .Input ("height" , default = 1024 , min = 1 , max = 4096 , step = 1 ),
39+ ],
40+ outputs = [
41+ IO .Image .Output (display_name = "image" ),
42+ IO .Mask .Output (display_name = "mask" ),
43+ IO .String .Output (display_name = "mesh_path" ),
44+ IO .Image .Output (display_name = "normal" ),
45+ IO .Load3DCamera .Output (display_name = "camera_info" ),
46+ IO .Video .Output (display_name = "recording_video" ),
47+ ],
48+ )
3349
34- return {"required" : {
35- "model_file" : (sorted (files ), {"file_upload" : True }),
36- "image" : ("LOAD_3D" , {}),
37- "width" : ("INT" , {"default" : 1024 , "min" : 1 , "max" : 4096 , "step" : 1 }),
38- "height" : ("INT" , {"default" : 1024 , "min" : 1 , "max" : 4096 , "step" : 1 }),
39- }}
40-
41- RETURN_TYPES = ("IMAGE" , "MASK" , "STRING" , "IMAGE" , "LOAD3D_CAMERA" , IO .VIDEO )
42- RETURN_NAMES = ("image" , "mask" , "mesh_path" , "normal" , "camera_info" , "recording_video" )
43-
44- FUNCTION = "process"
45- EXPERIMENTAL = True
46-
47- CATEGORY = "3d"
48-
49- def process (self , model_file , image , ** kwargs ):
50+ @classmethod
51+ def execute (cls , model_file , image , ** kwargs ) -> IO .NodeOutput :
5052 image_path = folder_paths .get_annotated_filepath (image ['image' ])
5153 mask_path = folder_paths .get_annotated_filepath (image ['mask' ])
5254 normal_path = folder_paths .get_annotated_filepath (image ['normal' ])
@@ -61,58 +63,47 @@ def process(self, model_file, image, **kwargs):
6163 if image ['recording' ] != "" :
6264 recording_video_path = folder_paths .get_annotated_filepath (image ['recording' ])
6365
64- video = VideoFromFile (recording_video_path )
66+ video = InputImpl . VideoFromFile (recording_video_path )
6567
66- return output_image , output_mask , model_file , normal_image , image ['camera_info' ], video
68+ return IO . NodeOutput ( output_image , output_mask , model_file , normal_image , image ['camera_info' ], video )
6769
68- class Preview3D ():
69- @classmethod
70- def INPUT_TYPES (s ):
71- return {"required" : {
72- "model_file" : ("STRING" , {"default" : "" , "multiline" : False }),
73- },
74- "optional" : {
75- "camera_info" : ("LOAD3D_CAMERA" , {}),
76- "bg_image" : ("IMAGE" , {})
77- }}
70+ process = execute # TODO: remove
7871
79- OUTPUT_NODE = True
80- RETURN_TYPES = ()
8172
82- CATEGORY = "3d"
83-
84- FUNCTION = "process"
85- EXPERIMENTAL = True
73+ class Preview3D (IO .ComfyNode ):
74+ @classmethod
75+ def define_schema (cls ):
76+ return IO .Schema (
77+ node_id = "Preview3D" ,
78+ display_name = "Preview 3D & Animation" ,
79+ category = "3d" ,
80+ is_experimental = True ,
81+ is_output_node = True ,
82+ inputs = [
83+ IO .String .Input ("model_file" , default = "" , multiline = False ),
84+ IO .Load3DCamera .Input ("camera_info" , optional = True ),
85+ IO .Image .Input ("bg_image" , optional = True ),
86+ ],
87+ outputs = [],
88+ )
8689
87- def process (self , model_file , ** kwargs ):
90+ @classmethod
91+ def execute (cls , model_file , ** kwargs ) -> IO .NodeOutput :
8892 camera_info = kwargs .get ("camera_info" , None )
8993 bg_image = kwargs .get ("bg_image" , None )
94+ return IO .NodeOutput (ui = UI .PreviewUI3D (model_file , camera_info , bg_image = bg_image ))
9095
91- bg_image_path = None
92- if bg_image is not None :
96+ process = execute # TODO: remove
9397
94- img_array = (bg_image [0 ].cpu ().numpy () * 255 ).astype (np .uint8 )
95- img = Image .fromarray (img_array )
9698
97- temp_dir = folder_paths .get_temp_directory ()
98- filename = f"bg_{ uuid .uuid4 ().hex } .png"
99- bg_image_path = os .path .join (temp_dir , filename )
100- img .save (bg_image_path , compress_level = 1 )
101-
102- bg_image_path = f"temp/{ filename } "
103-
104- return {
105- "ui" : {
106- "result" : [model_file , camera_info , bg_image_path ]
107- }
108- }
99+ class Load3DExtension (ComfyExtension ):
100+ @override
101+ async def get_node_list (self ) -> list [type [IO .ComfyNode ]]:
102+ return [
103+ Load3D ,
104+ Preview3D ,
105+ ]
109106
110- NODE_CLASS_MAPPINGS = {
111- "Load3D" : Load3D ,
112- "Preview3D" : Preview3D ,
113- }
114107
115- NODE_DISPLAY_NAME_MAPPINGS = {
116- "Load3D" : "Load 3D & Animation" ,
117- "Preview3D" : "Preview 3D & Animation" ,
118- }
108+ async def comfy_entrypoint () -> Load3DExtension :
109+ return Load3DExtension ()
0 commit comments