diff --git a/nodes/visualization/preview_mesh_dual.py b/nodes/visualization/preview_mesh_dual.py index c4deefb9..a6e4537b 100644 --- a/nodes/visualization/preview_mesh_dual.py +++ b/nodes/visualization/preview_mesh_dual.py @@ -46,6 +46,22 @@ def has_fields(mesh): return has_vertex_attrs or has_face_attrs +def get_texture_info(mesh): + """Extract texture/visual information from a mesh.""" + has_visual = hasattr(mesh, 'visual') and mesh.visual is not None + visual_kind = mesh.visual.kind if has_visual else None + has_texture = visual_kind == 'texture' and hasattr(mesh.visual, 'material') if has_visual else False + has_vertex_colors = visual_kind == 'vertex' if has_visual else False + has_material = has_texture + return { + 'has_visual': has_visual, + 'visual_kind': visual_kind, + 'has_texture': has_texture, + 'has_vertex_colors': has_vertex_colors, + 'has_material': has_material + } + + class PreviewMeshDualNode: """ Unified dual mesh preview with VTK.js - supports both side-by-side and overlay layouts. @@ -63,6 +79,7 @@ class PreviewMeshDualNode: }, "optional": { "layout": (["side_by_side", "overlay"], {"default": "side_by_side"}), + "mode": (["fields", "texture"], {"default": "fields"}), "opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}), } } @@ -72,20 +89,21 @@ class PreviewMeshDualNode: FUNCTION = "preview_dual" CATEGORY = "geompack/visualization" - def preview_dual(self, mesh_1, mesh_2, layout="side_by_side", opacity=1.0): + def preview_dual(self, mesh_1, mesh_2, layout="side_by_side", mode="fields", opacity=1.0): """ - Preview two meshes with chosen layout and field visualization. + Preview two meshes with chosen layout and visualization mode. Args: mesh_1: First trimesh object mesh_2: Second trimesh object layout: "side_by_side" or "overlay" + mode: "fields" (scientific visualization) or "texture" (textured rendering) opacity: Opacity for both meshes (0.0-1.0) Returns: dict: UI data for frontend widget """ - print(f"[PreviewMeshDual] Layout: {layout}") + print(f"[PreviewMeshDual] Layout: {layout}, Mode: {mode}") print(f"[PreviewMeshDual] Mesh 1: {get_geometry_type(mesh_1)} - {len(mesh_1.vertices)} vertices, {get_face_count(mesh_1)} faces") print(f"[PreviewMeshDual] Mesh 2: {get_geometry_type(mesh_2)} - {len(mesh_2.vertices)} vertices, {get_face_count(mesh_2)} faces") @@ -100,6 +118,13 @@ class PreviewMeshDualNode: print(f"[PreviewMeshDual] Mesh 2 fields: {field_names_2}") print(f"[PreviewMeshDual] Common fields: {common_fields}") + # Check for texture/visual data + texture_info_1 = get_texture_info(mesh_1) + texture_info_2 = get_texture_info(mesh_2) + + print(f"[PreviewMeshDual] Mesh 1 visual: kind={texture_info_1['visual_kind']}, texture={texture_info_1['has_texture']}, vertex_colors={texture_info_1['has_vertex_colors']}") + print(f"[PreviewMeshDual] Mesh 2 visual: kind={texture_info_2['visual_kind']}, texture={texture_info_2['has_texture']}, vertex_colors={texture_info_2['has_vertex_colors']}") + # Check if meshes are point clouds (need VTP, STL doesn't support point clouds) mesh_1_is_pc = is_point_cloud(mesh_1) mesh_2_is_pc = is_point_cloud(mesh_2) @@ -108,13 +133,20 @@ class PreviewMeshDualNode: preview_id = uuid.uuid4().hex[:8] if layout == "side_by_side": - # Export meshes separately - use VTP for fields OR point clouds - filename_1, filepath_1 = self._export_mesh(mesh_1, f"preview_dual_1_{preview_id}", mesh_1_has_fields or mesh_1_is_pc) - filename_2, filepath_2 = self._export_mesh(mesh_2, f"preview_dual_2_{preview_id}", mesh_2_has_fields or mesh_2_is_pc) + # Export meshes separately based on mode + if mode == "texture": + # Texture mode: export as GLB + filename_1, filepath_1 = self._export_mesh(mesh_1, f"preview_dual_1_{preview_id}", use_vtp=False, use_glb=True) + filename_2, filepath_2 = self._export_mesh(mesh_2, f"preview_dual_2_{preview_id}", use_vtp=False, use_glb=True) + else: + # Fields mode: use VTP for fields OR point clouds + filename_1, filepath_1 = self._export_mesh(mesh_1, f"preview_dual_1_{preview_id}", use_vtp=(mesh_1_has_fields or mesh_1_is_pc), use_glb=False) + filename_2, filepath_2 = self._export_mesh(mesh_2, f"preview_dual_2_{preview_id}", use_vtp=(mesh_2_has_fields or mesh_2_is_pc), use_glb=False) # Build UI data for side-by-side mode ui_data = { "layout": [layout], + "mode": [mode], "mesh_1_file": [filename_1], "mesh_2_file": [filename_2], "vertex_count_1": [len(mesh_1.vertices)], @@ -129,17 +161,44 @@ class PreviewMeshDualNode: "extents_2": [mesh_2.extents.tolist()], "is_watertight_1": [bool(mesh_1.is_watertight) if not is_point_cloud(mesh_1) else False], "is_watertight_2": [bool(mesh_2.is_watertight) if not is_point_cloud(mesh_2) else False], - "field_names_1": [field_names_1], - "field_names_2": [field_names_2], - "common_fields": [common_fields], } + # Add mode-specific metadata + if mode == "texture": + # Texture mode metadata + ui_data.update({ + "has_texture_1": [texture_info_1['has_texture']], + "has_texture_2": [texture_info_2['has_texture']], + "visual_kind_1": [texture_info_1['visual_kind'] if texture_info_1['visual_kind'] else "none"], + "visual_kind_2": [texture_info_2['visual_kind'] if texture_info_2['visual_kind'] else "none"], + "has_vertex_colors_1": [texture_info_1['has_vertex_colors']], + "has_vertex_colors_2": [texture_info_2['has_vertex_colors']], + "has_material_1": [texture_info_1['has_material']], + "has_material_2": [texture_info_2['has_material']], + }) + else: + # Fields mode metadata + ui_data.update({ + "field_names_1": [field_names_1], + "field_names_2": [field_names_2], + "common_fields": [common_fields], + }) + + else: # overlay # Combine meshes with color coding - filename, filepath = self._export_combined_mesh( - mesh_1, mesh_2, preview_id, opacity, - mesh_1_has_fields, mesh_2_has_fields - ) + if mode == "texture": + # Texture mode: export combined mesh as GLB + filename, filepath = self._export_combined_mesh( + mesh_1, mesh_2, preview_id, opacity, + mesh_1_has_fields, mesh_2_has_fields, use_glb=True + ) + else: + # Fields mode: export combined mesh as VTP + filename, filepath = self._export_combined_mesh( + mesh_1, mesh_2, preview_id, opacity, + mesh_1_has_fields, mesh_2_has_fields, use_glb=False + ) # Calculate combined bounds combined_bounds_min = np.minimum(mesh_1.bounds[0], mesh_2.bounds[0]) @@ -149,6 +208,7 @@ class PreviewMeshDualNode: # Build UI data for overlay mode ui_data = { "layout": [layout], + "mode": [mode], "mesh_file": [filename], "vertex_count_1": [len(mesh_1.vertices)], "vertex_count_2": [len(mesh_2.vertices)], @@ -160,17 +220,37 @@ class PreviewMeshDualNode: "opacity": [float(opacity)], "is_watertight_1": [bool(mesh_1.is_watertight) if not is_point_cloud(mesh_1) else False], "is_watertight_2": [bool(mesh_2.is_watertight) if not is_point_cloud(mesh_2) else False], - "field_names_1": [field_names_1], - "field_names_2": [field_names_2], - "common_fields": [common_fields], } + # Add mode-specific metadata + if mode == "texture": + # Texture mode metadata + ui_data.update({ + "has_texture_1": [texture_info_1['has_texture']], + "has_texture_2": [texture_info_2['has_texture']], + "visual_kind_1": [texture_info_1['visual_kind'] if texture_info_1['visual_kind'] else "none"], + "visual_kind_2": [texture_info_2['visual_kind'] if texture_info_2['visual_kind'] else "none"], + "has_vertex_colors_1": [texture_info_1['has_vertex_colors']], + "has_vertex_colors_2": [texture_info_2['has_vertex_colors']], + "has_material_1": [texture_info_1['has_material']], + "has_material_2": [texture_info_2['has_material']], + }) + else: + # Fields mode metadata + ui_data.update({ + "field_names_1": [field_names_1], + "field_names_2": [field_names_2], + "common_fields": [common_fields], + }) + print(f"[PreviewMeshDual] Preview ready") return {"ui": ui_data} - def _export_mesh(self, mesh, base_filename, use_vtp): + def _export_mesh(self, mesh, base_filename, use_vtp, use_glb): """Export a single mesh to appropriate format.""" - if use_vtp: + if use_glb: + filename = f"{base_filename}.glb" + elif use_vtp: filename = f"{base_filename}.vtp" else: filename = f"{base_filename}.stl" @@ -181,7 +261,10 @@ class PreviewMeshDualNode: filepath = os.path.join(tempfile.gettempdir(), filename) try: - if use_vtp: + if use_glb: + mesh.export(filepath, file_type='glb', include_normals=True) + print(f"[PreviewMeshDual] Exported GLB: {filepath}") + elif use_vtp: export_mesh_with_scalars_vtp(mesh, filepath) print(f"[PreviewMeshDual] Exported VTP with fields: {filepath}") else: @@ -190,29 +273,38 @@ class PreviewMeshDualNode: except Exception as e: print(f"[PreviewMeshDual] Export failed: {e}, trying fallback") # Fallback to OBJ - filename = filename.replace('.vtp', '.obj').replace('.stl', '.obj') - filepath = filepath.replace('.vtp', '.obj').replace('.stl', '.obj') + filename = filename.replace('.vtp', '.obj').replace('.stl', '.obj').replace('.glb', '.obj') + filepath = filepath.replace('.vtp', '.obj').replace('.stl', '.obj').replace('.glb', '.obj') mesh.export(filepath, file_type='obj') print(f"[PreviewMeshDual] Exported OBJ fallback: {filepath}") return filename, filepath def _export_combined_mesh(self, mesh_1, mesh_2, preview_id, opacity, - mesh_1_has_fields, mesh_2_has_fields): - """Export combined mesh for overlay mode as VTP.""" + mesh_1_has_fields, mesh_2_has_fields, use_glb): + """Export combined mesh for overlay mode as VTP or GLB.""" # Combine meshes (with or without fields) try: combined = trimesh_module.util.concatenate([mesh_1, mesh_2]) - filename = f"preview_dual_overlay_{preview_id}.vtp" + + if use_glb: + filename = f"preview_dual_overlay_{preview_id}.glb" + else: + filename = f"preview_dual_overlay_{preview_id}.vtp" if COMFYUI_OUTPUT_FOLDER: filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename) else: filepath = os.path.join(tempfile.gettempdir(), filename) - export_mesh_with_scalars_vtp(combined, filepath) - print(f"[PreviewMeshDual] Exported combined VTP: {filepath}") + if use_glb: + combined.export(filepath, file_type='glb', include_normals=True) + print(f"[PreviewMeshDual] Exported combined GLB: {filepath}") + else: + export_mesh_with_scalars_vtp(combined, filepath) + print(f"[PreviewMeshDual] Exported combined VTP: {filepath}") + print(f"[PreviewMeshDual] Combined {get_geometry_type(combined)}: {len(combined.vertices)} vertices, {get_face_count(combined)} faces") return filename, filepath except Exception as e: diff --git a/nodes/visualization/preview_mesh_vtk.py b/nodes/visualization/preview_mesh_vtk.py index f63e921c..13405a2c 100644 --- a/nodes/visualization/preview_mesh_vtk.py +++ b/nodes/visualization/preview_mesh_vtk.py @@ -39,6 +39,7 @@ class PreviewMeshVTKNode: return { "required": { "trimesh": ("TRIMESH",), + "mode": (["fields", "texture"], {"default": "fields"}), }, } @@ -47,15 +48,17 @@ class PreviewMeshVTKNode: FUNCTION = "preview_mesh_vtk" CATEGORY = "geompack/visualization" - def preview_mesh_vtk(self, trimesh): + def preview_mesh_vtk(self, trimesh, mode="fields"): """ Export mesh and prepare for VTK.js preview. - Automatically detects scalar fields (vertex/face attributes) and exports - to VTP format if present, otherwise exports to STL. + Supports two visualization modes: + - fields: Scientific visualization with scalar fields and colormaps (VTP/STL export) + - texture: Textured mesh visualization with materials (GLB export) Args: trimesh: Input trimesh_module.Trimesh object + mode: Visualization mode - "fields" or "texture" Returns: dict: UI data for frontend widget @@ -79,17 +82,36 @@ class PreviewMeshVTKNode: print(f"[PreviewMeshVTK] DEBUG - has_face_attrs: {has_face_attrs}") print(f"[PreviewMeshVTK] DEBUG - has_fields: {has_fields}") + # Check for visual data (textures/vertex colors) + has_visual = hasattr(trimesh, 'visual') and trimesh.visual is not None + visual_kind = trimesh.visual.kind if has_visual else None + has_texture = visual_kind == 'texture' and hasattr(trimesh.visual, 'material') if has_visual else False + has_vertex_colors = visual_kind == 'vertex' if has_visual else False + has_material = has_texture + + print(f"[PreviewMeshVTK] Mode: {mode}") + print(f"[PreviewMeshVTK] Visual data - has_visual: {has_visual}, kind: {visual_kind}, texture: {has_texture}, vertex_colors: {has_vertex_colors}") + # Check if this is a point cloud is_pc = is_point_cloud(trimesh) - # Choose export format based on whether fields exist or if it's a point cloud - if has_fields or is_pc: - # Export to VTP for: scalar fields OR point clouds (STL doesn't support point clouds) - filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.vtp" - print(f"[PreviewMeshVTK] Using VTP format (fields={has_fields}, point_cloud={is_pc})") + # Choose export format based on visualization mode + if mode == "texture": + # Texture mode: Export GLB to preserve textures/materials/UVs + filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.glb" + viewer_type = "texture" + print(f"[PreviewMeshVTK] Using texture mode - GLB export") else: - # Export to STL (compact format for simple surface meshes) - filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.stl" + # Fields mode: Export VTP/STL for scalar field visualization + if has_fields or is_pc: + # Export to VTP for: scalar fields OR point clouds (STL doesn't support point clouds) + filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.vtp" + print(f"[PreviewMeshVTK] Using VTP format (fields={has_fields}, point_cloud={is_pc})") + else: + # Export to STL (compact format for simple surface meshes) + filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.stl" + viewer_type = "fields" + print(f"[PreviewMeshVTK] Using fields mode - VTP/STL export") # Use ComfyUI's output directory if COMFYUI_OUTPUT_FOLDER: @@ -99,7 +121,11 @@ class PreviewMeshVTKNode: # Export mesh try: - if has_fields or is_pc: + if mode == "texture": + # Export GLB for texture rendering + trimesh.export(filepath, file_type='glb', include_normals=True) + print(f"[PreviewMeshVTK] Exported GLB to: {filepath}") + elif has_fields or is_pc: # Use VTP exporter for fields or point clouds export_mesh_with_scalars_vtp(trimesh, filepath) print(f"[PreviewMeshVTK] Exported VTP to: {filepath}") @@ -146,6 +172,8 @@ class PreviewMeshVTKNode: # Return metadata for frontend widget ui_data = { "mesh_file": [filename], + "viewer_type": [viewer_type], # "fields" or "texture" - tells frontend which viewer to load + "mode": [mode], # User-selected mode "vertex_count": [len(trimesh.vertices)], "face_count": [get_face_count(trimesh)], "bounds_min": [bounds[0].tolist()], @@ -153,19 +181,33 @@ class PreviewMeshVTKNode: "extents": [extents.tolist()], "max_extent": [float(max_extent)], "is_watertight": [bool(is_watertight)], - "field_names": [field_names], # Always include (empty array if no fields) } + # Add mode-specific metadata + if viewer_type == "texture": + # Texture mode metadata + ui_data.update({ + "has_texture": [has_texture], + "has_vertex_colors": [has_vertex_colors], + "has_material": [has_material], + "visual_kind": [visual_kind if visual_kind else "none"], + }) + else: + # Fields mode metadata + ui_data["field_names"] = [field_names] # Field visualization data + # Add optional fields if available if volume is not None: ui_data["volume"] = [volume] if area is not None: ui_data["area"] = [area] - if field_names: - print(f"[PreviewMeshVTK] Mesh info: watertight={is_watertight}, volume={volume}, area={area}, fields={field_names}") + if viewer_type == "texture": + print(f"[PreviewMeshVTK] Texture mode info: watertight={is_watertight}, volume={volume}, area={area}, texture={has_texture}, vertex_colors={has_vertex_colors}") + elif field_names: + print(f"[PreviewMeshVTK] Fields mode info: watertight={is_watertight}, volume={volume}, area={area}, fields={field_names}") else: - print(f"[PreviewMeshVTK] Mesh info: watertight={is_watertight}, volume={volume}, area={area}, no fields") + print(f"[PreviewMeshVTK] Fields mode info: watertight={is_watertight}, volume={volume}, area={area}, no fields") return {"ui": ui_data} diff --git a/web/js/mesh_preview_dual.js b/web/js/mesh_preview_dual.js index 0df6e0f8..5b7e8870 100644 --- a/web/js/mesh_preview_dual.js +++ b/web/js/mesh_preview_dual.js @@ -39,8 +39,12 @@ app.registerExtension({ console.log('[GeomPack Dual] Created iframe with minHeight:', iframe.style.minHeight); // Point to unified dual VTK.js HTML viewer (with cache buster) + // Note: viewer will be dynamically switched based on mode in onExecuted iframe.src = "/extensions/ComfyUI-GeometryPack/viewer_dual.html?v=" + Date.now(); + // Track current viewer type to avoid unnecessary reloads + let currentViewerType = "fields"; + // Create mesh info panel const infoPanel = document.createElement("div"); infoPanel.style.backgroundColor = "#1a1a1a"; @@ -126,6 +130,14 @@ app.registerExtension({ } const layout = message.layout[0]; + const mode = message.mode?.[0] || "fields"; + console.log(`[GeomPack Dual] onExecuted: layout=${layout}, mode=${mode}`); + + // Determine which viewer to use based on mode + const viewerType = mode === "texture" ? "texture" : "fields"; + const viewerUrl = viewerType === "texture" + ? "/extensions/ComfyUI-GeometryPack/viewer_dual_textured.html" + : "/extensions/ComfyUI-GeometryPack/viewer_dual.html"; let infoHTML = ''; let postMessageData = { @@ -157,8 +169,15 @@ app.registerExtension({ const extentsStr2 = extents2.length === 3 ? `${extents2.map(v => v.toFixed(2)).join(' × ')}` : 'N/A'; + // Build info panel with mode info + const modeLabel = mode.charAt(0).toUpperCase() + mode.slice(1); + const modeColor = mode === "texture" ? '#c8c' : '#6cc'; + infoHTML = `