diff --git a/nodes/visualization/_vtp_export.py b/nodes/visualization/_vtp_export.py index 04edcbba..29dbcae9 100644 --- a/nodes/visualization/_vtp_export.py +++ b/nodes/visualization/_vtp_export.py @@ -1,8 +1,8 @@ """ -Helper function for exporting mesh to VTK PolyData XML format (.vtp) with vertex scalar attributes. +Helper function for exporting mesh to VTK PolyData XML format (.vtp) with scalar attributes. -VTP format preserves vertex attributes (scalar fields) which can be visualized -in VTK.js with color mapping. +VTP format preserves vertex attributes (PointData) and face attributes (CellData) +which can be visualized in VTK.js with color mapping. """ import trimesh as trimesh_module @@ -11,13 +11,13 @@ import xml.etree.ElementTree as ET def export_mesh_with_scalars_vtp(trimesh: trimesh_module.Trimesh, filepath: str): """ - Export trimesh to VTK PolyData XML format (.vtp) with vertex scalar attributes. + Export trimesh to VTK PolyData XML format (.vtp) with scalar attributes. - VTP format preserves vertex attributes (scalar fields) which can be visualized - in VTK.js with color mapping. + VTP format preserves vertex attributes (PointData) and face attributes (CellData) + which can be visualized in VTK.js with color mapping. Args: - trimesh: Trimesh object with optional vertex_attributes + trimesh: Trimesh object with optional vertex_attributes and face_attributes filepath: Output .vtp file path """ print(f"[_export_mesh_with_scalars_vtp] Exporting to VTP: {filepath}") @@ -54,6 +54,19 @@ def export_mesh_with_scalars_vtp(trimesh: trimesh_module.Trimesh, filepath: str) format='ascii') scalar_array.text = ' '.join(map(str, attr_values.flatten())) + # CellData section (face attributes) + cell_data = ET.SubElement(piece, 'CellData') + + # Add face attributes as scalar arrays + if hasattr(trimesh, 'face_attributes') and trimesh.face_attributes: + for attr_name, attr_values in trimesh.face_attributes.items(): + print(f"[_export_mesh_with_scalars_vtp] Adding face field: {attr_name}") + scalar_array = ET.SubElement(cell_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') diff --git a/nodes/visualization/preview_mesh_vtk_pointcloud.py b/nodes/visualization/preview_mesh_vtk_pointcloud.py index 92d0cc83..05a1da7d 100644 --- a/nodes/visualization/preview_mesh_vtk_pointcloud.py +++ b/nodes/visualization/preview_mesh_vtk_pointcloud.py @@ -2,8 +2,8 @@ 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. +curvature, distance fields, etc.) and face scalar fields (like segmentation labels). +Always exports as VTP format to preserve scalar data. """ import trimesh as trimesh_module @@ -24,8 +24,8 @@ class PreviewMeshVTKPointCloudNode: 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. + curvature, distance fields, etc.) and face scalar fields (like segmentation labels). + Always exports as VTP format to preserve scalar data. """ @classmethod @@ -53,15 +53,20 @@ class PreviewMeshVTKPointCloudNode: """ print(f"[PreviewMeshVTKPointCloud] Preparing preview: {len(trimesh.vertices)} vertices, {len(trimesh.faces)} faces") - # Check if mesh has vertex attributes (scalar fields) + # Check if mesh has vertex or face attributes (scalar fields) has_vertex_attrs = hasattr(trimesh, 'vertex_attributes') and len(trimesh.vertex_attributes) > 0 + has_face_attrs = hasattr(trimesh, 'face_attributes') and len(trimesh.face_attributes) > 0 if hasattr(trimesh, 'vertex_attributes'): print(f"[PreviewMeshVTKPointCloud] trimesh.vertex_attributes: {trimesh.vertex_attributes.keys()}") print(f"[PreviewMeshVTKPointCloud] Number of vertex attributes: {len(trimesh.vertex_attributes)}") - if not has_vertex_attrs: - print(f"[PreviewMeshVTKPointCloud] WARNING: No vertex attributes found. This node is for scalar field visualization.") + if hasattr(trimesh, 'face_attributes'): + print(f"[PreviewMeshVTKPointCloud] trimesh.face_attributes: {trimesh.face_attributes.keys()}") + print(f"[PreviewMeshVTKPointCloud] Number of face attributes: {len(trimesh.face_attributes)}") + + if not has_vertex_attrs and not has_face_attrs: + print(f"[PreviewMeshVTKPointCloud] WARNING: No vertex or face attributes found. This node is for scalar field visualization.") # Always use VTP format for this node (designed for scalar fields) filename = f"preview_vtk_pointcloud_{uuid.uuid4().hex[:8]}.vtp" @@ -72,11 +77,12 @@ class PreviewMeshVTKPointCloudNode: else: filepath = os.path.join(tempfile.gettempdir(), filename) - # Export mesh with vertex attributes as VTP + # Export mesh with vertex and face 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"[PreviewMeshVTKPointCloud] Exported VTP with {num_fields} scalar fields to: {filepath}") + num_vertex_fields = len(trimesh.vertex_attributes) if has_vertex_attrs else 0 + num_face_fields = len(trimesh.face_attributes) if has_face_attrs else 0 + print(f"[PreviewMeshVTKPointCloud] Exported VTP with {num_vertex_fields} vertex fields and {num_face_fields} face fields to: {filepath}") except Exception as e: print(f"[PreviewMeshVTKPointCloud] Export failed: {e}") import traceback diff --git a/web/viewer_vtk_fields.html b/web/viewer_vtk_fields.html index 9a8ed5e6..08d1712c 100644 --- a/web/viewer_vtk_fields.html +++ b/web/viewer_vtk_fields.html @@ -456,15 +456,36 @@ for (let i = 0; i < numArrays; i++) { const array = pointData.getArray(i); const arrayName = array.getName(); - console.log(`[GeomPack VTK Fields Viewer] Found scalar field: ${arrayName}`); + console.log(`[GeomPack VTK Fields Viewer] Found point field: ${arrayName}`); const option = document.createElement('option'); - option.value = arrayName; - option.textContent = arrayName; + option.value = `point:${arrayName}`; + option.textContent = `${arrayName} (vertex)`; fieldSelector.appendChild(option); } fieldLabel.style.display = 'flex'; - console.log('[GeomPack VTK Fields Viewer] Scalar field selector populated with', numArrays, 'fields'); - } else { + console.log('[GeomPack VTK Fields Viewer] Point data selector populated with', numArrays, 'fields'); + } + + // Check for scalar fields in cell data (face attributes) + const cellData = polydata.getCellData(); + const numCellArrays = cellData.getNumberOfArrays(); + console.log('[GeomPack VTK Fields Viewer] Cell data arrays:', numCellArrays); + + if (numCellArrays > 0) { + for (let i = 0; i < numCellArrays; i++) { + const array = cellData.getArray(i); + const arrayName = array.getName(); + console.log(`[GeomPack VTK Fields Viewer] Found face field: ${arrayName}`); + const option = document.createElement('option'); + option.value = `cell:${arrayName}`; + option.textContent = `${arrayName} (face)`; + fieldSelector.appendChild(option); + } + fieldLabel.style.display = 'flex'; + console.log('[GeomPack VTK Fields Viewer] Cell data selector populated with', numCellArrays, 'fields'); + } + + if (numArrays === 0 && numCellArrays === 0) { fieldLabel.style.display = 'none'; console.log('[GeomPack VTK Fields Viewer] No scalar fields found'); } @@ -472,7 +493,7 @@ // Populate colormap selector const colormapSelector = document.getElementById('colormapSelector'); const colormapLabel = document.getElementById('colormapLabel'); - if (numArrays > 0) { + if (numArrays > 0 || numCellArrays > 0) { colormapSelector.innerHTML = ''; // Add popular colormaps first @@ -929,24 +950,54 @@ if (fieldName && currentReader) { // Get the polydata with scalar field (from mapper's input, not reader) const polydata = mapper.getInputData(); - const pointData = polydata.getPointData(); - const scalarArray = pointData.getArrayByName(fieldName); - if (scalarArray) { - // Set the scalar array as active scalars - pointData.setActiveScalars(fieldName); + // Check if this is a cell data field (face attribute) or point data field (vertex attribute) + if (fieldName.startsWith('cell:')) { + // Handle cell data (face attributes) + const actualFieldName = fieldName.substring(5); // Remove "cell:" prefix + const cellData = polydata.getCellData(); + const scalarArray = cellData.getArrayByName(actualFieldName); - // Configure mapper for scalar coloring - mapper.setScalarVisibility(true); - mapper.setScalarModeToUsePointData(); - mapper.setScalarRange(scalarArray.getRange()); + if (scalarArray) { + // Set the scalar array as active scalars + cellData.setActiveScalars(actualFieldName); - // Apply colormap using the updateColormap function - updateColormap(); + // Configure mapper for scalar coloring using cell data + mapper.setScalarVisibility(true); + mapper.setScalarModeToUseCellData(); + mapper.setScalarRange(scalarArray.getRange()); - console.log(`[GeomPack VTK Fields Viewer] Scalar field '${fieldName}' activated, range:`, scalarArray.getRange()); + // Apply colormap using the updateColormap function + updateColormap(); + + console.log(`[GeomPack VTK Fields Viewer] Face field '${actualFieldName}' activated, range:`, scalarArray.getRange()); + } else { + console.warn(`[GeomPack VTK Fields Viewer] Face field '${actualFieldName}' not found`); + } + } else if (fieldName.startsWith('point:')) { + // Handle point data (vertex attributes) + const actualFieldName = fieldName.substring(6); // Remove "point:" prefix + const pointData = polydata.getPointData(); + const scalarArray = pointData.getArrayByName(actualFieldName); + + if (scalarArray) { + // Set the scalar array as active scalars + pointData.setActiveScalars(actualFieldName); + + // Configure mapper for scalar coloring using point data + mapper.setScalarVisibility(true); + mapper.setScalarModeToUsePointData(); + mapper.setScalarRange(scalarArray.getRange()); + + // Apply colormap using the updateColormap function + updateColormap(); + + console.log(`[GeomPack VTK Fields Viewer] Vertex field '${actualFieldName}' activated, range:`, scalarArray.getRange()); + } else { + console.warn(`[GeomPack VTK Fields Viewer] Vertex field '${actualFieldName}' not found`); + } } else { - console.warn(`[GeomPack VTK Fields Viewer] Scalar field '${fieldName}' not found`); + console.warn(`[GeomPack VTK Fields Viewer] Unknown field type: ${fieldName}`); } } else { // Disable scalar coloring, use solid color diff --git a/web/viewer_vtk_filters.html b/web/viewer_vtk_filters.html index ff8105a1..dabcdd67 100644 --- a/web/viewer_vtk_filters.html +++ b/web/viewer_vtk_filters.html @@ -575,15 +575,36 @@ for (let i = 0; i < numArrays; i++) { const array = pointData.getArray(i); const arrayName = array.getName(); - console.log(`[GeomPack VTK Filters] Found scalar field: ${arrayName}`); + console.log(`[GeomPack VTK Filters] Found point field: ${arrayName}`); const option = document.createElement('option'); - option.value = arrayName; - option.textContent = arrayName; + option.value = `point:${arrayName}`; + option.textContent = `${arrayName} (vertex)`; scalarSelector.appendChild(option); } scalarLabel.style.display = 'flex'; - console.log('[GeomPack VTK Filters] Scalar field selector populated with', numArrays, 'fields'); - } else { + console.log('[GeomPack VTK Filters] Point data selector populated with', numArrays, 'fields'); + } + + // Check for scalar fields in cell data (face attributes) + const cellData = polydata.getCellData(); + const numCellArrays = cellData.getNumberOfArrays(); + console.log('[GeomPack VTK Filters] Cell data arrays:', numCellArrays); + + if (numCellArrays > 0) { + for (let i = 0; i < numCellArrays; i++) { + const array = cellData.getArray(i); + const arrayName = array.getName(); + console.log(`[GeomPack VTK Filters] Found face field: ${arrayName}`); + const option = document.createElement('option'); + option.value = `cell:${arrayName}`; + option.textContent = `${arrayName} (face)`; + scalarSelector.appendChild(option); + } + scalarLabel.style.display = 'flex'; + console.log('[GeomPack VTK Filters] Cell data selector populated with', numCellArrays, 'fields'); + } + + if (numArrays === 0 && numCellArrays === 0) { scalarLabel.style.display = 'none'; console.log('[GeomPack VTK Filters] No scalar fields found'); } @@ -591,7 +612,7 @@ // Populate colormap selector const colormapSelector = document.getElementById('colormapSelector'); const colormapLabel = document.getElementById('colormapLabel'); - if (numArrays > 0) { + if (numArrays > 0 || numCellArrays > 0) { colormapSelector.innerHTML = ''; // Add popular colormaps first @@ -1096,24 +1117,54 @@ if (fieldName && currentReader) { // Get the polydata with scalar field (from mapper's input, not reader) const polydata = mapper.getInputData(); - const pointData = polydata.getPointData(); - const scalarArray = pointData.getArrayByName(fieldName); - if (scalarArray) { - // Set the scalar array as active scalars - pointData.setActiveScalars(fieldName); + // Check if this is a cell data field (face attribute) or point data field (vertex attribute) + if (fieldName.startsWith('cell:')) { + // Handle cell data (face attributes) + const actualFieldName = fieldName.substring(5); // Remove "cell:" prefix + const cellData = polydata.getCellData(); + const scalarArray = cellData.getArrayByName(actualFieldName); - // Configure mapper for scalar coloring - mapper.setScalarVisibility(true); - mapper.setScalarModeToUsePointData(); - mapper.setScalarRange(scalarArray.getRange()); + if (scalarArray) { + // Set the scalar array as active scalars + cellData.setActiveScalars(actualFieldName); - // Apply colormap using the updateColormap function - updateColormap(); + // Configure mapper for scalar coloring using cell data + mapper.setScalarVisibility(true); + mapper.setScalarModeToUseCellData(); + mapper.setScalarRange(scalarArray.getRange()); - console.log(`[GeomPack VTK Filters] Scalar field '${fieldName}' activated, range:`, scalarArray.getRange()); + // Apply colormap using the updateColormap function + updateColormap(); + + console.log(`[GeomPack VTK Filters] Face field '${actualFieldName}' activated, range:`, scalarArray.getRange()); + } else { + console.warn(`[GeomPack VTK Filters] Face field '${actualFieldName}' not found`); + } + } else if (fieldName.startsWith('point:')) { + // Handle point data (vertex attributes) + const actualFieldName = fieldName.substring(6); // Remove "point:" prefix + const pointData = polydata.getPointData(); + const scalarArray = pointData.getArrayByName(actualFieldName); + + if (scalarArray) { + // Set the scalar array as active scalars + pointData.setActiveScalars(actualFieldName); + + // Configure mapper for scalar coloring using point data + mapper.setScalarVisibility(true); + mapper.setScalarModeToUsePointData(); + mapper.setScalarRange(scalarArray.getRange()); + + // Apply colormap using the updateColormap function + updateColormap(); + + console.log(`[GeomPack VTK Filters] Vertex field '${actualFieldName}' activated, range:`, scalarArray.getRange()); + } else { + console.warn(`[GeomPack VTK Filters] Vertex field '${actualFieldName}' not found`); + } } else { - console.warn(`[GeomPack VTK Filters] Scalar field '${fieldName}' not found`); + console.warn(`[GeomPack VTK Filters] Unknown field type: ${fieldName}`); } } else { // Disable scalar coloring, use solid color