748 lines
28 KiB
Python
748 lines
28 KiB
Python
"""
|
|
Visualization Nodes - 3D mesh preview and visualization
|
|
"""
|
|
|
|
import trimesh as trimesh_module
|
|
import os
|
|
import tempfile
|
|
import uuid
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Import create_bbox_visualization from Hunyuan3D-Part
|
|
# Add parent directory to path to allow cross-custom-node imports
|
|
custom_nodes_dir = Path(__file__).parent.parent.parent
|
|
hunyuan_utils_path = custom_nodes_dir / "ComfyUI-Hunyuan3D-Part" / "node_utils"
|
|
if str(hunyuan_utils_path) not in sys.path:
|
|
sys.path.insert(0, str(hunyuan_utils_path))
|
|
|
|
try:
|
|
from mesh_utils import create_bbox_visualization, export_scene_to_vtp
|
|
BBOX_VIZ_AVAILABLE = True
|
|
except ImportError:
|
|
print("[GeomPack] WARNING: Could not import bbox functions from Hunyuan3D-Part")
|
|
print("[GeomPack] The Preview Bounding Boxes node will not be available")
|
|
BBOX_VIZ_AVAILABLE = False
|
|
export_scene_to_vtp = None
|
|
|
|
# ComfyUI folder paths
|
|
try:
|
|
import folder_paths
|
|
COMFYUI_OUTPUT_FOLDER = folder_paths.get_output_directory()
|
|
except:
|
|
COMFYUI_OUTPUT_FOLDER = None
|
|
|
|
|
|
def _export_mesh_with_scalars_vtp(trimesh: trimesh_module.Trimesh, filepath: str):
|
|
"""
|
|
Export trimesh to VTK PolyData XML format (.vtp) with vertex scalar attributes.
|
|
|
|
VTP format preserves vertex attributes (scalar fields) which can be visualized
|
|
in VTK.js with color mapping.
|
|
|
|
Args:
|
|
trimesh: Trimesh object with optional vertex_attributes
|
|
filepath: Output .vtp file path
|
|
"""
|
|
import xml.etree.ElementTree as ET
|
|
|
|
print(f"[_export_mesh_with_scalars_vtp] Exporting to VTP: {filepath}")
|
|
|
|
# Create VTK PolyData XML structure
|
|
vtk_file = ET.Element('VTKFile', type='PolyData', version='1.0', byte_order='LittleEndian')
|
|
poly_data = ET.SubElement(vtk_file, 'PolyData')
|
|
|
|
num_verts = len(trimesh.vertices)
|
|
num_faces = len(trimesh.faces)
|
|
|
|
piece = ET.SubElement(poly_data, 'Piece', NumberOfPoints=str(num_verts), NumberOfPolys=str(num_faces))
|
|
|
|
# Points section
|
|
points = ET.SubElement(piece, 'Points')
|
|
points_data_array = ET.SubElement(points, 'DataArray',
|
|
type='Float32',
|
|
NumberOfComponents='3',
|
|
format='ascii')
|
|
# Flatten vertices to space-separated string
|
|
verts_flat = trimesh.vertices.flatten()
|
|
points_data_array.text = ' '.join(map(str, verts_flat))
|
|
|
|
# PointData section (scalar fields)
|
|
point_data = ET.SubElement(piece, 'PointData')
|
|
|
|
# Add vertex attributes as scalar arrays
|
|
if hasattr(trimesh, 'vertex_attributes') and trimesh.vertex_attributes:
|
|
for attr_name, attr_values in trimesh.vertex_attributes.items():
|
|
print(f"[_export_mesh_with_scalars_vtp] Adding scalar field: {attr_name}")
|
|
scalar_array = ET.SubElement(point_data, 'DataArray',
|
|
type='Float32',
|
|
Name=attr_name,
|
|
format='ascii')
|
|
scalar_array.text = ' '.join(map(str, attr_values.flatten()))
|
|
|
|
# Polys section (faces/triangles)
|
|
polys = ET.SubElement(piece, 'Polys')
|
|
|
|
# Connectivity: vertex indices for each face
|
|
connectivity = ET.SubElement(polys, 'DataArray',
|
|
type='Int32',
|
|
Name='connectivity',
|
|
format='ascii')
|
|
faces_flat = trimesh.faces.flatten()
|
|
connectivity.text = ' '.join(map(str, faces_flat))
|
|
|
|
# Offsets: cumulative count of indices (each triangle has 3 vertices)
|
|
offsets = ET.SubElement(polys, 'DataArray',
|
|
type='Int32',
|
|
Name='offsets',
|
|
format='ascii')
|
|
offset_values = [(i + 1) * 3 for i in range(num_faces)]
|
|
offsets.text = ' '.join(map(str, offset_values))
|
|
|
|
# Write to file with pretty formatting
|
|
tree = ET.ElementTree(vtk_file)
|
|
ET.indent(tree, space=' ')
|
|
tree.write(filepath, encoding='utf-8', xml_declaration=True)
|
|
|
|
print(f"[_export_mesh_with_scalars_vtp] Export complete: {num_verts} vertices, {num_faces} faces")
|
|
|
|
|
|
class PreviewMeshNode:
|
|
"""
|
|
Preview mesh with interactive 3D viewer.
|
|
|
|
Displays mesh in an interactive Three.js viewer with orbit controls.
|
|
Allows rotating, panning, and zooming to inspect the mesh geometry.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"trimesh": ("TRIMESH",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "preview_mesh"
|
|
CATEGORY = "geompack/visualization"
|
|
|
|
def preview_mesh(self, trimesh):
|
|
"""
|
|
Export mesh to GLB and prepare for 3D preview.
|
|
|
|
Args:
|
|
trimesh: Input trimesh_module.Trimesh object
|
|
|
|
Returns:
|
|
dict: UI data for frontend widget
|
|
"""
|
|
print(f"[PreviewMesh] Preparing preview: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
|
|
|
# Generate unique filename
|
|
filename = f"preview_{uuid.uuid4().hex[:8]}.glb"
|
|
|
|
# Use ComfyUI's output directory
|
|
if COMFYUI_OUTPUT_FOLDER:
|
|
filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename)
|
|
else:
|
|
filepath = os.path.join(tempfile.gettempdir(), filename)
|
|
|
|
# Export to GLB (best format for Three.js)
|
|
try:
|
|
trimesh.export(filepath, file_type='glb')
|
|
print(f"[PreviewMesh] Exported to: {filepath}")
|
|
except Exception as e:
|
|
print(f"[PreviewMesh] Export failed: {e}")
|
|
# Fallback to OBJ
|
|
filename = filename.replace('.glb', '.obj')
|
|
filepath = filepath.replace('.glb', '.obj')
|
|
trimesh.export(filepath, file_type='obj')
|
|
print(f"[PreviewMesh] Exported to OBJ: {filepath}")
|
|
|
|
# Calculate bounding box info for camera setup
|
|
bounds = trimesh.bounds
|
|
extents = trimesh.extents
|
|
max_extent = max(extents)
|
|
|
|
# Return metadata for frontend widget
|
|
return {
|
|
"ui": {
|
|
"mesh_file": [filename],
|
|
"vertex_count": [len(trimesh.vertices)],
|
|
"face_count": [len(trimesh.faces)],
|
|
"bounds_min": [bounds[0].tolist()],
|
|
"bounds_max": [bounds[1].tolist()],
|
|
"extents": [extents.tolist()],
|
|
"max_extent": [float(max_extent)],
|
|
}
|
|
}
|
|
|
|
|
|
class PreviewMeshVTKNode:
|
|
"""
|
|
Preview mesh with VTK.js scientific visualization viewer.
|
|
|
|
Displays mesh in an interactive VTK.js viewer with trackball controls.
|
|
Better for scientific visualization, mesh analysis, and large datasets.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"trimesh": ("TRIMESH",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "preview_mesh_vtk"
|
|
CATEGORY = "geompack/visualization"
|
|
|
|
def preview_mesh_vtk(self, trimesh):
|
|
"""
|
|
Export mesh to STL and prepare for VTK.js preview.
|
|
|
|
Args:
|
|
trimesh: Input trimesh_module.Trimesh object
|
|
|
|
Returns:
|
|
dict: UI data for frontend widget
|
|
"""
|
|
print(f"[PreviewMeshVTK] Preparing preview: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
|
|
|
# Generate unique filename
|
|
filename = f"preview_vtk_{uuid.uuid4().hex[:8]}.stl"
|
|
|
|
# Use ComfyUI's output directory
|
|
if COMFYUI_OUTPUT_FOLDER:
|
|
filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename)
|
|
else:
|
|
filepath = os.path.join(tempfile.gettempdir(), filename)
|
|
|
|
# Export to STL (native format for VTK.js)
|
|
try:
|
|
trimesh.export(filepath, file_type='stl')
|
|
print(f"[PreviewMeshVTK] Exported to: {filepath}")
|
|
except Exception as e:
|
|
print(f"[PreviewMeshVTK] Export failed: {e}")
|
|
# Fallback to OBJ
|
|
filename = filename.replace('.stl', '.obj')
|
|
filepath = filepath.replace('.stl', '.obj')
|
|
trimesh.export(filepath, file_type='obj')
|
|
print(f"[PreviewMeshVTK] Exported to OBJ: {filepath}")
|
|
|
|
# Calculate bounding box info for camera setup
|
|
bounds = trimesh.bounds
|
|
extents = trimesh.extents
|
|
max_extent = max(extents)
|
|
|
|
# Check if mesh is watertight
|
|
is_watertight = trimesh.is_watertight
|
|
|
|
# Calculate volume and area (only if watertight)
|
|
volume = None
|
|
area = None
|
|
try:
|
|
if is_watertight:
|
|
volume = float(trimesh.volume)
|
|
area = float(trimesh.area)
|
|
except Exception as e:
|
|
print(f"[PreviewMeshVTK] Could not calculate volume/area: {e}")
|
|
|
|
# Get field names (vertex/face data arrays)
|
|
field_names = []
|
|
if hasattr(trimesh, 'vertex_attributes') and trimesh.vertex_attributes:
|
|
field_names.extend([f"vertex.{k}" for k in trimesh.vertex_attributes.keys()])
|
|
if hasattr(trimesh, 'face_attributes') and trimesh.face_attributes:
|
|
field_names.extend([f"face.{k}" for k in trimesh.face_attributes.keys()])
|
|
|
|
# Return metadata for frontend widget
|
|
ui_data = {
|
|
"mesh_file": [filename],
|
|
"vertex_count": [len(trimesh.vertices)],
|
|
"face_count": [len(trimesh.faces)],
|
|
"bounds_min": [bounds[0].tolist()],
|
|
"bounds_max": [bounds[1].tolist()],
|
|
"extents": [extents.tolist()],
|
|
"max_extent": [float(max_extent)],
|
|
"is_watertight": [bool(is_watertight)],
|
|
}
|
|
|
|
# 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:
|
|
ui_data["field_names"] = [field_names]
|
|
|
|
print(f"[PreviewMeshVTK] Mesh info: watertight={is_watertight}, volume={volume}, area={area}, fields={len(field_names)}")
|
|
|
|
return {"ui": ui_data}
|
|
|
|
|
|
class PreviewMeshVTKHidableMenuNode:
|
|
"""
|
|
Preview mesh with VTK.js scientific visualization viewer (with hidable menu).
|
|
|
|
Displays mesh in an interactive VTK.js viewer with collapsible controls menu.
|
|
Provides a cleaner interface with toggle button to show/hide the control options.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"trimesh": ("TRIMESH",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "preview_mesh_vtk_hidable"
|
|
CATEGORY = "geompack/visualization"
|
|
|
|
def preview_mesh_vtk_hidable(self, trimesh):
|
|
"""
|
|
Export mesh to STL and prepare for VTK.js preview with hidable menu.
|
|
|
|
Args:
|
|
trimesh: Input trimesh_module.Trimesh object
|
|
|
|
Returns:
|
|
dict: UI data for frontend widget
|
|
"""
|
|
print(f"[PreviewMeshVTKHidable] Preparing preview: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
|
|
|
# Generate unique filename
|
|
filename = f"preview_vtk_hidable_{uuid.uuid4().hex[:8]}.stl"
|
|
|
|
# Use ComfyUI's output directory
|
|
if COMFYUI_OUTPUT_FOLDER:
|
|
filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename)
|
|
else:
|
|
filepath = os.path.join(tempfile.gettempdir(), filename)
|
|
|
|
# Export to STL (native format for VTK.js)
|
|
try:
|
|
trimesh.export(filepath, file_type='stl')
|
|
print(f"[PreviewMeshVTKHidable] Exported to: {filepath}")
|
|
except Exception as e:
|
|
print(f"[PreviewMeshVTKHidable] Export failed: {e}")
|
|
# Fallback to OBJ
|
|
filename = filename.replace('.stl', '.obj')
|
|
filepath = filepath.replace('.stl', '.obj')
|
|
trimesh.export(filepath, file_type='obj')
|
|
print(f"[PreviewMeshVTKHidable] Exported to OBJ: {filepath}")
|
|
|
|
# Calculate bounding box info for camera setup
|
|
bounds = trimesh.bounds
|
|
extents = trimesh.extents
|
|
max_extent = max(extents)
|
|
|
|
# Check if mesh is watertight
|
|
is_watertight = trimesh.is_watertight
|
|
|
|
# Calculate volume and area (only if watertight)
|
|
volume = None
|
|
area = None
|
|
try:
|
|
if is_watertight:
|
|
volume = float(trimesh.volume)
|
|
area = float(trimesh.area)
|
|
except Exception as e:
|
|
print(f"[PreviewMeshVTKHidable] Could not calculate volume/area: {e}")
|
|
|
|
# Get field names (vertex/face data arrays)
|
|
field_names = []
|
|
if hasattr(trimesh, 'vertex_attributes') and trimesh.vertex_attributes:
|
|
field_names.extend([f"vertex.{k}" for k in trimesh.vertex_attributes.keys()])
|
|
if hasattr(trimesh, 'face_attributes') and trimesh.face_attributes:
|
|
field_names.extend([f"face.{k}" for k in trimesh.face_attributes.keys()])
|
|
|
|
# Return metadata for frontend widget
|
|
ui_data = {
|
|
"mesh_file": [filename],
|
|
"vertex_count": [len(trimesh.vertices)],
|
|
"face_count": [len(trimesh.faces)],
|
|
"bounds_min": [bounds[0].tolist()],
|
|
"bounds_max": [bounds[1].tolist()],
|
|
"extents": [extents.tolist()],
|
|
"max_extent": [float(max_extent)],
|
|
"is_watertight": [bool(is_watertight)],
|
|
}
|
|
|
|
# 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:
|
|
ui_data["field_names"] = [field_names]
|
|
|
|
print(f"[PreviewMeshVTKHidable] Mesh info: watertight={is_watertight}, volume={volume}, area={area}, fields={len(field_names)}")
|
|
|
|
return {"ui": ui_data}
|
|
|
|
|
|
class PreviewMeshVTKFiltersNode:
|
|
"""
|
|
Preview mesh with VTK.js scientific visualization viewer including filters.
|
|
|
|
Displays mesh in an interactive VTK.js viewer with trackball controls
|
|
and filter controls (smoothing, outline, cutting plane).
|
|
Better for scientific visualization, mesh analysis, and large datasets.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"trimesh": ("TRIMESH",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "preview_mesh_vtk_filters"
|
|
CATEGORY = "geompack/visualization"
|
|
|
|
def preview_mesh_vtk_filters(self, trimesh):
|
|
"""
|
|
Export mesh to STL and prepare for VTK.js preview with filters.
|
|
|
|
Args:
|
|
trimesh: Input trimesh_module.Trimesh object
|
|
|
|
Returns:
|
|
dict: UI data for frontend widget
|
|
"""
|
|
print(f"[PreviewMeshVTKFilters] Preparing preview: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
|
|
|
# Check if mesh has vertex attributes (scalar fields)
|
|
has_vertex_attrs = hasattr(trimesh, 'vertex_attributes') and len(trimesh.vertex_attributes) > 0
|
|
|
|
print(f"[PreviewMeshVTKFilters] hasattr(trimesh, 'vertex_attributes'): {hasattr(trimesh, 'vertex_attributes')}")
|
|
if hasattr(trimesh, 'vertex_attributes'):
|
|
print(f"[PreviewMeshVTKFilters] trimesh.vertex_attributes: {trimesh.vertex_attributes.keys()}")
|
|
print(f"[PreviewMeshVTKFilters] Number of vertex attributes: {len(trimesh.vertex_attributes)}")
|
|
|
|
# Generate unique filename
|
|
# Use VTP format if we have vertex attributes, otherwise STL
|
|
if has_vertex_attrs:
|
|
filename = f"preview_vtk_filters_{uuid.uuid4().hex[:8]}.vtp"
|
|
file_format = 'vtp'
|
|
print(f"[PreviewMeshVTKFilters] Mesh has vertex attributes, using VTP format")
|
|
else:
|
|
filename = f"preview_vtk_filters_{uuid.uuid4().hex[:8]}.stl"
|
|
file_format = 'stl'
|
|
print(f"[PreviewMeshVTKFilters] No vertex attributes found, using STL format")
|
|
|
|
# Use ComfyUI's output directory
|
|
if COMFYUI_OUTPUT_FOLDER:
|
|
filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename)
|
|
else:
|
|
filepath = os.path.join(tempfile.gettempdir(), filename)
|
|
|
|
# Export mesh with vertex attributes
|
|
try:
|
|
if file_format == 'vtp':
|
|
# Export to VTK PolyData format (XML) which preserves vertex attributes
|
|
_export_mesh_with_scalars_vtp(trimesh, filepath)
|
|
print(f"[PreviewMeshVTKFilters] Exported VTP with {len(trimesh.vertex_attributes)} scalar fields to: {filepath}")
|
|
else:
|
|
# Export to STL (no scalar data)
|
|
trimesh.export(filepath, file_type='stl')
|
|
print(f"[PreviewMeshVTKFilters] Exported to STL: {filepath}")
|
|
except Exception as e:
|
|
print(f"[PreviewMeshVTKFilters] Export failed: {e}")
|
|
# Fallback to OBJ
|
|
filename = filename.replace('.vtp', '.obj').replace('.stl', '.obj')
|
|
filepath = filepath.replace('.vtp', '.obj').replace('.stl', '.obj')
|
|
trimesh.export(filepath, file_type='obj')
|
|
print(f"[PreviewMeshVTKFilters] Exported to OBJ: {filepath}")
|
|
|
|
# Calculate bounding box info for camera setup
|
|
bounds = trimesh.bounds
|
|
extents = trimesh.extents
|
|
max_extent = max(extents)
|
|
|
|
# Check if mesh is watertight
|
|
is_watertight = trimesh.is_watertight
|
|
|
|
# Calculate volume and area (only if watertight)
|
|
volume = None
|
|
area = None
|
|
try:
|
|
if is_watertight:
|
|
volume = float(trimesh.volume)
|
|
area = float(trimesh.area)
|
|
except Exception as e:
|
|
print(f"[PreviewMeshVTKFilters] Could not calculate volume/area: {e}")
|
|
|
|
# Get field names (vertex/face data arrays)
|
|
field_names = []
|
|
if hasattr(trimesh, 'vertex_attributes') and trimesh.vertex_attributes:
|
|
field_names.extend([f"vertex.{k}" for k in trimesh.vertex_attributes.keys()])
|
|
if hasattr(trimesh, 'face_attributes') and trimesh.face_attributes:
|
|
field_names.extend([f"face.{k}" for k in trimesh.face_attributes.keys()])
|
|
|
|
# Return metadata for frontend widget
|
|
ui_data = {
|
|
"mesh_file": [filename],
|
|
"vertex_count": [len(trimesh.vertices)],
|
|
"face_count": [len(trimesh.faces)],
|
|
"bounds_min": [bounds[0].tolist()],
|
|
"bounds_max": [bounds[1].tolist()],
|
|
"extents": [extents.tolist()],
|
|
"max_extent": [float(max_extent)],
|
|
"is_watertight": [bool(is_watertight)],
|
|
}
|
|
|
|
# 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:
|
|
ui_data["field_names"] = [field_names]
|
|
|
|
print(f"[PreviewMeshVTKFilters] Mesh info: watertight={is_watertight}, volume={volume}, area={area}, fields={len(field_names)}")
|
|
|
|
return {"ui": ui_data}
|
|
|
|
|
|
class PreviewMeshVTKFieldsNode:
|
|
"""
|
|
Preview mesh with VTK.js viewer optimized for scalar field visualization.
|
|
|
|
Specifically designed for visualizing vertex scalar fields (like boundary edges,
|
|
curvature, distance fields, etc.) with color mapping. Always exports as VTP
|
|
format to preserve scalar data.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"trimesh": ("TRIMESH",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "preview_mesh_vtk_fields"
|
|
CATEGORY = "geompack/visualization"
|
|
|
|
def preview_mesh_vtk_fields(self, trimesh):
|
|
"""
|
|
Export mesh with scalar fields to VTP and prepare for VTK.js preview.
|
|
|
|
Args:
|
|
trimesh: Input trimesh_module.Trimesh object
|
|
|
|
Returns:
|
|
dict: UI data for frontend widget
|
|
"""
|
|
print(f"[PreviewMeshVTKFields] Preparing preview: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
|
|
|
# Check if mesh has vertex attributes (scalar fields)
|
|
has_vertex_attrs = hasattr(trimesh, 'vertex_attributes') and len(trimesh.vertex_attributes) > 0
|
|
|
|
if hasattr(trimesh, 'vertex_attributes'):
|
|
print(f"[PreviewMeshVTKFields] trimesh.vertex_attributes: {trimesh.vertex_attributes.keys()}")
|
|
print(f"[PreviewMeshVTKFields] Number of vertex attributes: {len(trimesh.vertex_attributes)}")
|
|
|
|
if not has_vertex_attrs:
|
|
print(f"[PreviewMeshVTKFields] WARNING: No vertex attributes found. This node is for scalar field visualization.")
|
|
|
|
# Always use VTP format for this node (designed for scalar fields)
|
|
filename = f"preview_vtk_fields_{uuid.uuid4().hex[:8]}.vtp"
|
|
|
|
# Use ComfyUI's output directory
|
|
if COMFYUI_OUTPUT_FOLDER:
|
|
filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename)
|
|
else:
|
|
filepath = os.path.join(tempfile.gettempdir(), filename)
|
|
|
|
# Export mesh with vertex attributes as VTP
|
|
try:
|
|
_export_mesh_with_scalars_vtp(trimesh, filepath)
|
|
num_fields = len(trimesh.vertex_attributes) if has_vertex_attrs else 0
|
|
print(f"[PreviewMeshVTKFields] Exported VTP with {num_fields} scalar fields to: {filepath}")
|
|
except Exception as e:
|
|
print(f"[PreviewMeshVTKFields] Export failed: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
raise RuntimeError(f"Failed to export mesh with scalar fields: {e}")
|
|
|
|
# Calculate bounding box info for camera setup
|
|
bounds = trimesh.bounds
|
|
extents = trimesh.extents
|
|
max_extent = max(extents)
|
|
|
|
# Check if mesh is watertight
|
|
is_watertight = trimesh.is_watertight
|
|
|
|
# Get field names (vertex/face data arrays)
|
|
field_names = []
|
|
if hasattr(trimesh, 'vertex_attributes') and trimesh.vertex_attributes:
|
|
field_names.extend(list(trimesh.vertex_attributes.keys()))
|
|
if hasattr(trimesh, 'face_attributes') and trimesh.face_attributes:
|
|
field_names.extend([f"face.{k}" for k in trimesh.face_attributes.keys()])
|
|
|
|
# Return metadata for frontend widget
|
|
ui_data = {
|
|
"mesh_file": [filename],
|
|
"vertex_count": [len(trimesh.vertices)],
|
|
"face_count": [len(trimesh.faces)],
|
|
"bounds_min": [bounds[0].tolist()],
|
|
"bounds_max": [bounds[1].tolist()],
|
|
"extents": [extents.tolist()],
|
|
"max_extent": [float(max_extent)],
|
|
"is_watertight": [bool(is_watertight)],
|
|
"field_names": [field_names],
|
|
}
|
|
|
|
print(f"[PreviewMeshVTKFields] Scalar fields: {field_names}")
|
|
|
|
return {"ui": ui_data}
|
|
|
|
|
|
class PreviewBoundingBoxesVTKNode:
|
|
"""
|
|
Preview mesh with bounding boxes using VTK.js viewer.
|
|
|
|
Displays the original mesh with wireframe bounding boxes overlaid,
|
|
showing the segmentation results from P3-SAM or other bbox-generating nodes.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"trimesh": ("TRIMESH",),
|
|
"bboxes": ("BBOXES_3D",),
|
|
"line_width": ("FLOAT", {
|
|
"default": 2.0,
|
|
"min": 0.5,
|
|
"max": 10.0,
|
|
"step": 0.5,
|
|
"display": "slider",
|
|
"tooltip": "Thickness of bounding box wireframe lines in pixels. Rendered by VTK.js."
|
|
}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "preview_bboxes_vtk"
|
|
CATEGORY = "geompack/visualization"
|
|
|
|
def preview_bboxes_vtk(self, trimesh, bboxes, line_width):
|
|
"""
|
|
Export mesh with bounding boxes to VTP and prepare for VTK.js preview.
|
|
|
|
Args:
|
|
trimesh: Input trimesh_module.Trimesh object
|
|
bboxes: BBOXES_3D dict with 'bboxes' (array [N, 2, 3]) and 'num_parts'
|
|
line_width: Thickness of wireframe lines in pixels (for VTK.js rendering)
|
|
|
|
Returns:
|
|
dict: UI data for frontend widget
|
|
"""
|
|
if not BBOX_VIZ_AVAILABLE:
|
|
raise RuntimeError(
|
|
"create_bbox_visualization not available. "
|
|
"Make sure ComfyUI-Hunyuan3D-Part is installed."
|
|
)
|
|
|
|
# Extract bboxes array from BBOXES_3D dictionary
|
|
if isinstance(bboxes, dict):
|
|
bboxes_array = bboxes['bboxes']
|
|
num_parts = bboxes.get('num_parts', len(bboxes_array))
|
|
else:
|
|
# Fallback for raw array
|
|
bboxes_array = bboxes
|
|
num_parts = len(bboxes_array)
|
|
|
|
print(f"[PreviewBoundingBoxesVTK] Preparing preview with {num_parts} bounding boxes")
|
|
print(f"[PreviewBoundingBoxesVTK] Mesh: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces")
|
|
print(f"[PreviewBoundingBoxesVTK] Line width: {line_width}px")
|
|
|
|
# Create scene with mesh and bounding boxes
|
|
# Use Path3D wireframes (rendered as lines in VTK.js)
|
|
scene = create_bbox_visualization(trimesh, bboxes_array, use_tubes=False)
|
|
|
|
# Generate unique filename - use VTP which supports wireframes
|
|
filename = f"preview_bboxes_vtk_{uuid.uuid4().hex[:8]}.vtp"
|
|
|
|
# Use ComfyUI's output directory
|
|
if COMFYUI_OUTPUT_FOLDER:
|
|
filepath = os.path.join(COMFYUI_OUTPUT_FOLDER, filename)
|
|
else:
|
|
filepath = os.path.join(tempfile.gettempdir(), filename)
|
|
|
|
# Export scene to VTP (preserves wireframes)
|
|
try:
|
|
# Use custom VTP exporter that preserves both mesh and wireframe lines
|
|
export_scene_to_vtp(scene, filepath)
|
|
print(f"[PreviewBoundingBoxesVTK] Exported scene with {num_parts} bounding boxes to: {filepath}")
|
|
except Exception as e:
|
|
print(f"[PreviewBoundingBoxesVTK] VTP export failed: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
# Fallback to STL with tubes
|
|
print(f"[PreviewBoundingBoxesVTK] Falling back to STL with tube meshes")
|
|
scene = create_bbox_visualization(trimesh, bboxes_array, use_tubes=True)
|
|
filename = filename.replace('.vtp', '.stl')
|
|
filepath = filepath.replace('.vtp', '.stl')
|
|
scene.export(filepath, file_type='stl')
|
|
print(f"[PreviewBoundingBoxesVTK] Exported to: {filepath}")
|
|
|
|
# Calculate bounding box info for camera setup
|
|
# Use the original mesh bounds for camera positioning
|
|
bounds = trimesh.bounds
|
|
extents = trimesh.extents
|
|
max_extent = max(extents)
|
|
|
|
# Return metadata for frontend widget
|
|
ui_data = {
|
|
"mesh_file": [filename],
|
|
"vertex_count": [len(trimesh.vertices)],
|
|
"face_count": [len(trimesh.faces)],
|
|
"bounds_min": [bounds[0].tolist()],
|
|
"bounds_max": [bounds[1].tolist()],
|
|
"extents": [extents.tolist()],
|
|
"max_extent": [float(max_extent)],
|
|
"num_bboxes": [num_parts],
|
|
"line_width": [float(line_width)],
|
|
}
|
|
|
|
print(f"[PreviewBoundingBoxesVTK] Created visualization with {num_parts} bounding boxes")
|
|
|
|
return {"ui": ui_data}
|
|
|
|
|
|
# Node mappings
|
|
NODE_CLASS_MAPPINGS = {
|
|
"GeomPackPreviewMesh": PreviewMeshNode,
|
|
"GeomPackPreviewMeshVTK": PreviewMeshVTKNode,
|
|
"GeomPackPreviewMeshVTKHidableMenu": PreviewMeshVTKHidableMenuNode,
|
|
"GeomPackPreviewMeshVTKFilters": PreviewMeshVTKFiltersNode,
|
|
"GeomPackPreviewMeshVTKFields": PreviewMeshVTKFieldsNode,
|
|
"GeomPackPreviewBoundingBoxesVTK": PreviewBoundingBoxesVTKNode,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"GeomPackPreviewMesh": "Preview Mesh (3D)",
|
|
"GeomPackPreviewMeshVTK": "Preview Mesh (VTK)",
|
|
"GeomPackPreviewMeshVTKHidableMenu": "Preview Mesh VTK (Hidable Menu)",
|
|
"GeomPackPreviewMeshVTKFilters": "Preview Mesh (VTK with Filters)",
|
|
"GeomPackPreviewMeshVTKFields": "Preview Mesh (VTK with Fields)",
|
|
"GeomPackPreviewBoundingBoxesVTK": "Preview Bounding Boxes (VTK)",
|
|
}
|