888 lines
37 KiB
C#
888 lines
37 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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
|
||
|
||
/// <summary>
|
||
/// Captura screenshot de objetos específicos por sus IDs
|
||
/// </summary>
|
||
/// <param name="objectIds">Array de IDs de objetos a capturar</param>
|
||
/// <param name="paddingMeters">Padding adicional alrededor de los objetos en metros</param>
|
||
/// <param name="filename">Nombre del archivo (opcional)</param>
|
||
/// <param name="includeBackground">Si incluir imagen de fondo</param>
|
||
/// <param name="saveToFile">Si guardar el archivo</param>
|
||
/// <param name="returnBase64">Si retornar la imagen como base64</param>
|
||
/// <returns>Resultado de la captura</returns>
|
||
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
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Captura screenshot de un área específica en coordenadas de metros
|
||
/// </summary>
|
||
/// <param name="left">Coordenada X izquierda en metros</param>
|
||
/// <param name="top">Coordenada Y superior en metros</param>
|
||
/// <param name="width">Ancho en metros</param>
|
||
/// <param name="height">Alto en metros</param>
|
||
/// <param name="filename">Nombre del archivo (opcional)</param>
|
||
/// <param name="includeBackground">Si incluir imagen de fondo</param>
|
||
/// <param name="saveToFile">Si guardar el archivo</param>
|
||
/// <param name="returnBase64">Si retornar la imagen como base64</param>
|
||
/// <returns>Resultado de la captura</returns>
|
||
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
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Captura screenshot de un área centrada en coordenadas específicas
|
||
/// </summary>
|
||
/// <param name="centerX">Coordenada X del centro en metros</param>
|
||
/// <param name="centerY">Coordenada Y del centro en metros</param>
|
||
/// <param name="width">Ancho en metros</param>
|
||
/// <param name="height">Alto en metros</param>
|
||
/// <param name="filename">Nombre del archivo (opcional)</param>
|
||
/// <param name="includeBackground">Si incluir imagen de fondo</param>
|
||
/// <param name="saveToFile">Si guardar el archivo</param>
|
||
/// <param name="returnBase64">Si retornar la imagen como base64</param>
|
||
/// <returns>Resultado de la captura</returns>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Captura screenshot de todo el canvas con auto-zoom inteligente
|
||
/// </summary>
|
||
/// <param name="filename">Nombre del archivo (opcional)</param>
|
||
/// <param name="includeBackground">Si incluir imagen de fondo</param>
|
||
/// <param name="saveToFile">Si guardar el archivo</param>
|
||
/// <param name="returnBase64">Si retornar la imagen como base64</param>
|
||
/// <param name="autoZoom">Si usar auto-zoom a área con objetos (por defecto true)</param>
|
||
/// <returns>Resultado de la captura</returns>
|
||
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
|
||
|
||
/// <summary>
|
||
/// Busca objetos por sus IDs
|
||
/// </summary>
|
||
private List<osBase> FindObjectsByIds(string[] objectIds)
|
||
{
|
||
var allObjects = _mainViewModel.ObjetosSimulables.ToList();
|
||
var result = new List<osBase>();
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Calcula el bounding box de un conjunto de objetos usando coordenadas directas
|
||
/// </summary>
|
||
private ScreenshotArea CalculateObjectsBoundingBox(List<osBase> 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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// SOLUCIÓN 2: Calcula el bounding box mejorado que maneja objetos con dimensiones 0.0
|
||
/// </summary>
|
||
private ScreenshotArea CalculateObjectsBoundingBoxImproved(List<osBase> 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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Captura un área específica del canvas
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Captura todo el canvas
|
||
/// </summary>
|
||
private RenderTargetBitmap CaptureEntireCanvas(bool includeBackground)
|
||
{
|
||
_canvas.UpdateLayout();
|
||
var canvasRect = new Rect(0, 0, _canvas.ActualWidth, _canvas.ActualHeight);
|
||
return CaptureCanvasRect(canvasRect, includeBackground);
|
||
}
|
||
|
||
/// <summary>
|
||
/// SOLUCIÓN 3: Captura un rectángulo específico del canvas usando DrawingVisual (sin VisualBrush)
|
||
/// </summary>
|
||
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();
|
||
|
||
// CORRECCIÓN: Calcular posición relativa dentro del área de captura
|
||
var relativeLeft = (elementRect.X - captureRect.X) * scaleFactor;
|
||
var relativeTop = (elementRect.Y - captureRect.Y) * scaleFactor;
|
||
|
||
// Verificar que la posición relativa esté dentro del canvas temporal
|
||
var tempCanvasWidth = captureRect.Width * scaleFactor;
|
||
var tempCanvasHeight = captureRect.Height * scaleFactor;
|
||
|
||
var elementWidth = child.RenderSize.Width * scaleFactor;
|
||
var elementHeight = child.RenderSize.Height * scaleFactor;
|
||
|
||
// Verificar si el elemento está realmente dentro del área visible
|
||
if (relativeLeft < tempCanvasWidth && relativeTop < tempCanvasHeight &&
|
||
relativeLeft + elementWidth > 0 && relativeTop + elementHeight > 0)
|
||
{
|
||
// 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 = elementWidth,
|
||
Height = elementHeight,
|
||
Fill = visualBrush
|
||
};
|
||
|
||
Canvas.SetLeft(rect, relativeLeft);
|
||
Canvas.SetTop(rect, relativeTop);
|
||
|
||
tempCanvas.Children.Add(rect);
|
||
elementCount++;
|
||
|
||
Trace.WriteLine($"[ScreenshotManager] ✅ Renderizado {child.GetType().Name} - Absoluta:({elementRect.X:F1},{elementRect.Y:F1}) → Relativa:({relativeLeft:F1},{relativeTop:F1})");
|
||
}
|
||
catch (Exception brushEx)
|
||
{
|
||
Trace.WriteLine($"[ScreenshotManager] Error con VisualBrush para {child.GetType().Name}: {brushEx.Message}");
|
||
|
||
// Fallback: Rectángulo de placeholder visible
|
||
var placeholder = new Rectangle()
|
||
{
|
||
Width = elementWidth,
|
||
Height = elementHeight,
|
||
Fill = Brushes.LightBlue,
|
||
Stroke = Brushes.DarkBlue,
|
||
StrokeThickness = 2,
|
||
Opacity = 0.7
|
||
};
|
||
|
||
Canvas.SetLeft(placeholder, relativeLeft);
|
||
Canvas.SetTop(placeholder, relativeTop);
|
||
tempCanvas.Children.Add(placeholder);
|
||
elementCount++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine($"[ScreenshotManager] ⚠️ UserControl {child.GetType().Name} fuera del área visible - Rel:({relativeLeft:F1},{relativeTop:F1}) Canvas:({tempCanvasWidth:F1}×{tempCanvasHeight:F1})");
|
||
}
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// SOLUCIÓN 3: Valida si un elemento debe ser incluido en la captura
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Procesa la salida del screenshot (guardar archivo y/o generar base64)
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Determina si se debe generar base64 basado en las restricciones de tamaño y cantidad de objetos
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Obtiene la razón por la cual no se generó el base64
|
||
/// </summary>
|
||
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
|
||
|
||
/// <summary>
|
||
/// Resultado de una operación de screenshot
|
||
/// </summary>
|
||
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<CapturedObjectInfo> CapturedObjects { get; set; } = new List<CapturedObjectInfo>();
|
||
public AreaInfo BoundingBox { get; set; }
|
||
public AreaInfo CaptureArea { get; set; }
|
||
public float PaddingMeters { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Información de un objeto capturado
|
||
/// </summary>
|
||
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; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Información de un área
|
||
/// </summary>
|
||
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; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Área de screenshot en coordenadas de metros
|
||
/// </summary>
|
||
internal class ScreenshotArea
|
||
{
|
||
public float Left { get; set; }
|
||
public float Top { get; set; }
|
||
public float Width { get; set; }
|
||
public float Height { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Tipos de captura de screenshot
|
||
/// </summary>
|
||
public enum ScreenshotType
|
||
{
|
||
Objects,
|
||
Area,
|
||
FullCanvas
|
||
}
|
||
|
||
#endregion
|
||
}
|