AnsysLink/backend/pymechanical/simple_mesh_visualizer.py
2025-08-11 13:58:59 +08:00

85 lines
13 KiB
Python

"""
Simple Mesh Visualizer for CAE Mesh Generator
This module provides basic mesh visualization export functionality using real ANSYS Mechanical
PyMechanical API to generate mesh images for display purposes.
"""
import logging
import time
import os
from typing import Dict, Any, Optional
from datetime import datetime
from dataclasses import dataclass
from pathlib import Path
logger = logging.getLogger(__name__)
@dataclass
class VisualizationResult:
"""Result of mesh visualization export"""
success: bool = False
image_path: str = None
image_size: tuple = None
file_size: int = 0
export_time: float = 0.0
error_message: str = None
warnings: list = None
class SimpleMeshVisualizer:
"""
Simple mesh visualizer using real ANSYS Mechanical integration
This class provides basic mesh visualization export functionality
for displaying generated meshes to users.
"""
def __init__(self, mechanical_session, output_dir: str = "static/visualizations"):
"""
Initialize simple mesh visualizer
Args:
mechanical_session: Active PyMechanical session
output_dir: Directory to save visualization images
"""
if mechanical_session is None:
raise ValueError("Mechanical session is required for mesh visualization")
self.mechanical = mechanical_session
self.output_dir = Path(output_dir)
# Create output directory if it doesn't exist
self.output_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Simple Mesh Visualizer initialized with output directory: {self.output_dir}")
def export_mesh_image(self, filename: str = None, width: int = 1280, height: int = 720) -> VisualizationResult:
"""
Export mesh visualization as an image
Args:
filename: Output filename (optional, auto-generated if None)
width: Image width in pixels
height: Image height in pixels
Returns:
VisualizationResult with export information
"""
result = VisualizationResult()
start_time = time.time()
try:
logger.info(f"Starting mesh visualization export ({width}x{height})...")
# Generate filename if not provided
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"mesh_visualization_{timestamp}.png"
# Ensure filename has .png extension
if not filename.lower().endswith('.png'):
filename += '.png'
output_path = self.output_dir / filename
# Create PyMechanical script for mesh visualization
visualization_script = f'''\n# Export mesh visualization\ntry:\n # Get the mesh object\n mesh = Model.Mesh\n \n # Check if mesh exists\n if mesh is None:\n print("VISUALIZATION_ERROR:No mesh available for visualization")\n else:\n # Set up graphics for export\n try:\n # Get graphics object\n graphics = ExtAPI.Graphics\n \n # Set camera to isometric view\n graphics.Camera.SetFit()\n \n # Set image size\n graphics.ImageOptions.Width = {width}\n graphics.ImageOptions.Height = {height}\n \n # Set background to white\n graphics.Appearance.BackgroundColor = 255, 255, 255\n \n # Show mesh\n mesh.GenerateMesh()\n \n # Export image\n output_file = r"{str(output_path).replace(chr(92), chr(92) + chr(92))}"\n graphics.ExportImage(output_file)\n \n print("VISUALIZATION_SUCCESS:" + output_file)\n \n except Exception as graphics_error:\n # Fallback: try alternative visualization method\n try:\n # Alternative method using Tree operations\n tree = ExtAPI.DataModel.Tree\n mesh_object = tree.Find("Mesh")[0] if tree.Find("Mesh") else None\n \n if mesh_object:\n mesh_object.Activate()\n \n # Try to export using different method\n output_file = r"{str(output_path).replace(chr(92), chr(92) + chr(92))}"\n \n # Simple export attempt\n ExtAPI.Graphics.ExportImage(output_file)\n print("VISUALIZATION_SUCCESS:" + output_file)\n else:\n print("VISUALIZATION_ERROR:Could not find mesh object")\n \n except Exception as fallback_error:\n print("VISUALIZATION_ERROR:" + str(fallback_error))\n \nexcept Exception as e:\n print("VISUALIZATION_ERROR:" + str(e))\n'''\n \n # Execute the visualization script\n script_result = self.mechanical.run_python_script(visualization_script)\n logger.debug(f"Visualization script result: {script_result}")\n \n # Parse the result\n if script_result:\n lines = str(script_result).split('\\n')\n for line in lines:\n if line.startswith('VISUALIZATION_SUCCESS:'):\n exported_path = line.split(':', 1)[1].strip()\n \n # Verify file was created\n if output_path.exists():\n result.success = True\n result.image_path = str(output_path)\n result.file_size = output_path.stat().st_size\n result.image_size = (width, height)\n \n logger.info(f"✓ Mesh visualization exported successfully: {output_path}")\n else:\n result.error_message = "Image file was not created despite success message"\n break\n elif line.startswith('VISUALIZATION_ERROR:'):\n result.error_message = line.split(':', 1)[1].strip()\n break\n \n # If no success/error message found, check if file exists\n if not result.success and not result.error_message:\n if output_path.exists():\n result.success = True\n result.image_path = str(output_path)\n result.file_size = output_path.stat().st_size\n result.image_size = (width, height)\n result.warnings = ["Visualization completed but with unclear status"]\n logger.warning("Visualization file exists but status unclear")\n else:\n result.error_message = "Visualization script completed but no image was generated"\n \n result.export_time = time.time() - start_time\n return result\n \n except Exception as e:\n logger.error(f"Mesh visualization export failed: {str(e)}")\n result.success = False\n result.error_message = str(e)\n result.export_time = time.time() - start_time\n return result\n \n def export_simple_mesh_preview(self) -> VisualizationResult:\n """\n Export a simple mesh preview with default settings\n \n Returns:\n VisualizationResult with export information\n """\n try:\n return self.export_mesh_image(\n filename="mesh_preview.png",\n width=800,\n height=600\n )\n except Exception as e:\n logger.error(f"Simple mesh preview export failed: {str(e)}")\n result = VisualizationResult()\n result.success = False\n result.error_message = str(e)\n return result\n \n def get_visualization_info(self) -> Dict[str, Any]:\n """\n Get information about visualization capabilities\n \n Returns:\n Dictionary with visualization information\n """\n try:\n return {\n 'visualizer_type': 'SimpleMeshVisualizer',\n 'output_directory': str(self.output_dir),\n 'supported_formats': ['PNG'],\n 'default_settings': {\n 'width': 800,\n 'height': 600,\n 'background': 'white',\n 'view': 'isometric'\n },\n 'mechanical_session_active': self.mechanical is not None,\n 'capabilities': {\n 'basic_mesh_export': True,\n 'custom_dimensions': True,\n 'multiple_views': False,\n 'quality_mapping': False,\n 'animation': False\n }\n }\n except Exception as e:\n logger.error(f"Failed to get visualization info: {str(e)}")\n return {\n 'error': str(e),\n 'visualizer_type': 'SimpleMeshVisualizer',\n 'mechanical_session_active': False\n }\n \n def cleanup_old_images(self, max_age_hours: int = 24) -> Dict[str, Any]:\n """\n Clean up old visualization images\n \n Args:\n max_age_hours: Maximum age of images to keep (in hours)\n \n Returns:\n Dictionary with cleanup results\n """\n try:\n cleanup_result = {\n 'files_removed': 0,\n 'space_freed': 0,\n 'errors': []\n }\n \n if not self.output_dir.exists():\n return cleanup_result\n \n current_time = time.time()\n max_age_seconds = max_age_hours * 3600\n \n for image_file in self.output_dir.glob('*.png'):\n try:\n file_age = current_time - image_file.stat().st_mtime\n \n if file_age > max_age_seconds:\n file_size = image_file.stat().st_size\n image_file.unlink()\n \n cleanup_result['files_removed'] += 1\n cleanup_result['space_freed'] += file_size\n \n logger.debug(f"Removed old visualization: {image_file}")\n \n except Exception as file_error:\n cleanup_result['errors'].append(f"Failed to remove {image_file}: {str(file_error)}")\n \n if cleanup_result['files_removed'] > 0:\n logger.info(f"Cleaned up {cleanup_result['files_removed']} old visualization files, freed {cleanup_result['space_freed']} bytes")\n \n return cleanup_result\n \n except Exception as e:\n logger.error(f"Cleanup failed: {str(e)}")\n return {\n 'files_removed': 0,\n 'space_freed': 0,\n 'errors': [str(e)]\n }\n \n def get_available_images(self) -> Dict[str, Any]:\n """\n Get list of available visualization images\n \n Returns:\n Dictionary with available images information\n """\n try:\n images = []\n \n if self.output_dir.exists():\n for image_file in self.output_dir.glob('*.png'):\n try:\n stat = image_file.stat()\n images.append({\n 'filename': image_file.name,\n 'path': str(image_file),\n 'size': stat.st_size,\n 'created': datetime.fromtimestamp(stat.st_ctime).isoformat(),\n 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat()\n })\n except Exception as file_error:\n logger.warning(f"Could not get info for {image_file}: {str(file_error)}")\n \n return {\n 'success': True,\n 'images': sorted(images, key=lambda x: x['modified'], reverse=True),\n 'total_images': len(images),\n 'total_size': sum(img['size'] for img in images),\n 'output_directory': str(self.output_dir)\n }\n \n except Exception as e:\n logger.error(f"Failed to get available images: {str(e)}")\n return {\n 'success': False,\n 'error': str(e),\n 'images': [],\n 'total_images': 0\n }\n