using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; using CtrEditor.ObjetosSim; using System.Diagnostics; namespace CtrEditor.Services { /// /// Gestor centralizado de capturas de pantalla del canvas. /// Proporciona funcionalidades para capturar: /// 1. Objetos específicos por ID /// 2. Áreas específicas en coordenadas de metros /// 3. Todo el canvas /// /// Utiliza la misma lógica que ObjectManipulationManager para calcular /// las dimensiones reales de los objetos en el canvas. /// public class ScreenshotManager { private readonly MainViewModel _mainViewModel; private readonly Canvas _canvas; private readonly string _defaultScreenshotsDirectory; public ScreenshotManager(MainViewModel mainViewModel, Canvas canvas) { _mainViewModel = mainViewModel ?? throw new ArgumentNullException(nameof(mainViewModel)); _canvas = canvas ?? throw new ArgumentNullException(nameof(canvas)); _defaultScreenshotsDirectory = System.IO.Path.Combine(EstadoPersistente.Instance.directorio, "screenshots"); } #region Public Methods /// /// Captura screenshot de objetos específicos por sus IDs /// /// Array de IDs de objetos a capturar /// Padding adicional alrededor de los objetos en metros /// Nombre del archivo (opcional) /// Si incluir imagen de fondo /// Si guardar el archivo /// Si retornar la imagen como base64 /// Resultado de la captura public ScreenshotResult CaptureObjects( string[] objectIds, float paddingMeters = 0.5f, string filename = null, bool includeBackground = false, bool saveToFile = true, bool returnBase64 = true) { try { if (objectIds == null || objectIds.Length == 0) throw new ArgumentException("objectIds no puede estar vacío"); // Buscar objetos por ID var targetObjects = FindObjectsByIds(objectIds); if (!targetObjects.Any()) throw new ArgumentException($"No se encontraron objetos visibles con los IDs: {string.Join(", ", objectIds)}"); // SOLUCIÓN 2: Usar bounding box mejorado para objetos con dimensiones 0.0 var boundingBox = CalculateObjectsBoundingBoxImproved(targetObjects); // Aplicar padding var captureArea = new ScreenshotArea { Left = boundingBox.Left - paddingMeters, Top = boundingBox.Top - paddingMeters, Width = boundingBox.Width + (paddingMeters * 2), Height = boundingBox.Height + (paddingMeters * 2) }; // Asegurar dimensiones mínimas captureArea.Width = Math.Max(captureArea.Width, 0.1f); captureArea.Height = Math.Max(captureArea.Height, 0.1f); // Capturar la imagen var bitmap = CaptureCanvasArea(captureArea, includeBackground); // Preparar resultado var result = new ScreenshotResult { Success = true, Bitmap = bitmap, CaptureType = ScreenshotType.Objects, CapturedObjects = targetObjects.Select(obj => new CapturedObjectInfo { Id = obj.Id.Value.ToString(), Name = obj.Nombre, Type = obj.GetType().Name, Left = obj.Left, Top = obj.Top, Width = obj.Ancho, Height = obj.Alto, CenterX = obj.Left + obj.Ancho / 2, CenterY = obj.Top + obj.Alto / 2 }).ToList(), BoundingBox = new AreaInfo { Left = boundingBox.Left, Top = boundingBox.Top, Width = boundingBox.Width, Height = boundingBox.Height, CenterX = boundingBox.Left + boundingBox.Width / 2, CenterY = boundingBox.Top + boundingBox.Height / 2 }, CaptureArea = new AreaInfo { Left = captureArea.Left, Top = captureArea.Top, Width = captureArea.Width, Height = captureArea.Height, CenterX = captureArea.Left + captureArea.Width / 2, CenterY = captureArea.Top + captureArea.Height / 2 }, PaddingMeters = paddingMeters, IncludeBackground = includeBackground }; // Guardar archivo y/o generar base64 return ProcessScreenshotOutput(result, filename, saveToFile, returnBase64); } catch (Exception ex) { return new ScreenshotResult { Success = false, ErrorMessage = ex.Message, ErrorType = ex.GetType().Name }; } } /// /// Captura screenshot de un área específica en coordenadas de metros /// /// Coordenada X izquierda en metros /// Coordenada Y superior en metros /// Ancho en metros /// Alto en metros /// Nombre del archivo (opcional) /// Si incluir imagen de fondo /// Si guardar el archivo /// Si retornar la imagen como base64 /// Resultado de la captura public ScreenshotResult CaptureArea( float left, float top, float width, float height, string filename = null, bool includeBackground = false, bool saveToFile = true, bool returnBase64 = true) { try { if (width <= 0 || height <= 0) throw new ArgumentException("Width y height deben ser mayores que 0"); var captureArea = new ScreenshotArea { Left = left, Top = top, Width = width, Height = height }; var bitmap = CaptureCanvasArea(captureArea, includeBackground); var result = new ScreenshotResult { Success = true, Bitmap = bitmap, CaptureType = ScreenshotType.Area, CaptureArea = new AreaInfo { Left = left, Top = top, Width = width, Height = height, CenterX = left + width / 2, CenterY = top + height / 2 }, IncludeBackground = includeBackground }; return ProcessScreenshotOutput(result, filename, saveToFile, returnBase64); } catch (Exception ex) { return new ScreenshotResult { Success = false, ErrorMessage = ex.Message, ErrorType = ex.GetType().Name }; } } /// /// Captura screenshot de un área centrada en coordenadas específicas /// /// Coordenada X del centro en metros /// Coordenada Y del centro en metros /// Ancho en metros /// Alto en metros /// Nombre del archivo (opcional) /// Si incluir imagen de fondo /// Si guardar el archivo /// Si retornar la imagen como base64 /// Resultado de la captura public ScreenshotResult CaptureCenteredArea( float centerX, float centerY, float width, float height, string filename = null, bool includeBackground = false, bool saveToFile = true, bool returnBase64 = true) { var left = centerX - width / 2; var top = centerY - height / 2; return CaptureArea(left, top, width, height, filename, includeBackground, saveToFile, returnBase64); } /// /// Captura screenshot de todo el canvas con auto-zoom inteligente /// /// Nombre del archivo (opcional) /// Si incluir imagen de fondo /// Si guardar el archivo /// Si retornar la imagen como base64 /// Si usar auto-zoom a área con objetos (por defecto true) /// Resultado de la captura public ScreenshotResult CaptureFullCanvas( string filename = null, bool includeBackground = true, bool saveToFile = true, bool returnBase64 = true, bool autoZoom = true) { try { // SOLUCIÓN 1: Auto-zoom inteligente if (autoZoom) { var visibleObjects = _mainViewModel.ObjetosSimulables .Where(o => o.Show_On_This_Page).ToList(); if (visibleObjects.Any()) { try { var bounds = CalculateObjectsBoundingBoxImproved(visibleObjects); var padding = Math.Max(2.0f, Math.Max(bounds.Width, bounds.Height) * 0.2f); Trace.WriteLine($"[ScreenshotManager] Auto-zoom detectado: área {bounds.Width:F1}×{bounds.Height:F1}m, padding {padding:F1}m"); return CaptureArea( bounds.Left - padding, bounds.Top - padding, bounds.Width + (padding * 2), bounds.Height + (padding * 2), filename, includeBackground, saveToFile, returnBase64); } catch (Exception ex) { Trace.WriteLine($"[ScreenshotManager] Auto-zoom falló: {ex.Message}, usando canvas completo"); } } } // Fallback a canvas completo var bitmap = CaptureEntireCanvas(includeBackground); var canvasWidthMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)_canvas.ActualWidth); var canvasHeightMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)_canvas.ActualHeight); var result = new ScreenshotResult { Success = true, Bitmap = bitmap, CaptureType = ScreenshotType.FullCanvas, CaptureArea = new AreaInfo { Left = 0, Top = 0, Width = canvasWidthMeters, Height = canvasHeightMeters, CenterX = canvasWidthMeters / 2, CenterY = canvasHeightMeters / 2 }, IncludeBackground = includeBackground }; return ProcessScreenshotOutput(result, filename, saveToFile, returnBase64); } catch (Exception ex) { return new ScreenshotResult { Success = false, ErrorMessage = ex.Message, ErrorType = ex.GetType().Name }; } } #endregion #region Private Helper Methods /// /// Busca objetos por sus IDs /// private List FindObjectsByIds(string[] objectIds) { var allObjects = _mainViewModel.ObjetosSimulables.ToList(); var result = new List(); foreach (var objectId in objectIds) { var obj = allObjects.FirstOrDefault(o => o.Id.Value.ToString() == objectId); if (obj != null && obj.Show_On_This_Page) { result.Add(obj); Console.WriteLine($"DEBUG: Object {objectId} ADDED - Type: {obj.GetType().Name}, Left: {obj.Left}, Top: {obj.Top}"); } else { Console.WriteLine($"DEBUG: Object {objectId} REJECTED - Found: {obj != null}, Show_On_This_Page: {obj?.Show_On_This_Page}"); } } Console.WriteLine($"DEBUG: Total objects found: {result.Count}"); return result; } /// /// Calcula el bounding box de un conjunto de objetos usando coordenadas directas /// private ScreenshotArea CalculateObjectsBoundingBox(List objects) { if (!objects.Any()) throw new ArgumentException("La lista de objetos no puede estar vacía"); float left = float.MaxValue; float top = float.MaxValue; float right = float.MinValue; float bottom = float.MinValue; foreach (var obj in objects) { // Usar coordenadas directas del objeto (ya están en metros) float objLeft = obj.Left; float objTop = obj.Top; float objRight = obj.Left + obj.Ancho; float objBottom = obj.Top + obj.Alto; left = Math.Min(left, objLeft); top = Math.Min(top, objTop); right = Math.Max(right, objRight); bottom = Math.Max(bottom, objBottom); Console.WriteLine($"DEBUG: Object {obj.Id.Value} bounds - L:{objLeft} T:{objTop} R:{objRight} B:{objBottom}"); } if (left == float.MaxValue) // No se encontraron objetos válidos throw new InvalidOperationException("No se encontraron objetos válidos para calcular el bounding box"); Console.WriteLine($"DEBUG: Final bounding box - L:{left} T:{top} W:{right - left} H:{bottom - top}"); return new ScreenshotArea { Left = left, Top = top, Width = right - left, Height = bottom - top }; } /// /// SOLUCIÓN 2: Calcula el bounding box mejorado que maneja objetos con dimensiones 0.0 /// private ScreenshotArea CalculateObjectsBoundingBoxImproved(List objects) { if (!objects.Any()) throw new ArgumentException("La lista de objetos no puede estar vacía"); float left = float.MaxValue; float top = float.MaxValue; float right = float.MinValue; float bottom = float.MinValue; foreach (var obj in objects) { // Obtener dimensiones reales del objeto float objWidth = obj.Ancho; float objHeight = obj.Alto; // SOLUCIÓN 2: Manejar objetos con dimensiones 0.0 (como botellas) if (objWidth <= 0.0f || objHeight <= 0.0f) { // Usar dimensiones por defecto basadas en el tipo de objeto if (obj.GetType().Name == "osBotella") { objWidth = 0.2f; // 20cm por defecto para botellas objHeight = 0.2f; } else { objWidth = Math.Max(objWidth, 0.1f); // Mínimo 10cm objHeight = Math.Max(objHeight, 0.1f); } } float objLeft = obj.Left - objWidth / 2; // Centrar el objeto float objTop = obj.Top - objHeight / 2; float objRight = obj.Left + objWidth / 2; float objBottom = obj.Top + objHeight / 2; left = Math.Min(left, objLeft); top = Math.Min(top, objTop); right = Math.Max(right, objRight); bottom = Math.Max(bottom, objBottom); Trace.WriteLine($"[ScreenshotManager] Object {obj.Id.Value} ({obj.GetType().Name}) - Original: {obj.Ancho}×{obj.Alto}, Used: {objWidth}×{objHeight}"); } if (left == float.MaxValue) throw new InvalidOperationException("No se encontraron objetos válidos para calcular el bounding box"); var finalWidth = right - left; var finalHeight = bottom - top; // Asegurar dimensiones mínimas if (finalWidth < 0.5f) { var center = (left + right) / 2; left = center - 0.25f; right = center + 0.25f; finalWidth = 0.5f; } if (finalHeight < 0.5f) { var center = (top + bottom) / 2; top = center - 0.25f; bottom = center + 0.25f; finalHeight = 0.5f; } Trace.WriteLine($"[ScreenshotManager] Bounding box mejorado - L:{left:F2} T:{top:F2} W:{finalWidth:F2} H:{finalHeight:F2}"); return new ScreenshotArea { Left = left, Top = top, Width = finalWidth, Height = finalHeight }; } /// /// Captura un área específica del canvas /// private RenderTargetBitmap CaptureCanvasArea(ScreenshotArea area, bool includeBackground) { // Asegurar que el canvas esté actualizado _canvas.UpdateLayout(); // Convertir área de metros a píxeles var pixelLeft = PixelToMeter.Instance.calc.MetersToPixels(area.Left); var pixelTop = PixelToMeter.Instance.calc.MetersToPixels(area.Top); var pixelWidth = PixelToMeter.Instance.calc.MetersToPixels(area.Width); var pixelHeight = PixelToMeter.Instance.calc.MetersToPixels(area.Height); var captureRect = new Rect(pixelLeft, pixelTop, pixelWidth, pixelHeight); // Validar que el área esté dentro del canvas var canvasRect = new Rect(0, 0, _canvas.ActualWidth, _canvas.ActualHeight); if (!canvasRect.IntersectsWith(captureRect)) throw new ArgumentException("El área especificada está fuera del canvas"); // Intersectar con el canvas para evitar áreas fuera de límites captureRect.Intersect(canvasRect); return CaptureCanvasRect(captureRect, includeBackground); } /// /// Captura todo el canvas /// private RenderTargetBitmap CaptureEntireCanvas(bool includeBackground) { _canvas.UpdateLayout(); var canvasRect = new Rect(0, 0, _canvas.ActualWidth, _canvas.ActualHeight); return CaptureCanvasRect(canvasRect, includeBackground); } /// /// SOLUCIÓN 3: Captura un rectángulo específico del canvas usando DrawingVisual (sin VisualBrush) /// private RenderTargetBitmap CaptureCanvasRect(Rect captureRect, bool includeBackground) { if (captureRect.Width <= 0 || captureRect.Height <= 0) throw new ArgumentException("Las dimensiones de captura deben ser mayores que 0"); // SOLUCIÓN 3: Reducir scaleFactor de 2.0 a 1.0 para evitar problemas de memoria var scaleFactor = 1.0; // Reducido para mayor estabilidad var renderWidth = Math.Max(1, (int)(captureRect.Width * scaleFactor)); var renderHeight = Math.Max(1, (int)(captureRect.Height * scaleFactor)); var dpi = 96 * scaleFactor; Trace.WriteLine($"[ScreenshotManager] Capturando {renderWidth}×{renderHeight} px (scaleFactor: {scaleFactor})"); var renderBitmap = new RenderTargetBitmap( renderWidth, renderHeight, dpi, dpi, PixelFormats.Pbgra32); // SOLUCIÓN 3 CORREGIDA: Enfoque híbrido - Canvas temporal con VisualBrush mejorado var tempCanvas = new Canvas() { Width = captureRect.Width * scaleFactor, Height = captureRect.Height * scaleFactor, Background = includeBackground ? _canvas.Background : Brushes.White }; tempCanvas.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor); // Forzar actualización de layout del canvas principal antes de capturar _canvas.UpdateLayout(); _canvas.InvalidateVisual(); // Renderizar elementos con VisualBrush mejorado int elementCount = 0; foreach (UIElement child in _canvas.Children) { if (ValidateElementForCapture(child, captureRect)) { try { var left = Canvas.GetLeft(child); var top = Canvas.GetTop(child); // SOLUCIÓN 3: Validación mejorada de coordenadas if (double.IsNaN(left)) left = 0; if (double.IsNaN(top)) top = 0; if (double.IsInfinity(left) || double.IsInfinity(top)) continue; var elementRect = new Rect(left, top, child.RenderSize.Width, child.RenderSize.Height); if (captureRect.IntersectsWith(elementRect)) { // Forzar actualización del UserControl child.UpdateLayout(); child.InvalidateVisual(); // Usar VisualBrush mejorado con manejo robusto de errores try { var visualBrush = new VisualBrush(child) { Stretch = Stretch.None, AlignmentX = AlignmentX.Left, AlignmentY = AlignmentY.Top, ViewboxUnits = BrushMappingMode.Absolute, Viewbox = new Rect(0, 0, child.RenderSize.Width, child.RenderSize.Height) }; var rect = new Rectangle() { Width = child.RenderSize.Width * scaleFactor, Height = child.RenderSize.Height * scaleFactor, Fill = visualBrush }; Canvas.SetLeft(rect, (elementRect.X - captureRect.X) * scaleFactor); Canvas.SetTop(rect, (elementRect.Y - captureRect.Y) * scaleFactor); tempCanvas.Children.Add(rect); elementCount++; Trace.WriteLine($"[ScreenshotManager] Capturado UserControl: {child.GetType().Name} en ({elementRect.X:F1}, {elementRect.Y:F1})"); } catch (Exception brushEx) { Trace.WriteLine($"[ScreenshotManager] Error con VisualBrush para {child.GetType().Name}: {brushEx.Message}"); // Fallback: Rectángulo de placeholder var placeholder = new Rectangle() { Width = child.RenderSize.Width * scaleFactor, Height = child.RenderSize.Height * scaleFactor, Fill = Brushes.LightGray, Stroke = Brushes.Red, StrokeThickness = 2 }; Canvas.SetLeft(placeholder, (elementRect.X - captureRect.X) * scaleFactor); Canvas.SetTop(placeholder, (elementRect.Y - captureRect.Y) * scaleFactor); tempCanvas.Children.Add(placeholder); } } } catch (Exception ex) { Trace.WriteLine($"[ScreenshotManager] Error procesando elemento {child.GetType().Name}: {ex.Message}"); } } } Trace.WriteLine($"[ScreenshotManager] Renderizados {elementCount} elementos UserControl"); // Renderizar canvas temporal mejorado var scaledSize = new Size(captureRect.Width * scaleFactor, captureRect.Height * scaleFactor); tempCanvas.Measure(scaledSize); tempCanvas.Arrange(new Rect(0, 0, scaledSize.Width, scaledSize.Height)); tempCanvas.UpdateLayout(); renderBitmap.Render(tempCanvas); return renderBitmap; } /// /// SOLUCIÓN 3: Valida si un elemento debe ser incluido en la captura /// private bool ValidateElementForCapture(UIElement element, Rect captureRect) { if (element.Visibility != Visibility.Visible) return false; if (element.RenderSize.Width <= 0 || element.RenderSize.Height <= 0) return false; var left = Canvas.GetLeft(element); var top = Canvas.GetTop(element); // Validación mejorada de posiciones if (double.IsNaN(left) || double.IsNaN(top) || double.IsInfinity(left) || double.IsInfinity(top)) return false; var elementRect = new Rect(left, top, element.RenderSize.Width, element.RenderSize.Height); return captureRect.IntersectsWith(elementRect); } /// /// Procesa la salida del screenshot (guardar archivo y/o generar base64) /// private ScreenshotResult ProcessScreenshotOutput(ScreenshotResult result, string filename, bool saveToFile, bool returnBase64) { try { // Generar nombre de archivo si no se proporcionó if (string.IsNullOrEmpty(filename)) { var typePrefix = result.CaptureType.ToString().ToLower(); filename = $"{typePrefix}_screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.png"; } // Asegurar extensión .png if (!filename.ToLower().EndsWith(".png")) filename += ".png"; // Guardar archivo si se solicita if (saveToFile) { Directory.CreateDirectory(_defaultScreenshotsDirectory); var fullPath = System.IO.Path.IsPathRooted(filename) ? filename : System.IO.Path.Combine(_defaultScreenshotsDirectory, filename); var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(result.Bitmap)); using (var fileStream = new FileStream(fullPath, FileMode.Create)) { encoder.Save(fileStream); } var fileInfo = new FileInfo(fullPath); result.FilePath = fullPath; result.FileName = System.IO.Path.GetFileName(fullPath); result.Directory = System.IO.Path.GetDirectoryName(fullPath); result.FileSizeBytes = fileInfo.Length; } // Generar base64 solo si se cumplen las condiciones restrictivas if (returnBase64 && ShouldGenerateBase64(result)) { using (var memoryStream = new MemoryStream()) { var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(result.Bitmap)); encoder.Save(memoryStream); result.Base64Data = Convert.ToBase64String(memoryStream.ToArray()); } } else if (returnBase64) { // Informar por qué no se generó el base64 result.Base64SkipReason = GetBase64SkipReason(result); } result.Timestamp = DateTime.Now; return result; } catch (Exception ex) { result.Success = false; result.ErrorMessage = $"Error procesando salida: {ex.Message}"; result.ErrorType = ex.GetType().Name; return result; } } /// /// Determina si se debe generar base64 basado en las restricciones de tamaño y cantidad de objetos /// private bool ShouldGenerateBase64(ScreenshotResult result) { const int MAX_DIMENSION = 1024; // Solo permitir base64 para objetos únicos if (result.CaptureType == ScreenshotType.Objects && (result.CapturedObjects?.Count != 1)) { return false; } // Verificar dimensiones de la imagen if (result.Bitmap != null) { if (result.Bitmap.PixelWidth > MAX_DIMENSION || result.Bitmap.PixelHeight > MAX_DIMENSION) { return false; } } // No generar base64 para canvas completo o áreas grandes if (result.CaptureType == ScreenshotType.FullCanvas) { return false; } return true; } /// /// Obtiene la razón por la cual no se generó el base64 /// private string GetBase64SkipReason(ScreenshotResult result) { const int MAX_DIMENSION = 1024; if (result.CaptureType == ScreenshotType.FullCanvas) { return "Base64 no generado: Canvas completo excede límites de tokens"; } if (result.CaptureType == ScreenshotType.Objects && (result.CapturedObjects?.Count != 1)) { return $"Base64 no generado: Solo se permite para objetos únicos (encontrados: {result.CapturedObjects?.Count ?? 0})"; } if (result.Bitmap != null && (result.Bitmap.PixelWidth > MAX_DIMENSION || result.Bitmap.PixelHeight > MAX_DIMENSION)) { return $"Base64 no generado: Imagen excede {MAX_DIMENSION}x{MAX_DIMENSION} píxeles (actual: {result.Bitmap.PixelWidth}x{result.Bitmap.PixelHeight})"; } return "Base64 no generado: Condiciones no cumplidas"; } #endregion } #region Data Classes /// /// Resultado de una operación de screenshot /// public class ScreenshotResult { public bool Success { get; set; } public string ErrorMessage { get; set; } public string ErrorType { get; set; } public RenderTargetBitmap Bitmap { get; set; } public ScreenshotType CaptureType { get; set; } public string FilePath { get; set; } public string FileName { get; set; } public string Directory { get; set; } public long FileSizeBytes { get; set; } public string Base64Data { get; set; } public string Base64SkipReason { get; set; } public DateTime Timestamp { get; set; } public bool IncludeBackground { get; set; } public List CapturedObjects { get; set; } = new List(); public AreaInfo BoundingBox { get; set; } public AreaInfo CaptureArea { get; set; } public float PaddingMeters { get; set; } } /// /// Información de un objeto capturado /// public class CapturedObjectInfo { public string Id { get; set; } public string Name { get; set; } public string Type { get; set; } public float Left { get; set; } public float Top { get; set; } public float Width { get; set; } public float Height { get; set; } public float CenterX { get; set; } public float CenterY { get; set; } } /// /// Información de un área /// public class AreaInfo { public float Left { get; set; } public float Top { get; set; } public float Width { get; set; } public float Height { get; set; } public float CenterX { get; set; } public float CenterY { get; set; } } /// /// Área de screenshot en coordenadas de metros /// internal class ScreenshotArea { public float Left { get; set; } public float Top { get; set; } public float Width { get; set; } public float Height { get; set; } } /// /// Tipos de captura de screenshot /// public enum ScreenshotType { Objects, Area, FullCanvas } #endregion }