diff --git a/Documentation/WorkspaceConfigurationSystem.md b/Documentation/WorkspaceConfigurationSystem.md new file mode 100644 index 0000000..379a407 --- /dev/null +++ b/Documentation/WorkspaceConfigurationSystem.md @@ -0,0 +1,90 @@ +# Sistema de Persistencia de Configuración del Entorno de Trabajo + +## Resumen +Se ha implementado un sistema completo para guardar y restaurar la configuración del entorno de trabajo para cada imagen, incluyendo la posición del GridSplitter, el estado del canvas (zoom, posición, offset) y el estado de `Is3DUpdateEnabled`. + +## Archivos Creados/Modificados + +### 1. Nuevo Archivo: `Models/WorkspaceConfiguration.cs` +- **Propósito**: Contiene la configuración del entorno de trabajo +- **Propiedades principales**: + - `GridSplitterPosition`: Altura del GridSplitter que separa vista 2D y 3D + - `CanvasCenterX/Y`: Posición central del canvas en píxeles + - `ZoomLevel`: Nivel de zoom actual del canvas + - `CanvasOffsetX/Y`: Offset de transformación del canvas + - `Is3DUpdateEnabled`: Estado de las actualizaciones 3D + - `HorizontalScrollOffset/VerticalScrollOffset`: Posición del scroll + +### 2. Modificado: `MainViewModel.cs` +- **Agregado**: Propiedad `WorkspaceConfig` para manejar la configuración +- **Nuevos métodos**: + - `CaptureWorkspaceState()`: Captura el estado actual desde la UI + - `RestoreWorkspaceState()`: Restaura el estado a la UI + - `InitializeDefaultWorkspaceConfig()`: Inicializa configuración por defecto + +### 3. Modificado: `SimulationData` (en MainViewModel.cs) +- **Agregado**: Propiedad `WorkspaceConfig` para persistir la configuración junto con cada imagen + +### 4. Modificado: `Serialization/StateSerializer.cs` +- **Modificado `SaveState()`**: Ahora captura automáticamente el estado del workspace antes de guardar +- **Modificado `LoadCurrentPageState()`**: Carga y restaura la configuración del workspace + +### 5. Modificado: `MainWindow.xaml` +- **Agregado**: Nombres a los elementos Grid para poder accederlos desde código: + - `x:Name="MainGrid"` al Grid principal + - `x:Name="MiddleColumnGrid"` al Grid de la segunda columna + +## Funcionamiento + +### Guardado Automático +1. Cuando se guarda el estado de una imagen (`SaveState()`), el sistema automáticamente: + - Captura la posición actual del GridSplitter + - Captura el estado actual del canvas (zoom, posición, scroll) + - Captura el estado de `Is3DUpdateEnabled` + - Guarda toda esta información junto con los datos de la imagen + +### Carga Automática +1. Cuando se carga una imagen (`LoadState()`), el sistema: + - Carga la configuración del workspace guardada para esa imagen + - Restaura automáticamente todos los valores a la UI + - Si no hay configuración guardada, usa valores por defecto + +### Persistencia por Imagen +- Cada imagen tiene su propia configuración independiente +- La configuración se guarda en el mismo archivo JSON que los objetos simulables +- Compatible con versiones anteriores (si no hay configuración, usa valores por defecto) + +## Características Técnicas + +### Manejo de Errores +- Todos los métodos de captura y restauración tienen manejo de excepciones +- Los errores se registran en Debug pero no interrumpen el proceso de guardado/carga + +### Compatibilidad +- Compatible con archivos existentes (retro-compatibilidad) +- Si un archivo no tiene configuración del workspace, se crea una por defecto + +### Uso del Dispatcher +- La restauración del estado usa `Dispatcher.BeginInvoke()` para asegurar que la UI esté lista +- Prioridad `Background` para no bloquear la interfaz + +## Beneficios + +1. **Consistencia**: Cada imagen mantiene su configuración visual específica +2. **Productividad**: Los usuarios no necesitan reconfigurar la vista cada vez que cambian de imagen +3. **Flexibilidad**: Diferentes imágenes pueden tener diferentes configuraciones de trabajo +4. **Transparencia**: El sistema funciona automáticamente sin intervención del usuario +5. **Robustez**: Manejo de errores y compatibilidad con versiones anteriores + +## Uso +El sistema es completamente automático. Los usuarios simplemente trabajan normalmente y: +- Al cambiar de imagen, la configuración se guarda automáticamente +- Al seleccionar una imagen, su configuración se restaura automáticamente +- El estado de `Is3DUpdateEnabled` se mantiene independiente para cada imagen +- La posición del GridSplitter se recuerda para cada imagen + +## Notas de Implementación +- Se usa `TransformGroup`, `ScaleTransform` y `TranslateTransform` para manejar el canvas +- El GridSplitter se maneja a través de las `RowDefinitions` del Grid de la segunda columna +- La configuración se serializa junto con los datos de simulación en formato JSON +- Se mantiene la estructura existente del sistema de serialización diff --git a/MainViewModel.cs b/MainViewModel.cs index 939e37c..6f6ba03 100644 --- a/MainViewModel.cs +++ b/MainViewModel.cs @@ -24,7 +24,8 @@ using System.Text.RegularExpressions; using System.Collections.Specialized; using CtrEditor.Serialization; // Add this line using CtrEditor.Controls; // Add this using directive -using CtrEditor.PopUps; // Add this using directive +using System.Linq; +using System.Windows.Media; namespace CtrEditor @@ -273,6 +274,9 @@ namespace CtrEditor [ObservableProperty] private PLCViewModel pLCViewModel; + [ObservableProperty] + private Models.WorkspaceConfiguration workspaceConfig; + [ObservableProperty] private string selectedImage; @@ -442,8 +446,138 @@ namespace CtrEditor // NOTA: La conexión del manager 3D se hace en MainWindow_Loaded // después de que se cree la instancia de BEPUVisualization3DManager + + // Inicializar configuración del workspace + WorkspaceConfig = new Models.WorkspaceConfiguration(); } + #region Workspace Configuration Management + + /// + /// Captura el estado actual del workspace desde la UI + /// + public void CaptureWorkspaceState() + { + if (MainWindow == null) return; + + try + { + // Capturar posición del GridSplitter (altura de la fila 3 que contiene la vista 3D) + var middleGrid = MainWindow.FindName("MiddleColumnGrid") as Grid; + if (middleGrid != null && middleGrid.RowDefinitions.Count > 3) + { + // Capturar la altura actual de la fila 3 (vista 3D) en píxeles + var row3ActualHeight = middleGrid.RowDefinitions[3].ActualHeight; + WorkspaceConfig.GridSplitterPosition = row3ActualHeight; + } + + // Capturar estado del canvas y scroll + var scrollViewer = MainWindow.FindName("ImagenEnTrabajoScrollViewer") as ScrollViewer; + if (scrollViewer != null) + { + WorkspaceConfig.HorizontalScrollOffset = scrollViewer.HorizontalOffset; + WorkspaceConfig.VerticalScrollOffset = scrollViewer.VerticalOffset; + } + + // Capturar transformaciones del canvas + var canvas = MainWindow.FindName("ImagenEnTrabajoCanvas") as Canvas; + if (canvas?.RenderTransform is TransformGroup transformGroup) + { + var scaleTransform = transformGroup.Children.OfType().FirstOrDefault(); + var translateTransform = transformGroup.Children.OfType().FirstOrDefault(); + + if (scaleTransform != null) + { + WorkspaceConfig.ZoomLevel = scaleTransform.ScaleX; + } + + if (translateTransform != null) + { + WorkspaceConfig.CanvasOffsetX = translateTransform.X; + WorkspaceConfig.CanvasOffsetY = translateTransform.Y; + } + } + + // Capturar centro del canvas + var centerPixels = MainWindow.ObtenerCentroCanvasPixels(); + WorkspaceConfig.CanvasCenterX = centerPixels.X; + WorkspaceConfig.CanvasCenterY = centerPixels.Y; + + // Capturar estado de Is3DUpdateEnabled + WorkspaceConfig.Is3DUpdateEnabled = Is3DUpdateEnabled; + } + catch (Exception ex) + { + // Log error but don't throw to avoid interrupting save process + System.Diagnostics.Debug.WriteLine($"Error capturing workspace state: {ex.Message}"); + } + } + + /// + /// Restaura el estado del workspace a la UI + /// + public void RestoreWorkspaceState() + { + if (MainWindow == null || WorkspaceConfig == null) return; + + try + { + // Restaurar posición del GridSplitter (altura de la fila 3 que contiene la vista 3D) + var middleGrid = MainWindow.FindName("MiddleColumnGrid") as Grid; + if (middleGrid != null && middleGrid.RowDefinitions.Count > 3) + { + // Establecer la altura de la vista 3D en píxeles absolutos + middleGrid.RowDefinitions[3].Height = new GridLength(WorkspaceConfig.GridSplitterPosition, GridUnitType.Pixel); + } + + // Restaurar estado del canvas y scroll + var scrollViewer = MainWindow.FindName("ImagenEnTrabajoScrollViewer") as ScrollViewer; + if (scrollViewer != null) + { + scrollViewer.ScrollToHorizontalOffset(WorkspaceConfig.HorizontalScrollOffset); + scrollViewer.ScrollToVerticalOffset(WorkspaceConfig.VerticalScrollOffset); + } + + // Restaurar transformaciones del canvas + var canvas = MainWindow.FindName("ImagenEnTrabajoCanvas") as Canvas; + if (canvas?.RenderTransform is TransformGroup transformGroup) + { + var scaleTransform = transformGroup.Children.OfType().FirstOrDefault(); + var translateTransform = transformGroup.Children.OfType().FirstOrDefault(); + + if (scaleTransform != null) + { + scaleTransform.ScaleX = WorkspaceConfig.ZoomLevel; + scaleTransform.ScaleY = WorkspaceConfig.ZoomLevel; + } + + if (translateTransform != null) + { + translateTransform.X = WorkspaceConfig.CanvasOffsetX; + translateTransform.Y = WorkspaceConfig.CanvasOffsetY; + } + } + + // Restaurar estado de Is3DUpdateEnabled + Is3DUpdateEnabled = WorkspaceConfig.Is3DUpdateEnabled; + } + catch (Exception ex) + { + // Log error but don't throw to avoid interrupting load process + System.Diagnostics.Debug.WriteLine($"Error restoring workspace state: {ex.Message}"); + } + } + + /// + /// Inicializa la configuración del workspace con valores por defecto + /// + public void InitializeDefaultWorkspaceConfig() + { + WorkspaceConfig = new Models.WorkspaceConfiguration(); + } + + #endregion + // Métodos para manejo de datos de imágenes private void RenameImage(string imageName) { @@ -1385,6 +1519,9 @@ namespace CtrEditor // Diccionario para almacenar datos expandidos de imágenes public Dictionary? ImageDataDictionary { get; set; } + // Configuración del entorno de trabajo para cada imagen + public Models.WorkspaceConfiguration? WorkspaceConfig { get; set; } + // Compatibilidad con versiones anteriores - OBSOLETO [System.Obsolete("Use ImageDataDictionary instead")] public Dictionary? ImageCustomNames { get; set; } diff --git a/MainWindow.xaml b/MainWindow.xaml index 79cd52c..e6e8972 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -76,7 +76,7 @@ - + @@ -120,7 +120,7 @@ - + diff --git a/Models/WorkspaceConfiguration.cs b/Models/WorkspaceConfiguration.cs new file mode 100644 index 0000000..69c147c --- /dev/null +++ b/Models/WorkspaceConfiguration.cs @@ -0,0 +1,104 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.ComponentModel; + +namespace CtrEditor.Models +{ + /// + /// Configuración del entorno de trabajo que se guarda por imagen. + /// Incluye posiciones de splitters, canvas, zoom y otras configuraciones del workspace. + /// + public partial class WorkspaceConfiguration : ObservableObject + { + [ObservableProperty] + [property: Description("Height of the 3D view row in pixels")] + [property: Category("Splitters:")] + private double gridSplitterPosition = 100.0; // Valor por defecto en píxeles + + [ObservableProperty] + [property: Description("X position of the canvas center in pixels")] + [property: Category("Canvas Position:")] + private double canvasCenterX = 0.0; + + [ObservableProperty] + [property: Description("Y position of the canvas center in pixels")] + [property: Category("Canvas Position:")] + private double canvasCenterY = 0.0; + + [ObservableProperty] + [property: Description("Current zoom level of the canvas")] + [property: Category("Canvas View:")] + private double zoomLevel = 1.0; + + [ObservableProperty] + [property: Description("X offset of the canvas")] + [property: Category("Canvas View:")] + private double canvasOffsetX = 0.0; + + [ObservableProperty] + [property: Description("Y offset of the canvas")] + [property: Category("Canvas View:")] + private double canvasOffsetY = 0.0; + + [ObservableProperty] + [property: Description("Whether 3D updates are enabled")] + [property: Category("3D Settings:")] + private bool is3DUpdateEnabled = true; + + [ObservableProperty] + [property: Description("Horizontal scroll position of the canvas")] + [property: Category("Canvas View:")] + private double horizontalScrollOffset = 0.0; + + [ObservableProperty] + [property: Description("Vertical scroll position of the canvas")] + [property: Category("Canvas View:")] + private double verticalScrollOffset = 0.0; + + /// + /// Constructor por defecto con valores iniciales + /// + public WorkspaceConfiguration() + { + // Valores por defecto ya establecidos en las propiedades + } + + /// + /// Crea una copia de la configuración actual + /// + /// Nueva instancia con los mismos valores + public WorkspaceConfiguration Clone() + { + return new WorkspaceConfiguration + { + GridSplitterPosition = this.GridSplitterPosition, + CanvasCenterX = this.CanvasCenterX, + CanvasCenterY = this.CanvasCenterY, + ZoomLevel = this.ZoomLevel, + CanvasOffsetX = this.CanvasOffsetX, + CanvasOffsetY = this.CanvasOffsetY, + Is3DUpdateEnabled = this.Is3DUpdateEnabled, + HorizontalScrollOffset = this.HorizontalScrollOffset, + VerticalScrollOffset = this.VerticalScrollOffset + }; + } + + /// + /// Copia los valores de otra configuración + /// + /// Configuración fuente + public void CopyFrom(WorkspaceConfiguration source) + { + if (source == null) return; + + GridSplitterPosition = source.GridSplitterPosition; + CanvasCenterX = source.CanvasCenterX; + CanvasCenterY = source.CanvasCenterY; + ZoomLevel = source.ZoomLevel; + CanvasOffsetX = source.CanvasOffsetX; + CanvasOffsetY = source.CanvasOffsetY; + Is3DUpdateEnabled = source.Is3DUpdateEnabled; + HorizontalScrollOffset = source.HorizontalScrollOffset; + VerticalScrollOffset = source.VerticalScrollOffset; + } + } +} diff --git a/ObjetosSim/Emuladores/ucFiller.xaml.cs b/ObjetosSim/Emuladores/ucFiller.xaml.cs index 920cff9..0753f44 100644 --- a/ObjetosSim/Emuladores/ucFiller.xaml.cs +++ b/ObjetosSim/Emuladores/ucFiller.xaml.cs @@ -53,6 +53,7 @@ namespace CtrEditor.ObjetosSim [property: Name("Tag Consenso")] private string tag_consenso; + [ObservableProperty] [property: Category("Simulación")] [property: Description("Estado del consenso")] diff --git a/Serialization/StateSerializer.cs b/Serialization/StateSerializer.cs index b6b0462..80438bb 100644 --- a/Serialization/StateSerializer.cs +++ b/Serialization/StateSerializer.cs @@ -65,12 +65,16 @@ namespace CtrEditor.Serialization } } - // Paso 2: Guardar datos de la p�gina actual + // Paso 2: Capturar estado del workspace antes de guardar + _mainViewModel.CaptureWorkspaceState(); + + // Paso 3: Guardar datos de la página actual var dataToSerialize = new SimulationData { ObjetosSimulables = objetosSimulables.Count > 0 ? objetosSimulables : null, UnitConverter = PixelToMeter.Instance.calc, PLC_ConnectionData = _mainViewModel.PLCViewModel, + WorkspaceConfig = _mainViewModel.WorkspaceConfig, DatosLocalesObjetos = datosLocalesObjetos.Count > 0 ? datosLocalesObjetos : null, ImageDataDictionary = _mainViewModel._imageDataDictionary.Count > 0 ? _mainViewModel._imageDataDictionary : null }; @@ -114,9 +118,16 @@ namespace CtrEditor.Serialization // Paso 1: Cargar objetos globales LoadAllPagesState(settings); - // Paso 2: Cargar datos de la p�gina actual + // Paso 2: Cargar datos de la página actual var simulationData = LoadCurrentPageState(selectedImage, settings); + // Paso 2.1: Restaurar configuración del workspace + // Usar un dispatcher para asegurar que la UI esté lista + System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + _mainViewModel.RestoreWorkspaceState(); + }), System.Windows.Threading.DispatcherPriority.Background); + // Paso 3: Crear snapshots de los objetos globales // Nota: No filtramos por Enable_Local_Data aqu� ya que se establecer� despu�s foreach (var obj in _mainViewModel.ObjetosSimulables) @@ -188,6 +199,12 @@ namespace CtrEditor.Serialization else _mainViewModel.PLCViewModel = new PLCViewModel(); + // Cargar configuración del workspace + if (simulationData.WorkspaceConfig is not null) + _mainViewModel.WorkspaceConfig = simulationData.WorkspaceConfig; + else + _mainViewModel.InitializeDefaultWorkspaceConfig(); + // Solo sobrescribir el UnitConverter si existe uno guardado if (simulationData.UnitConverter != null) PixelToMeter.Instance.calc = simulationData.UnitConverter;