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;