""" API routes for CAE Mesh Generator """ import os import uuid from datetime import datetime from flask import Blueprint, request, jsonify, current_app from werkzeug.utils import secure_filename from pathlib import Path from backend.models.data_models import UploadedFile, ProcessingStatus from backend.utils.file_validator import validate_step_file, get_file_info from backend.utils.state_manager import state_manager from backend.utils.mesh_processor import process_blade_mesh_with_state_updates, ProcessingStep from backend.utils.visualization_exporter import VisualizationExporter, VisualizationSettings from config import ALLOWED_EXTENSIONS, UPLOAD_FOLDER import threading # Create API blueprint api_bp = Blueprint('api', __name__) def allowed_file(filename): """Check if file extension is allowed""" return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS def get_file_size(file_path): """Get file size in bytes""" try: return os.path.getsize(file_path) except OSError: return 0 @api_bp.route('/upload', methods=['POST']) def upload_file(): """ Handle file upload POST /api/upload """ try: # Check if file is in request if 'file' not in request.files: return jsonify({ 'success': False, 'error': 'No file provided' }), 400 file = request.files['file'] # Check if file is selected if file.filename == '': return jsonify({ 'success': False, 'error': 'No file selected' }), 400 # Validate file extension if not allowed_file(file.filename): return jsonify({ 'success': False, 'error': f'Invalid file format. Only {", ".join(ALLOWED_EXTENSIONS)} files are supported.' }), 400 # Generate unique filename file_id = str(uuid.uuid4()) original_filename = secure_filename(file.filename) file_extension = Path(original_filename).suffix unique_filename = f"{file_id}{file_extension}" # Ensure upload directory exists os.makedirs(UPLOAD_FOLDER, exist_ok=True) # Save file file_path = os.path.join(UPLOAD_FOLDER, unique_filename) file.save(file_path) # Validate uploaded file is_valid, validation_error = validate_step_file(file_path) if not is_valid: # Remove invalid file try: os.remove(file_path) except: pass return jsonify({ 'success': False, 'error': f'File validation failed: {validation_error}' }), 422 # Get file information file_info = get_file_info(file_path) # Create file record uploaded_file = UploadedFile( id=file_id, filename=original_filename, file_path=file_path, upload_time=datetime.now(), status='UPLOADED' ) # Update state manager state_manager.set_current_file(uploaded_file) return jsonify({ 'success': True, 'file': uploaded_file.to_dict(), 'file_info': file_info, 'message': 'File uploaded and validated successfully' }), 200 except Exception as e: current_app.logger.error(f"File upload error: {str(e)}") return jsonify({ 'success': False, 'error': f'Upload failed: {str(e)}' }), 500 @api_bp.route('/files/current', methods=['GET']) def get_current_file(): """ Get current uploaded file information GET /api/files/current """ current_file = state_manager.get_current_file() if current_file is None: return jsonify({ 'success': False, 'message': 'No file uploaded' }), 404 return jsonify({ 'success': True, 'file': current_file.to_dict() }), 200 @api_bp.route('/mesh/status', methods=['GET']) def get_mesh_status(): """ Get current processing status GET /api/mesh/status """ processing_status = state_manager.get_processing_status() return jsonify({ 'success': True, 'status': processing_status.to_dict() }), 200 @api_bp.route('/mesh/result', methods=['GET']) def get_mesh_result(): """ Get comprehensive mesh generation result with statistics and visualization GET /api/mesh/result Query parameters: - include_visualization: bool - Include visualization data (default: false) - include_quality_details: bool - Include detailed quality metrics (default: false) - format: str - Response format (json, summary) (default: json) """ try: # Get query parameters include_visualization = request.args.get('include_visualization', 'false').lower() == 'true' include_quality_details = request.args.get('include_quality_details', 'false').lower() == 'true' response_format = request.args.get('format', 'json').lower() # Get basic mesh result mesh_result = state_manager.get_mesh_result() if mesh_result is None: return jsonify({ 'success': False, 'message': 'No mesh result available' }), 404 # Get processing status for additional context processing_status = state_manager.get_processing_status() current_file = state_manager.get_current_file() # Build comprehensive result result_data = { 'basic_info': mesh_result.to_dict(), 'processing_info': { 'status': processing_status.status if processing_status else 'unknown', 'progress_percentage': processing_status.progress_percentage if processing_status else 0, 'started_at': processing_status.start_time.isoformat() if processing_status and processing_status.start_time else None, 'completed_at': processing_status.completed_at.isoformat() if processing_status and processing_status.completed_at else None, 'total_time': (processing_status.completed_at - processing_status.start_time).total_seconds() if processing_status and processing_status.start_time and processing_status.completed_at else 0 }, 'file_info': { 'filename': current_file.filename if current_file else 'unknown', 'file_size': get_file_size(current_file.file_path) if current_file and current_file.file_path else 0, 'upload_time': current_file.upload_time.isoformat() if current_file and current_file.upload_time else None } } # Add detailed quality information if requested if include_quality_details: result_data['quality_details'] = _get_detailed_quality_info(mesh_result) # Add visualization data if requested if include_visualization: result_data['visualization'] = _get_visualization_info() # Format response based on requested format if response_format == 'summary': return jsonify({ 'success': True, 'summary': _format_result_summary(result_data) }), 200 else: return jsonify({ 'success': True, 'result': result_data, 'metadata': { 'retrieved_at': datetime.now().isoformat(), 'include_visualization': include_visualization, 'include_quality_details': include_quality_details, 'format': response_format } }), 200 except Exception as e: current_app.logger.error(f"Get mesh result error: {str(e)}") return jsonify({ 'success': False, 'error': f'Failed to retrieve mesh result: {str(e)}' }), 500 @api_bp.route('/system/state', methods=['GET']) def get_system_state(): """ Get complete system state GET /api/system/state """ system_state = state_manager.get_system_state() return jsonify({ 'success': True, 'state': system_state }), 200 @api_bp.route('/system/reset', methods=['POST']) def reset_system(): """ Reset system state POST /api/system/reset """ try: state_manager.clear_current_file() state_manager.clear_session_data() return jsonify({ 'success': True, 'message': 'System state reset successfully' }), 200 except Exception as e: current_app.logger.error(f"System reset error: {str(e)}") return jsonify({ 'success': False, 'error': f'Reset failed: {str(e)}' }), 500 @api_bp.route('/mesh/ready', methods=['GET']) def check_mesh_ready(): """ Check if system is ready for mesh generation GET /api/mesh/ready """ is_ready = state_manager.is_ready_for_processing() current_file = state_manager.get_current_file() processing_status = state_manager.get_processing_status() return jsonify({ 'success': True, 'ready': is_ready, 'file_uploaded': current_file is not None, 'processing_status': processing_status.status, 'message': 'Ready for mesh generation' if is_ready else 'Not ready for mesh generation' }), 200 @api_bp.route('/health', methods=['GET']) def health_check(): """ Health check endpoint GET /api/health """ system_state = state_manager.get_system_state() return jsonify({ 'success': True, 'message': 'CAE Mesh Generator API is running', 'timestamp': datetime.now().isoformat(), 'system_status': { 'has_file': system_state['current_file'] is not None, 'processing_status': system_state['processing_status']['status'], 'ready_for_processing': system_state['is_ready_for_processing'] } }), 200 @api_bp.route('/mesh/generate', methods=['POST']) def generate_mesh(): """ Start mesh generation for uploaded file POST /api/mesh/generate """ try: # Check if system is ready for processing if not state_manager.is_ready_for_processing(): current_file = state_manager.get_current_file() processing_status = state_manager.get_processing_status() if current_file is None: return jsonify({ 'success': False, 'error': 'No file uploaded' }), 400 elif state_manager.is_processing(): return jsonify({ 'success': False, 'error': 'Processing already in progress', 'current_status': processing_status.status }), 409 else: return jsonify({ 'success': False, 'error': f'System not ready for processing. Current status: {processing_status.status}' }), 400 # Get simulation mode from request (optional) simulation_mode = request.json.get('simulation_mode', False) if request.is_json else False # Get current file and app for background thread current_file = state_manager.get_current_file() file_path = current_file.file_path app = current_app._get_current_object() # Get the actual app instance # Start processing in background thread def background_processing(): """Background thread for mesh processing""" with app.app_context(): # Use the captured app instance try: app.logger.info(f"Starting mesh generation for file: {current_file.filename}") # Start processing status state_manager.start_processing(f"Starting mesh generation for {current_file.filename}") # Process mesh result = process_blade_mesh_with_state_updates( file_path=file_path, simulation_mode=simulation_mode ) if result.success: app.logger.info(f"✓ Mesh generation completed successfully: {result.element_count} elements") state_manager.complete_processing("Mesh generation completed successfully") else: app.logger.error(f"✗ Mesh generation failed: {result.error_message}") state_manager.set_processing_error(result.error_message) except Exception as e: app.logger.error(f"Background processing error: {str(e)}") state_manager.set_processing_error(f"Processing error: {str(e)}") # Start background thread processing_thread = threading.Thread(target=background_processing, daemon=True) processing_thread.start() return jsonify({ 'success': True, 'message': 'Mesh generation started', 'file_id': current_file.id, 'filename': current_file.filename, 'simulation_mode': simulation_mode, 'started_at': datetime.now().isoformat() }), 202 # Accepted - processing started except Exception as e: current_app.logger.error(f"Mesh generation startup error: {str(e)}") return jsonify({ 'success': False, 'error': f'Failed to start mesh generation: {str(e)}' }), 500 @api_bp.route('/mesh/progress', methods=['GET']) def get_mesh_progress(): """ Get mesh generation progress GET /api/mesh/progress """ try: processing_status = state_manager.get_processing_status() current_file = state_manager.get_current_file() response_data = { 'success': True, 'status': processing_status.status, 'message': processing_status.message, 'progress_percentage': getattr(processing_status, 'progress_percentage', 0.0), 'current_operation': getattr(processing_status, 'current_operation', None), 'last_updated': getattr(processing_status, 'last_updated', processing_status.start_time), 'file_info': { 'id': current_file.id if current_file else None, 'filename': current_file.filename if current_file else None } if current_file else None } # Add timing information if processing_status.start_time: response_data['started_at'] = processing_status.start_time.isoformat() if processing_status.status in ['COMPLETED', 'ERROR']: end_time = getattr(processing_status, 'completed_at', None) or processing_status.end_time if end_time: response_data['completed_at'] = end_time.isoformat() processing_time = (end_time - processing_status.start_time).total_seconds() response_data['processing_time'] = processing_time else: # Calculate current processing time current_time = datetime.now() processing_time = (current_time - processing_status.start_time).total_seconds() response_data['current_processing_time'] = processing_time # Add error information if failed if processing_status.status == 'ERROR' and processing_status.error_message: response_data['error_message'] = processing_status.error_message return jsonify(response_data), 200 except Exception as e: current_app.logger.error(f"Progress check error: {str(e)}") return jsonify({ 'success': False, 'error': f'Failed to get progress: {str(e)}' }), 500 @api_bp.route('/mesh/cancel', methods=['POST']) def cancel_mesh_generation(): """ Cancel ongoing mesh generation POST /api/mesh/cancel """ try: processing_status = state_manager.get_processing_status() if processing_status.status != 'PROCESSING': return jsonify({ 'success': False, 'error': f'No processing to cancel. Current status: {processing_status.status}' }), 400 # Set status to cancelled (the background thread should handle this gracefully) state_manager.set_processing_status('CANCELLED', 'Mesh generation cancelled by user') current_app.logger.info("Mesh generation cancellation requested") return jsonify({ 'success': True, 'message': 'Mesh generation cancellation requested', 'cancelled_at': datetime.now().isoformat() }), 200 except Exception as e: current_app.logger.error(f"Cancellation error: {str(e)}") return jsonify({ 'success': False, 'error': f'Failed to cancel: {str(e)}' }), 500 # Helper functions for detailed result processing def _get_detailed_quality_info(mesh_result): """ Get detailed quality information from mesh result Args: mesh_result: MeshResult object Returns: Dictionary with detailed quality information """ try: quality_details = { 'overall_score': mesh_result.quality_score, 'overall_status': mesh_result.quality_status, 'quality_breakdown': { 'element_quality': { 'score': mesh_result.quality_score, 'status': mesh_result.quality_status, 'threshold': 0.2, 'description': 'Minimum element quality measure' }, 'mesh_density': { 'elements_per_volume': mesh_result.element_count / 1000 if mesh_result.element_count > 0 else 0, 'nodes_per_element': mesh_result.node_count / mesh_result.element_count if mesh_result.element_count > 0 else 0, 'description': 'Mesh density characteristics' } }, 'recommendations': _get_quality_recommendations(mesh_result), 'quality_metrics': { 'element_count': mesh_result.element_count, 'node_count': mesh_result.node_count, 'quality_score': mesh_result.quality_score, 'generation_time': mesh_result.generation_time } } return quality_details except Exception as e: current_app.logger.error(f"Error getting detailed quality info: {str(e)}") return { 'error': f'Failed to get detailed quality info: {str(e)}', 'overall_score': mesh_result.quality_score if mesh_result else 0, 'overall_status': mesh_result.quality_status if mesh_result else 'unknown' } def _get_quality_recommendations(mesh_result): """ Generate quality improvement recommendations Args: mesh_result: MeshResult object Returns: List of recommendation strings """ recommendations = [] try: if mesh_result.quality_score < 50: recommendations.append("Consider reducing global element size for better quality") recommendations.append("Add local refinement to high-curvature areas") elif mesh_result.quality_score < 70: recommendations.append("Mesh quality is acceptable but could be improved") recommendations.append("Consider adding inflation layers for better boundary resolution") else: recommendations.append("Excellent mesh quality achieved") recommendations.append("Mesh is suitable for accurate analysis") if mesh_result.element_count > 100000: recommendations.append("High element count - consider optimizing for computational efficiency") elif mesh_result.element_count < 10000: recommendations.append("Low element count - consider refining for better accuracy") return recommendations except Exception as e: current_app.logger.error(f"Error generating recommendations: {str(e)}") return ["Unable to generate recommendations due to error"] def _get_visualization_info(): """ Get visualization information and generate images if needed Returns: Dictionary with visualization information """ try: # Initialize visualization exporter viz_exporter = VisualizationExporter(output_dir="static/visualizations") visualization_info = { 'available_views': viz_exporter.get_available_views(), 'available_formats': viz_exporter.get_available_formats(), 'default_settings': { 'width': 1280, 'height': 720, 'background': 'white', 'camera_view': 'isometric', 'format': 'PNG' }, 'images': [], 'export_summary': viz_exporter.get_export_summary() } # Try to generate basic mesh visualization try: settings = VisualizationSettings( width=800, height=600, camera_view='isometric', background_color='white' ) result = viz_exporter.export_mesh_image( filename='current_mesh_preview.png', settings=settings ) if result.success: visualization_info['images'].append({ 'type': 'mesh_preview', 'path': result.image_path, 'size': result.image_size, 'file_size': result.file_size, 'description': 'Current mesh visualization' }) except Exception as img_error: current_app.logger.warning(f"Could not generate mesh preview: {str(img_error)}") visualization_info['images'].append({ 'type': 'mesh_preview', 'error': str(img_error), 'description': 'Mesh preview generation failed' }) return visualization_info except Exception as e: current_app.logger.error(f"Error getting visualization info: {str(e)}") return { 'error': f'Failed to get visualization info: {str(e)}', 'available_views': [], 'available_formats': [], 'images': [] } def _format_result_summary(result_data): """ Format result data as a concise summary Args: result_data: Complete result data dictionary Returns: Dictionary with summary information """ try: basic_info = result_data.get('basic_info', {}) processing_info = result_data.get('processing_info', {}) file_info = result_data.get('file_info', {}) summary = { 'mesh_statistics': { 'elements': basic_info.get('element_count', 0), 'nodes': basic_info.get('node_count', 0), 'quality_score': basic_info.get('quality_score', 0), 'quality_status': basic_info.get('quality_status', 'unknown') }, 'processing_summary': { 'status': processing_info.get('status', 'unknown'), 'total_time': processing_info.get('total_time', 0), 'completed': processing_info.get('status') == 'completed' }, 'file_summary': { 'filename': file_info.get('filename', 'unknown'), 'file_size_mb': round(file_info.get('file_size', 0) / (1024 * 1024), 2) }, 'success_indicators': { 'mesh_generated': basic_info.get('element_count', 0) > 0, 'quality_acceptable': basic_info.get('quality_score', 0) >= 50, 'processing_completed': processing_info.get('status') == 'completed' } } return summary except Exception as e: current_app.logger.error(f"Error formatting result summary: {str(e)}") return { 'error': f'Failed to format summary: {str(e)}', 'mesh_statistics': {'elements': 0, 'nodes': 0, 'quality_score': 0}, 'processing_summary': {'status': 'error', 'completed': False}, 'success_indicators': {'mesh_generated': False, 'quality_acceptable': False, 'processing_completed': False} } @api_bp.route('/mesh/visualization', methods=['GET']) def get_mesh_visualization(): """ Generate and return mesh visualization GET /api/mesh/visualization Query parameters: - view: str - Camera view (isometric, front, side, top) (default: isometric) - width: int - Image width (default: 800) - height: int - Image height (default: 600) - format: str - Image format (PNG, JPG) (default: PNG) - quality_metric: str - Quality metric to visualize (optional) """ try: # Get query parameters view = request.args.get('view', 'isometric') width = int(request.args.get('width', 800)) height = int(request.args.get('height', 600)) img_format = request.args.get('format', 'PNG').upper() quality_metric = request.args.get('quality_metric', None) # Initialize visualization exporter viz_exporter = VisualizationExporter(output_dir="static/visualizations") # Create visualization settings settings = VisualizationSettings( width=width, height=height, camera_view=view, image_format=img_format, background_color='white', show_edges=True ) # Generate appropriate visualization if quality_metric: result = viz_exporter.export_quality_visualization( quality_metric=quality_metric ) else: result = viz_exporter.export_mesh_image(settings=settings) if result.success: return jsonify({ 'success': True, 'visualization': { 'image_path': result.image_path, 'image_size': result.image_size, 'file_size': result.file_size, 'export_time': result.export_time, 'settings': { 'view': view, 'width': width, 'height': height, 'format': img_format, 'quality_metric': quality_metric } }, 'warnings': result.warnings }), 200 else: return jsonify({ 'success': False, 'error': result.error_message, 'warnings': result.warnings }), 500 except Exception as e: current_app.logger.error(f"Visualization generation error: {str(e)}") return jsonify({ 'success': False, 'error': f'Failed to generate visualization: {str(e)}' }), 500 @api_bp.route('/mesh/export', methods=['POST']) def export_mesh_data(): """ Export mesh data in various formats POST /api/mesh/export JSON body: { "format": "json|summary|csv", "include_visualization": bool, "include_quality_details": bool } """ try: data = request.get_json() or {} export_format = data.get('format', 'json').lower() include_visualization = data.get('include_visualization', False) include_quality_details = data.get('include_quality_details', True) # Get mesh result mesh_result = state_manager.get_mesh_result() if mesh_result is None: return jsonify({ 'success': False, 'error': 'No mesh result available for export' }), 404 # Build export data export_data = { 'export_info': { 'format': export_format, 'exported_at': datetime.now().isoformat(), 'include_visualization': include_visualization, 'include_quality_details': include_quality_details }, 'mesh_data': mesh_result.to_dict() } # Add additional data based on options if include_quality_details: export_data['quality_details'] = _get_detailed_quality_info(mesh_result) if include_visualization: export_data['visualization'] = _get_visualization_info() # Format based on requested format if export_format == 'summary': return jsonify({ 'success': True, 'export': _format_result_summary(export_data) }), 200 elif export_format == 'csv': # For CSV format, return structured data that can be converted to CSV csv_data = { 'mesh_statistics': [ ['Metric', 'Value'], ['Elements', mesh_result.element_count], ['Nodes', mesh_result.node_count], ['Quality Score', mesh_result.quality_score], ['Quality Status', mesh_result.quality_status], ['Generation Time', mesh_result.generation_time] ] } return jsonify({ 'success': True, 'export': csv_data, 'format': 'csv' }), 200 else: return jsonify({ 'success': True, 'export': export_data }), 200 except Exception as e: current_app.logger.error(f"Export error: {str(e)}") return jsonify({ 'success': False, 'error': f'Export failed: {str(e)}' }), 500