using CtrEditor; using System.ComponentModel; using System.Windows.Controls; using System.Windows.Input; using Ookii.Dialogs.Wpf; using System.Collections.ObjectModel; using System.Windows.Threading; using CtrEditor.ObjetosSim; using LibS7Adv; // Using stub implementation using System.IO; using Newtonsoft.Json; using System.Windows; using System.Text; // Para StringBuilder using CtrEditor.Simulacion; using CtrEditor.HydraulicSimulator; // Nuevo using para el simulador hidráulico using CtrEditor.HydraulicSimulator.TSNet; // Integración TSNet using System.Diagnostics; using System.Reflection; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using Xceed.Wpf.Toolkit.PropertyGrid; using CtrEditor.ObjetosSim.Extraccion_Datos; using ClosedXML.Excel; using System.Timers; // Para el nuevo timer de simulación más preciso using CtrEditor.PopUps; using System.Windows.Data; using CommunityToolkit.Mvvm.Input; using CtrEditor.Services; // Para MCPServer y DebugConsoleServer using System.Text.RegularExpressions; using System.Collections.Specialized; using CtrEditor.Serialization; // Add this line using CtrEditor.Controls; // Add this using directive using System.Linq; using System.Windows.Media; using CtrEditor.Services; // Add this for MCP Server namespace CtrEditor { public partial class MainViewModel : ObservableObject { private readonly StateSerializer _stateSerializer; public Stopwatch stopwatch_Sim; private double stopwatch_SimPLC_last; private double stopwatch_SimModel_last; private double accumulatedSimTime; private double accumulatedPlcTime; private int simSampleCount; private int plcSampleCount; // Sistema adaptativo para timing de PLC private double lastPlcExecutionTime = 0; private double maxPlcExecutionTime = 0; private int plcTimingAdaptationCounter = 0; private const int PLC_ADAPTATION_SAMPLES = 10; // Evaluar cada 10 muestras private const double MIN_PLC_INTERVAL = 10; // Mínimo intervalo PLC (ms) private const double MAX_PLC_INTERVAL = 100; // Máximo intervalo PLC (ms) private const double SIM_PRIORITY_TIME = 10; // Tiempo reservado para simulación (ms) // Sistema adaptativo para timing de simulación private double lastSimExecutionTime = 0; private double maxSimExecutionTime = 0; private int simTimingAdaptationCounter = 0; private const int SIM_ADAPTATION_SAMPLES = 5; // Evaluar cada 5 muestras para más responsividad private const double MIN_SIM_INTERVAL = 10; // Mínimo intervalo simulación (ms) private const double MAX_SIM_INTERVAL = 100; // Máximo intervalo simulación (ms) private const double SIM_BUFFER_TIME = 2; // Buffer de 2ms extra respecto al tiempo real // Variables para promedio móvil de SimulationSpeed private Queue simSpeedSamples = new Queue(); private double totalSimSpeedTime = 0; private const double SIM_SPEED_WINDOW_MS = 500; // Ventana de 500ms para promedio private float TiempoDesdeStartSimulacion; private bool Debug_SimulacionCreado = false; public SimulationManagerBEPU simulationManager = new SimulationManagerBEPU(); public HydraulicSimulationManager hydraulicSimulationManager = new HydraulicSimulationManager(); public TSNetSimulationManager tsnetSimulationManager = new TSNetSimulationManager(); private readonly System.Timers.Timer _timerSimulacion; // Cambiado a System.Timers.Timer para mejor precisión private readonly System.Timers.Timer _timerPLCUpdate; // Cambiado a System.Timers.Timer para mejor precisión private readonly DispatcherTimer _timerDisplayUpdate; private readonly DispatcherTimer _timer3DUpdate; // Nuevo timer para actualización 3D cuando simulación está detenida private readonly System.Timers.Timer _timerDebugFlush; // Timer para flush automático del buffer de debug private readonly System.Timers.Timer _timerTSNet; // Timer para TSNet automático cada 100ms public Canvas MainCanvas; // Manager para la visualización 3D public BEPUVisualization3DManager Visualization3DManager { get; set; } // Servidor MCP para control remoto private MCPServer _mcpServer; // Servidor de debug console para logging private DebugConsoleServer _debugConsoleServer; [ObservableProperty] private int mcpServerPort = 5006; [ObservableProperty] private bool isConnected; partial void OnPLCViewModelChanged(PLCViewModel value) { if (value != null) { value.PropertyChanged += PLCViewModel_PropertyChanged; IsConnected = value.IsConnected; } } private void PLCViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PLCViewModel.IsConnected)) { IsConnected = PLCViewModel.IsConnected; } } [ObservableProperty] private DatosDeTrabajo datosDeTrabajo; [ObservableProperty] private ObservableCollection listaImagenes; // Publicación de las claves del diccionario [ObservableProperty] public ObservableCollection listaOsBase; [ObservableProperty] public ObservableCollection categoriasOsBase; // Diccionario para almacenar datos expandidos de imágenes public Dictionary _imageDataDictionary = new Dictionary(); // Comando para renombrar imagen public ICommand RenameImageCommand { get; private set; } public ICommand StartSimulationCommand { get; } public ICommand StopSimulationCommand { get; } public ICommand ItemDoubleClickCommand { get; private set; } public ICommand TBStartSimulationCommand { get; } public ICommand TBStopSimulationCommand { get; } public ICommand TBStartFluidSimulationCommand { get; } public ICommand TBStopFluidSimulationCommand { get; } public ICommand TBSaveCommand { get; } public ICommand TBExtractTagsCommand { get; } public ICommand TBEliminarUserControlCommand { get; } public ICommand TBDuplicarUserControlCommand { get; } public ICommand OpenWorkDirectoryCommand { get; } public ICommand TBEliminarTodosCommand { get; } public ICommand TBEliminarAutoCreatedCommand { get; } public ICommand TBEliminarClonedCommand { get; } public ICommand TBAssingPagesCommand { get; } public ICommand TBMultiPageExtractTagsCommand { get; } public ICommand TBMultiPageAnalizeCommand { get; } public ICommand TBMultiPageMatrixCommand { get; } public ICommand TBLibraryManagerCommand { get; } // Comandos para vista 3D public ICommand TB3DViewTopCommand { get; } public ICommand TB3DViewSideCommand { get; } public ICommand TB3DViewFrontCommand { get; } public ICommand TB3DViewIsometricCommand { get; } // Comandos para TSNet - Temporalmente comentados //public ICommand TBTestTSNetCommand { get; } //public ICommand TBRunTSNetSimulationCommand { get; } //public ICommand TBGenerateINPCommand { get; } public ICommand TBTogglePLCConnectionCommand => new RelayCommand(() => { if (IsConnected) DisconnectPLC(); else ConnectPLC(); }); // Evento que se dispara cuando se selecciona una nueva imagen public event EventHandler ImageSelected; public event EventHandler TickSimulacion; // Propiedades private bool habilitarEliminarUserControl; private MainWindow mainWindow; private ObjectManipulationManager _objectManager; // Add this line public MainWindow MainWindow { get => mainWindow; set { mainWindow = value; _objectManager = mainWindow._objectManager; // Initialize _objectManager // Add the filter changed event handler here instead of in constructor if (mainWindow.VisFilter != null) { mainWindow.VisFilter.FilterChanged += (s, e) => OnVisFilterChanged(e.FilterViewModel); } } } [ObservableProperty] private float canvasLeft; [ObservableProperty] private float canvasTop; [ObservableProperty] private bool isSimulationRunning; [ObservableProperty] private bool hasUnsavedChanges; [ObservableProperty] private ObservableCollection recentDirectories; private bool inhibitSaveChangesControl; public ICommand OpenRecentDirectoryCommand { get; private set; } partial void OnIsSimulationRunningChanged(bool value) { CommandManager.InvalidateRequerySuggested(); // Notificar que el estado de los comandos ha cambiado // Controlar el timer de actualización 3D if (value) { // Simulación iniciada - detener timer 3D (la sincronización se hace en Step()) _timer3DUpdate.Stop(); } else { // Simulación detenida - iniciar timer 3D para mantener sincronización _timer3DUpdate.Start(); } } partial void OnHasUnsavedChangesChanged(bool value) { // Notificar el cambio del título indirectamente a través de directorioTrabajo OnPropertyChanged(nameof(directorioTrabajo)); } public string directorioTrabajo { get => EstadoPersistente.Instance.directorio; set { if (value != null) { EstadoPersistente.Instance.directorio = value; // Actualizar el estado persistente EstadoPersistente.Instance.GuardarEstado(); // Guardar el estado actualizado DatosDeTrabajo.CargarImagenes(); ListaImagenes = new ObservableCollection(DatosDeTrabajo.Imagenes.Keys); // Actualizar claves // Si no hay imágenes en el directorio, copiar base.png desde los recursos if (!ListaImagenes.Any()) { CopiarImagenBase(value); // Recargar las imágenes después de copiar la imagen base DatosDeTrabajo.CargarImagenes(); ListaImagenes = new ObservableCollection(DatosDeTrabajo.Imagenes.Keys); // Actualizar claves nuevamente } SelectedImage = null; var x = ListaImagenes.FirstOrDefault(o => o == EstadoPersistente.Instance.imagen, null); if (EstadoPersistente.Instance.imagen != null && EstadoPersistente.Instance.imagen.Length > 0 && x != null) SelectedImage = EstadoPersistente.Instance.imagen; else if (ListaImagenes.FirstOrDefault() != null) SelectedImage = ListaImagenes.FirstOrDefault(); OnPropertyChanged(nameof(directorioTrabajo)); // Notificar el cambio de propiedad OnPropertyChanged(nameof(ListaImagenes)); // Notificar que la lista de imágenes ha cambiado AddToRecentDirectories(value); } } } // Función para copiar la imagen base desde los recursos private void CopiarImagenBase(string directorio) { try { // Obtener el path del archivo base.png desde los recursos (dentro del ensamblado) var assembly = Assembly.GetExecutingAssembly(); var resourcePath = "CtrEditor.Images.base.png"; // Ajusta el namespace y el path según tu proyecto using (Stream resourceStream = assembly.GetManifestResourceStream(resourcePath)) { if (resourceStream != null) { string destino = Path.Combine(directorio, "base.png"); using (FileStream fileStream = new FileStream(destino, FileMode.Create, FileAccess.Write)) { resourceStream.CopyTo(fileStream); } } } } catch (Exception ex) { MessageBox.Show($"Error al copiar la imagen base: {ex.Message}"); } } [RelayCommand] public void DebugWindow() { MainWindow.DebugWindow(); } [ObservableProperty] private PLCViewModel pLCViewModel; [ObservableProperty] private Models.WorkspaceConfiguration workspaceConfig; [ObservableProperty] private string selectedImage; partial void OnSelectedImageChanging(string? oldValue, string newValue) { if (HasUnsavedChanges && !inhibitSaveChangesControl) { var result = MessageBox.Show("There are unsaved changes. Do you want to save them?", "Save Changes", MessageBoxButton.YesNo); if (result == MessageBoxResult.Yes) { SaveStateObjetosSimulables(); } } } partial void OnSelectedImageChanged(string value) { if (value != null) { StopSimulation(); ImageSelected?.Invoke(this, datosDeTrabajo.Imagenes[value]); LoadStateObjetosSimulables(); EstadoPersistente.Instance.imagen = value; EstadoPersistente.Instance.GuardarEstado(); HasUnsavedChanges = false; } } [ObservableProperty] private osBase selectedItemOsList; partial void OnSelectedItemOsListChanged(osBase value) { // Enable delete and duplicate commands when either an individual item is selected // or when there are multiple objects selected habilitarEliminarUserControl = _objectManager.SelectedObjects.Count > 0; } [ObservableProperty] private TipoSimulable selectedItem; public ICollectionView ObjetosSimulablesAllPages { get; } [ObservableProperty] public ObservableCollection objetosSimulables; [ObservableProperty] private bool isMultiSelectionActive; partial void OnIsMultiSelectionActiveChanged(bool value) { _objectManager?.OnMultiSelectionModeChanged(value); } partial void OnObjetosSimulablesChanged(ObservableCollection value) { if (value != null) { value.CollectionChanged += ObjetosSimulables_CollectionChanged; UpdateVisFilterTypes(); } } private void ObjetosSimulables_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { UpdateVisFilterTypes(); } private void UpdateVisFilterTypes() { MainWindow?.VisFilter?.UpdateAvailableTypes(ObjetosSimulables.Select(o => o.GetType()).Distinct()); MainWindow?.VisFilter?.UpdateAvailableTags(ObjetosSimulables); } // // Constructor // private StateManager _stateManager; public MainViewModel() { OpenWorkDirectoryCommand = new RelayCommand(OpenWorkDirectory); datosDeTrabajo = new DatosDeTrabajo(); // Initialize ObjetosSimulables first ObjetosSimulables = new ObservableCollection(); ObjetosSimulables = new ObservableCollection(); ListaOsBase = new ObservableCollection(); CategoriasOsBase = new ObservableCollection(); // Inicializa el PLCViewModel PLCViewModel = new PLCViewModel(); _timerPLCUpdate = new System.Timers.Timer(); _timerPLCUpdate.Interval = 10; // 10ms - más preciso que DispatcherTimer _timerPLCUpdate.Elapsed += OnRefreshEvent; _timerPLCUpdate.AutoReset = true; // Reinicio automático _timerPLCUpdate.SynchronizingObject = null; // No sincronizar automáticamente con UI InitializeTipoSimulableList(); ItemDoubleClickCommand = new ParameterizedRelayCommand(ExecuteDoubleClick); _timerSimulacion = new System.Timers.Timer(); _timerSimulacion.Interval = 15; // 15ms inicial - más conservador para permitir adaptación _timerSimulacion.Elapsed += OnTickSimulacion; _timerSimulacion.AutoReset = true; // Reinicio automático _timerSimulacion.SynchronizingObject = null; // No sincronizar automáticamente con UI // Nuevo timer para actualización de display _timerDisplayUpdate = new DispatcherTimer(); _timerDisplayUpdate.Interval = TimeSpan.FromMilliseconds(250); _timerDisplayUpdate.Tick += OnDisplayUpdate; _timerDisplayUpdate.Start(); _timer3DUpdate = new DispatcherTimer(); _timer3DUpdate.Interval = TimeSpan.FromMilliseconds(20); _timer3DUpdate.Tick += OnTick3DUpdate; _timer3DUpdate.Start(); // Iniciar porque la simulación empieza detenida // Timer para flush automático del buffer de debug (cada 5 minutos) _timerDebugFlush = new System.Timers.Timer(); _timerDebugFlush.Interval = 300000; // 5 minutos = 300,000 ms _timerDebugFlush.Elapsed += OnDebugFlushTimer; _timerDebugFlush.AutoReset = true; _timerDebugFlush.Start(); // Timer para TSNet automático (cada 100ms) _timerTSNet = new System.Timers.Timer(); _timerTSNet.Interval = 100; // 100ms para TSNet _timerTSNet.Elapsed += OnTSNetTimerElapsed; _timerTSNet.AutoReset = true; _timerTSNet.SynchronizingObject = null; // No sincronizar automáticamente con UI StartSimulationCommand = new RelayCommand(StartSimulation); StopSimulationCommand = new RelayCommand(StopSimulation); TBStartSimulationCommand = new RelayCommand(StartSimulation, () => !IsSimulationRunning); TBStopSimulationCommand = new RelayCommand(StopSimulation, () => IsSimulationRunning); TBSaveCommand = new RelayCommand(Save); TBEliminarUserControlCommand = new RelayCommand(EliminarUserControl, () => habilitarEliminarUserControl); TBDuplicarUserControlCommand = new RelayCommand(DuplicarUserControl, () => habilitarEliminarUserControl); TBEliminarTodosCommand = new RelayCommand(EliminarTodosCommand); TBEliminarAutoCreatedCommand = new RelayCommand(EliminarAutoCreatedCommand); TBEliminarClonedCommand = new RelayCommand(EliminarClonedCommand); TBAssingPagesCommand = new RelayCommand(AssingPagesCommand); TBMultiPageAnalizeCommand = new RelayCommand(MultiPageAnalizeCommand); TBMultiPageMatrixCommand = new RelayCommand(MultiPageMatrixCommand); TBLibraryManagerCommand = new RelayCommand(ShowLibraryManager); // Comandos para vista 3D TB3DViewTopCommand = new RelayCommand(() => Visualization3DManager?.SetCameraView(CameraView.Top)); TB3DViewSideCommand = new RelayCommand(() => Visualization3DManager?.SetCameraView(CameraView.Side)); TB3DViewFrontCommand = new RelayCommand(() => Visualization3DManager?.SetCameraView(CameraView.Front)); TB3DViewIsometricCommand = new RelayCommand(() => Visualization3DManager?.SetCameraView(CameraView.Isometric)); TBToggle3DUpdateCommand = new RelayCommand(() => Is3DUpdateEnabled = !Is3DUpdateEnabled); RenameImageCommand = new RelayCommand(RenameImage); // Comandos para TSNet - Temporalmente comentados para compilación //TBTestTSNetCommand = new RelayCommand(() => TestTSNetIntegrationSync()); //TBRunTSNetSimulationCommand = new RelayCommand(() => RunTSNetSimulationSync()); //TBGenerateINPCommand = new RelayCommand(() => GenerateINPFileSync()); stopwatch_Sim = new Stopwatch(); stopwatch_Sim.Start(); ObjetosSimulables.CollectionChanged += (s, e) => { if (e.Action != NotifyCollectionChangedAction.Move) HasUnsavedChanges = true; }; recentDirectories = new ObservableCollection(EstadoPersistente.Instance.RecentDirectories); OpenRecentDirectoryCommand = new RelayCommand(OpenRecentDirectory); _stateManager = new StateManager(EstadoPersistente.Instance.directorio, this); _stateSerializer = new StateSerializer(this, datosDeTrabajo, simulationManager); // Conectar DatosDeTrabajo con este ViewModel para el escaneo de imágenes datosDeTrabajo.SetMainViewModel(this); // 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(); // Iniciar servidor MCP automáticamente StartMcpServer(); } #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) { if (string.IsNullOrEmpty(imageName)) return; var imageData = GetOrCreateImageData(imageName); var dialog = new PopUps.RenameImageWindow(imageName, imageData); dialog.Owner = MainWindow; if (dialog.ShowDialog() == true) { // El dialog ya ha modificado directamente el imageData // Solo necesitamos verificar si debemos remover la entrada si está vacía if (string.IsNullOrEmpty(imageData.CustomName) && imageData.Tags.Count == 0 && string.IsNullOrEmpty(imageData.Etiquetas)) { _imageDataDictionary.Remove(imageName); } // Forzar actualización del UI usando CollectionViewSource var collectionView = System.Windows.Data.CollectionViewSource.GetDefaultView(ListaImagenes); collectionView?.Refresh(); HasUnsavedChanges = true; } } public string GetImageDisplayName(string imageName) { if (_imageDataDictionary.TryGetValue(imageName, out var imageData)) { return imageData.DisplayName; } return imageName; } public string GetImageDisplayNameWithTags(string imageName) { if (_imageDataDictionary.TryGetValue(imageName, out var imageData)) { var displayName = imageData.DisplayName; var tags = imageData.Etiquetas; if (!string.IsNullOrWhiteSpace(tags)) { return $"{displayName} {tags}"; } return displayName; } return imageName; } public Models.ImageData GetOrCreateImageData(string imageName) { if (!_imageDataDictionary.TryGetValue(imageName, out var imageData)) { imageData = new Models.ImageData(imageName); _imageDataDictionary[imageName] = imageData; } return imageData; } private void OnVisFilterChanged(osVisFilterViewModel filter) // Changed signature to accept viewmodel directly { foreach (var obj in ObjetosSimulables) { bool isVisible = true; // Apply Show All filter if (!filter.ShowAll) { // Check type filter var typeFilter = filter.TypeFilters.FirstOrDefault(tf => tf.Type == obj.GetType()); if (typeFilter == null || !typeFilter.IsSelected) { isVisible = false; } // Check tag filters if (filter.TagFilters.Any() && filter.TagFilters.Any(tf => tf.IsSelected)) { var selectedTags = filter.TagFilters.Where(tf => tf.IsSelected).Select(tf => tf.TagName).ToList(); bool hasMatchingTag = obj.ListaEtiquetas.Any(tag => selectedTags.Contains(tag)); if (!hasMatchingTag) { isVisible = false; } } // Check search tags if (!string.IsNullOrWhiteSpace(filter.SearchTags)) { var searchTags = filter.SearchTags .Split(' ', StringSplitOptions.RemoveEmptyEntries) .Where(tag => tag.StartsWith("#") && tag.Length > 1) .Select(tag => tag.Substring(1).ToLowerInvariant()) .ToList(); if (searchTags.Any()) { bool hasMatchingSearchTag = searchTags.Any(searchTag => obj.ListaEtiquetas.Contains(searchTag)); if (!hasMatchingSearchTag) { isVisible = false; } } } // Check other filters if (filter.ShowCloned && !obj.Cloned) isVisible = false; if (filter.ShowAutoCreated && !obj.AutoCreated) isVisible = false; if (filter.ShowEnableOnAllPages && !obj.Enable_On_All_Pages) isVisible = false; if (filter.ShowOnThisPage && !obj.Show_On_This_Page) isVisible = false; } obj.IsVisFilter = isVisible; // Update Canvas object visibility if (obj.VisualRepresentation != null) { obj.VisualRepresentation.Visibility = isVisible ? Visibility.Visible : Visibility.Collapsed; } } } public void LoadStateObjetosSimulables() { if (SelectedImage != null) { _stateSerializer.LoadState(SelectedImage); // Registrar objetos hidráulicos cargados RegisterLoadedHydraulicObjects(); // Aplicar los filtros actuales a los objetos recién cargados if (MainWindow?.VisFilter?.FilterViewModel != null) { OnVisFilterChanged(MainWindow.VisFilter.FilterViewModel); } // Limpiar historial de undo al cargar un estado desde archivo MainWindow?.ClearUndoHistory(); } } public void LoadInitialData() { // Suponiendo que "SelectedImage" es una propiedad que al establecerse dispara "ImageSelected" directorioTrabajo = EstadoPersistente.Instance.directorio; // Limpiar historial de undo al cargar datos iniciales MainWindow?.ClearUndoHistory(); } // Crear un nuevo Objeto private void ExecuteDoubleClick(object parameter) { if (parameter is TipoSimulable tipoSimulable) { CrearObjetoSimulableEnCentroCanvas(tipoSimulable.Tipo); } } public void CrearObjetoSimulableEnCentroCanvas(Type tipoSimulable) { var CentroCanvas = MainWindow.ObtenerCentroCanvasMeters(); CrearObjetoSimulable(tipoSimulable, CentroCanvas.X, CentroCanvas.Y); } public osBase CrearObjetoSimulable(Type tipoSimulable, float Left, float Top) { // Crear una nueva instancia del osBase correspondiente osBase? NuevoOsBase = UserControlFactory.GetInstanceForType(tipoSimulable); NuevoOsBase.Left = Left; NuevoOsBase.Top = Top; if (NuevoOsBase != null) { if (CrearUserControlDesdeObjetoSimulable(NuevoOsBase)) { // Añadir el nuevo osBase a la colección de objetos simulables ObjetosSimulables.Add(NuevoOsBase); // Registrar en el simulador hidráulico si tiene componentes hidráulicos RegisterHydraulicObjectIfNeeded(NuevoOsBase); HasUnsavedChanges = true; } } return NuevoOsBase; } // Crear UserControl desde osBase : Nuevo o desde Deserealizacion public bool CrearUserControlDesdeObjetoSimulable(osBase osObjeto) { Type tipoObjeto = osObjeto.GetType(); // Obtén el UserControl correspondiente para el tipo de objeto UserControl? userControl = UserControlFactory.GetControlForType(tipoObjeto); if (userControl != null) { // Asignar los datos al UserControl UserControlFactory.AssignDatos(userControl, osObjeto, simulationManager, hydraulicSimulationManager); osObjeto._mainViewModel = this; if (osObjeto.Id == null) // Para los objetos salvados antes de usar UniqueID osObjeto.Id = new UniqueId().ObtenerNuevaID(); MainWindow.AgregarRegistrarUserControlCanvas(userControl); return true; } return false; } public void RemoverObjetoSimulable(osBase osObjeto) { if (osObjeto != null && ObjetosSimulables.Contains(osObjeto)) { // Desregistrar del simulador hidráulico si estaba registrado UnregisterHydraulicObjectIfNeeded(osObjeto); ObjetosSimulables.Remove(osObjeto); if (osObjeto.VisualRepresentation != null) MainWindow.EliminarUserControlDelCanvas(osObjeto.VisualRepresentation); HasUnsavedChanges = true; } } private void DuplicarUserControl() { if (_objectManager.SelectedObjects.Count > 0) { // Create a copy of the selected objects to avoid issues during iteration var objectsToDuplicate = _objectManager.SelectedObjects.ToList(); // Clear current selection before duplicating _objectManager.ClearSelection(); // Track all newly created objects var newObjects = new List(); // Duplicate each object with a small offset float offsetX = 0.5f; float offsetY = 0.5f; foreach (var objToDuplicate in objectsToDuplicate) { var newObj = DuplicarObjeto(objToDuplicate, offsetX, offsetY); if (newObj != null) { newObjects.Add(newObj); } } // Force a complete layout update to ensure all controls are positioned MainWindow.ImagenEnTrabajoCanvas.UpdateLayout(); // Use a dispatcher to delay the selection until the UI has had time to fully render Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => { // Select all newly created objects foreach (var newObj in newObjects) { if (newObj.VisualRepresentation != null) { double left = Canvas.GetLeft(newObj.VisualRepresentation); double top = Canvas.GetTop(newObj.VisualRepresentation); // Only add to selection if the object has valid coordinates if (!double.IsNaN(left) && !double.IsNaN(top) && !double.IsInfinity(left) && !double.IsInfinity(top)) { _objectManager.SelectObject(newObj); } } } // Force another layout update before updating selection visuals MainWindow.ImagenEnTrabajoCanvas.UpdateLayout(); // Update SelectedItemOsList if there are newly created objects if (newObjects.Count > 0) { // Set to the last duplicated object so it's visible in the property panel SelectedItemOsList = newObjects.LastOrDefault(); } // Now update selection visuals _objectManager.UpdateSelectionVisuals(); })); } } public osBase DuplicarObjeto(osBase objDuplicar, float OffsetX, float OffsetY) { StopSimulation(); DisconnectPLC(); osBase? NuevoObjetoDuplicado = null; objDuplicar.SalvarDatosNoSerializables(); var settings = new JsonSerializerSettings { Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Ignore, TypeNameHandling = TypeNameHandling.All }; try { // Serializar var serializedData = JsonConvert.SerializeObject(objDuplicar, settings); // Duplicar NuevoObjetoDuplicado = JsonConvert.DeserializeObject(serializedData, settings); if (NuevoObjetoDuplicado != null) { NuevoObjetoDuplicado.Id.ObtenerNuevaID(); string nombre = Regex.IsMatch(NuevoObjetoDuplicado.Nombre, @"_\d+$") ? Regex.Replace(NuevoObjetoDuplicado.Nombre, @"_\d+$", $"_{NuevoObjetoDuplicado.Id.Value}") : NuevoObjetoDuplicado.Nombre + "_" + NuevoObjetoDuplicado.Id.Value; NuevoObjetoDuplicado.Nombre = nombre; NuevoObjetoDuplicado.Left += OffsetX; NuevoObjetoDuplicado.Top += OffsetY; ObjetosSimulables.Add(NuevoObjetoDuplicado); CrearUserControlDesdeObjetoSimulable(NuevoObjetoDuplicado); // Registrar en el simulador hidráulico si tiene componentes hidráulicos RegisterHydraulicObjectIfNeeded(NuevoObjetoDuplicado); HasUnsavedChanges = true; } } catch { // Log error or handle it accordingly } finally { objDuplicar.RestaurarDatosNoSerializables(); } return NuevoObjetoDuplicado; } private void EliminarUserControl() { _objectManager.EliminarObjetosSeleccionados(); SelectedItemOsList = null; } private void EliminarTodosCommand() { var objetosSimulablesCopy = new List(ObjetosSimulables); foreach (var obj in objetosSimulablesCopy) RemoverObjetoSimulable(obj); } private void EliminarAutoCreatedCommand() { var osAutoCreated_List = ObjetosSimulables .Where(o => o.Show_On_This_Page && o.AutoCreated) .ToList(); foreach (var obj in osAutoCreated_List) RemoverObjetoSimulable(obj); } private void EliminarClonedCommand() { var osCloned_List = ObjetosSimulables .Where(o => o.Show_On_This_Page && o.Cloned) .ToList(); foreach (var obj in osCloned_List) RemoverObjetoSimulable(obj); } private void AssingPagesCommand() { var assignImagesWindow = new AssignImagesWindow(); var assignImagesViewModel = new AssignImagesViewModel(); assignImagesViewModel.Initialize(this, assignImagesWindow); assignImagesWindow.DataContext = assignImagesViewModel; assignImagesWindow.ShowDialog(); if (assignImagesWindow.DataContext is AssignImagesViewModel dialog && dialog.CloseOK) SaveStateObjetosSimulables(); } public async Task WaitForUIUpdateAsync() { await Task.Yield(); Application.Current.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle); } private async void MultiPageAnalizeCommand() { var ImagenesSeleccionadas = new ObservableCollection { SelectedImage }; StopSimulation(); var selectPagesWindow = new SelectPages(); var selectPagesViewModel = new SelectPagesViewModel(); selectPagesViewModel.Initialize(this, selectPagesWindow, ref ImagenesSeleccionadas); selectPagesWindow.DataContext = selectPagesViewModel; selectPagesWindow.ShowDialog(); bool originalHasUnsavedChanges = HasUnsavedChanges; HasUnsavedChanges = false; try { if (selectPagesWindow.DataContext is SelectPagesViewModel dialog && dialog.CloseOK) { SaveStateObjetosSimulables(); // Guarda el estado antes de cambiar la imagen foreach (var page in ImagenesSeleccionadas) { SelectedImage = page; await WaitForUIUpdateAsync(); // Espera a que la UI se actualice AnalizePageCommand(); await WaitForUIUpdateAsync(); // Espera a que la UI se actualice SaveStateObjetosSimulables(); // Guarda el estado antes de cambiar la imagen } } } finally { HasUnsavedChanges = originalHasUnsavedChanges; } } private void AnalizePageCommand() { foreach (var obj in ObjetosSimulables) if (obj is osBuscarCoincidencias objBC) if (objBC.Show_On_This_Page) objBC.BuscarCoincidencias(); } private void InitializeTipoSimulableList() { var baseType = typeof(osBase); var types = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(assembly => assembly.GetTypes()) .Where(type => type.IsSubclassOf(baseType) && !type.IsAbstract && typeof(IosBase).IsAssignableFrom(type)); var categorias = new Dictionary(); foreach (var type in types) { var methodInfoNombre = type.GetMethod("NombreClase", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); var methodInfoCategoria = type.GetMethod("NombreCategoria", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); string nombre = methodInfoNombre != null ? methodInfoNombre.Invoke(null, null)?.ToString() : "Nombre no encontrado"; string categoria = methodInfoCategoria != null ? methodInfoCategoria.Invoke(null, null)?.ToString() : "Sin categoría"; var tipoSimulable = new TipoSimulable { Nombre = nombre, Tipo = type, Categoria = categoria }; ListaOsBase.Add(tipoSimulable); // Crear o obtener el nodo de categoría if (!categorias.ContainsKey(categoria)) { categorias[categoria] = new CategoriaNode(categoria); } // Agregar el tipo simulable a la categoría correspondiente categorias[categoria].Elementos.Add(tipoSimulable); } // Ordenar categorías y sus elementos foreach (var categoria in categorias.Values.OrderBy(c => c.Nombre)) { // Ordenar elementos dentro de cada categoría por nombre var elementosOrdenados = categoria.Elementos.OrderBy(e => e.Nombre).ToList(); categoria.Elementos.Clear(); foreach (var elemento in elementosOrdenados) { categoria.Elementos.Add(elemento); } CategoriasOsBase.Add(categoria); } } private void StartSimulation() { IsSimulationRunning = true; // Resetear contadores adaptativos para timing limpio ResetAdaptiveTimingCounters(); // Ocultar rectángulos de selección antes de iniciar la simulación _objectManager.UpdateSelectionVisuals(); foreach (var objetoSimulable in ObjetosSimulables) objetoSimulable.UpdateGeometryStart(); TiempoDesdeStartSimulacion = 0; Debug_SimulacionCreado = true; _timerSimulacion.Start(); _timerTSNet.Start(); // Iniciar TSNet automático simulationManager.Start(); } public void StopSimulation() { IsSimulationRunning = false; foreach (var objetoSimulable in ObjetosSimulables) objetoSimulable.SimulationStop(); // Reiniciar simulador hidráulico al detener la simulación ResetHydraulicSimulation(); if (Debug_SimulacionCreado) { Debug_SimulacionCreado = false; } _timerSimulacion.Stop(); _timerTSNet.Stop(); // Detener TSNet automático _timerPLCUpdate.Stop(); // También detener el timer PLC // Resetear contadores adaptativos al detener ResetAdaptiveTimingCounters(); // Restaurar los rectángulos de selección si hay objetos seleccionados _objectManager.UpdateSelectionVisuals(); // Limpiar historial de undo al detener la simulación MainWindow?.ClearUndoHistory(); } private void OnTickSimulacion(object sender, System.Timers.ElapsedEventArgs e) { // Verificar si la aplicación todavía está disponible if (Application.Current == null) return; // Verificar si el dispatcher está disponible y no ha comenzado el shutdown if (Application.Current.Dispatcher == null || Application.Current.Dispatcher.HasShutdownStarted) return; // Usar BeginInvoke para evitar bloqueos durante el uso del MCP Application.Current.Dispatcher.BeginInvoke(new Action(() => { try { var executionStopwatch = Stopwatch.StartNew(); // ✅ NUEVO: Medir tiempo de ejecución del método // ✅ MANTENER: Tiempo entre llamadas del timer para lógica interna var timeBetweenCalls = stopwatch_Sim.Elapsed.TotalMilliseconds - stopwatch_SimModel_last; stopwatch_SimModel_last = stopwatch_Sim.Elapsed.TotalMilliseconds; // ✅ NUEVO: Actualizar velocidad de simulación con promedio móvil de 500ms UpdateSimulationSpeedAverage(timeBetweenCalls); // Acumular tiempo para el promedio (usando tiempo real del timer) accumulatedSimTime += timeBetweenCalls; simSampleCount++; // Contador de tiempo desde el inicio de la simulación if (TiempoDesdeStartSimulacion <= 1200) TiempoDesdeStartSimulacion += (float)timeBetweenCalls; // ✅ NUEVO: Optimización temprana para escenarios sin objetos if (ObjetosSimulables?.Count > 0) { foreach (var objetoSimulable in ObjetosSimulables) objetoSimulable.UpdateGeometryStep(); } // Ejecutar simulación física BEPU simulationManager.Step(); // Ejecutar simulación hidráulica hydraulicSimulationManager.Step((float)(timeBetweenCalls / 1000.0)); // Convertir ms a segundos // ✅ NUEVO: Solo crear la copia si hay objetos para procesar if (ObjetosSimulables?.Count > 0) { var objetosSimulablesCopy = new List(ObjetosSimulables); foreach (var objetoSimulable in objetosSimulablesCopy) { if (!objetoSimulable.RemoverDesdeSimulacion) objetoSimulable.UpdateControl((int)timeBetweenCalls); else RemoverObjetoSimulable(objetoSimulable); } } executionStopwatch.Stop(); // ✅ CORREGIDO: Detener cronómetro de ejecución // Sistema adaptativo de timing de simulación AdaptSimulationTiming(executionStopwatch.Elapsed.TotalMilliseconds); //Debug.WriteLine($"OnTickSimulacion execution time: {stopwatch.ElapsedMilliseconds} ms"); //Debug.WriteLine($"OnTickSimulacion execution time: {executionStopwatch.Elapsed.TotalMilliseconds:F2}ms | Timer interval: {timeBetweenCalls:F2}ms | Objects: {ObjetosSimulables?.Count ?? 0}"); } catch (Exception ex) { // Manejo de errores para evitar crashes en el timer Debug.WriteLine($"[MainViewModel] Error en OnTickSimulacion: {ex.Message}"); } }), DispatcherPriority.Background); // Usar prioridad Background para no interferir con MCP } /// /// Timer para ejecutar TSNet automáticamente cada 100ms durante la simulación /// private async void OnTSNetTimerElapsed(object sender, System.Timers.ElapsedEventArgs e) { // Solo ejecutar si la simulación está corriendo y hay objetos hidráulicos if (!IsSimulationRunning || tsnetSimulationManager.IsRunning) return; try { // Contar objetos hidráulicos var hydraulicObjects = ObjetosSimulables?.Where(obj => obj.GetType().Name.Contains("osHyd")).ToList(); if (hydraulicObjects == null || hydraulicObjects.Count == 0) return; // Resetear y registrar objetos hidráulicos tsnetSimulationManager.ResetAllCalculatedValues(); foreach (var obj in hydraulicObjects) { tsnetSimulationManager.RegisterHydraulicObject(obj); } // Validar configuración var configErrors = tsnetSimulationManager.ValidateAllConfigurations(); if (configErrors.Count > 0) { Debug.WriteLine($"TSNet Auto: Errores de configuración: {string.Join(", ", configErrors)}"); return; } // Reconstruir red y ejecutar simulación tsnetSimulationManager.RebuildNetwork(); // Ejecutar simulación TSNet de forma asíncrona sin bloquear var result = await tsnetSimulationManager.RunSimulationAsync(); if (result.Success) { Debug.WriteLine($"TSNet Auto: Simulación exitosa con {hydraulicObjects.Count} objetos hidráulicos"); } else { Debug.WriteLine($"TSNet Auto: Error en simulación: {result.Message}"); } } catch (Exception ex) { Debug.WriteLine($"TSNet Auto: Excepción: {ex.Message}"); } } private void ConnectPLC() { _timerPLCUpdate.Start(); PLCViewModel.Connect(); foreach (var objetoSimulable in ObjetosSimulables) objetoSimulable.SetPLC(PLCViewModel); } public void DisconnectPLC() { PLCViewModel.Disconnect(); _timerPLCUpdate.Stop(); foreach (var objetoSimulable in ObjetosSimulables) objetoSimulable.SetPLC(null); // Limpiar historial de undo al desconectar del PLC MainWindow?.ClearUndoHistory(); } /// /// Limpia todos los recursos y detiene todos los timers antes del cierre de la aplicación /// public void CleanupResources() { // Detener simulación si está corriendo if (IsSimulationRunning) { StopSimulation(); } // Detener todos los timers _timerSimulacion?.Stop(); _timerPLCUpdate?.Stop(); _timer3DUpdate?.Stop(); _timerDisplayUpdate?.Stop(); _timerDebugFlush?.Stop(); _timerTSNet?.Stop(); // Desconectar PLC si está conectado if (PLCViewModel?.IsConnected == true) { PLCViewModel.Disconnect(); } // Limpiar servidor MCP CleanupMcpServer(); } private List objetosSimulablesLlamados = new List(); private void OnRefreshEvent(object sender, System.Timers.ElapsedEventArgs e) { // Verificar si la aplicación todavía está disponible if (Application.Current == null) return; // Verificar si el dispatcher está disponible y no ha comenzado el shutdown if (Application.Current.Dispatcher == null || Application.Current.Dispatcher.HasShutdownStarted) return; // Usar BeginInvoke para evitar bloqueos durante el uso del MCP Application.Current.Dispatcher.BeginInvoke(new Action(() => { try { var stopwatch = Stopwatch.StartNew(); // Start measuring time if (PLCViewModel.IsConnected) { // Detener el cronómetro y obtener el tiempo transcurrido en milisegundos var elapsedMilliseconds = stopwatch_Sim.Elapsed.TotalMilliseconds - stopwatch_SimPLC_last; stopwatch_SimPLC_last = stopwatch_Sim.Elapsed.TotalMilliseconds; // Acumular tiempo para el promedio accumulatedPlcTime += elapsedMilliseconds; plcSampleCount++; // Reiniciar el cronómetro para la próxima medición var remainingObjetosSimulables = ObjetosSimulables.Except(objetosSimulablesLlamados).ToList(); foreach (var objetoSimulable in remainingObjetosSimulables) { var objStopwatch = Stopwatch.StartNew(); objetoSimulable.UpdatePLC(PLCViewModel, (int)elapsedMilliseconds); objStopwatch.Stop(); objetosSimulablesLlamados.Add(objetoSimulable); if (stopwatch.Elapsed.TotalMilliseconds >= 10) break; } if (remainingObjetosSimulables.Count == 0) { objetosSimulablesLlamados.Clear(); } } stopwatch.Stop(); // Stop measuring time // Sistema adaptativo de timing PLC AdaptPlcTiming(stopwatch.Elapsed.TotalMilliseconds); // Debug.WriteLine($"OnRefreshEvent: {stopwatch.Elapsed.TotalMilliseconds} ms, Interval: {_timerPLCUpdate.Interval}ms"); } catch (Exception ex) { // Log del error para el debugging Debug.WriteLine($"Error en OnRefreshEvent: {ex.Message}"); // No relanzar la excepción para evitar crashear el timer } }), DispatcherPriority.Background); } /// /// Evento del timer que hace flush automático del buffer de debug para evitar acumulación excesiva /// private void OnDebugFlushTimer(object sender, System.Timers.ElapsedEventArgs e) { try { if (_debugConsoleServer != null) { var stats = _debugConsoleServer.GetBufferStats(); // Solo hacer flush si el buffer tiene un número significativo de mensajes if (stats.messageCount > 500) { _debugConsoleServer.FlushMessageBuffer(); Debug.WriteLine($"[Debug Flush Timer] Buffer automático limpiado. Mensajes procesados: {stats.messageCount}"); } else { Debug.WriteLine($"[Debug Flush Timer] Buffer saludable. Mensajes actuales: {stats.messageCount}"); } } } catch (Exception ex) { Debug.WriteLine($"[Debug Flush Timer] Error durante flush automático: {ex.Message}"); } } /// /// Evento del timer para actualización de display (UI) /// private void OnDisplayUpdate(object sender, EventArgs e) { try { // Actualizar elementos de UI que no requieren alta frecuencia // Este timer corre a 250ms por lo que es adecuado para actualizaciones de UI menos críticas // Se puede agregar lógica de actualización de UI aquí si es necesario } catch (Exception ex) { Debug.WriteLine($"[Display Update Timer] Error durante actualización de display: {ex.Message}"); } } /// /// Evento del timer para actualización 3D cuando la simulación está detenida /// private void OnTick3DUpdate(object sender, EventArgs e) { try { // Mantener la visualización 3D actualizada cuando la simulación no está corriendo if (!IsSimulationRunning && Visualization3DManager != null) { Visualization3DManager.SynchronizeWorld(); } } catch (Exception ex) { Debug.WriteLine($"[3D Update Timer] Error durante actualización 3D: {ex.Message}"); } } private void AdaptPlcTiming(double executionTime) { lastPlcExecutionTime = executionTime; maxPlcExecutionTime = Math.Max(maxPlcExecutionTime, executionTime); plcTimingAdaptationCounter++; // Evaluar cada PLC_ADAPTATION_SAMPLES ejecuciones if (plcTimingAdaptationCounter >= PLC_ADAPTATION_SAMPLES) { double currentInterval = _timerPLCUpdate.Interval; double newInterval = currentInterval; // Si el tiempo máximo de ejecución + tiempo para simulación > intervalo actual if (maxPlcExecutionTime + SIM_PRIORITY_TIME > currentInterval) { // Incrementar intervalo para dar más tiempo newInterval = Math.Min(maxPlcExecutionTime + SIM_PRIORITY_TIME + 5, MAX_PLC_INTERVAL); // Debug.WriteLine($"PLC Timer: Aumentando intervalo a {newInterval:F1}ms (ejecución máxima: {maxPlcExecutionTime:F1}ms)"); } // Si hemos estado por debajo del umbral, podemos reducir el intervalo else if (maxPlcExecutionTime + SIM_PRIORITY_TIME < currentInterval * 0.7) { newInterval = Math.Max(maxPlcExecutionTime + SIM_PRIORITY_TIME + 2, MIN_PLC_INTERVAL); // Debug.WriteLine($"PLC Timer: Reduciendo intervalo a {newInterval:F1}ms (ejecución máxima: {maxPlcExecutionTime:F1}ms)"); } // Aplicar el nuevo intervalo si cambió if (Math.Abs(newInterval - currentInterval) > 1) { _timerPLCUpdate.Interval = newInterval; } // Reset para el próximo período de evaluación maxPlcExecutionTime = 0; plcTimingAdaptationCounter = 0; } } /// /// Sistema adaptativo para el timing de simulación que mantiene 2ms de buffer extra /// private void AdaptSimulationTiming(double executionTime) { lastSimExecutionTime = executionTime; maxSimExecutionTime = Math.Max(maxSimExecutionTime, executionTime); simTimingAdaptationCounter++; // Evaluar cada SIM_ADAPTATION_SAMPLES ejecuciones para responsividad rápida if (simTimingAdaptationCounter >= SIM_ADAPTATION_SAMPLES) { double currentInterval = _timerSimulacion.Interval; double newInterval = currentInterval; // Calcular intervalo ideal: tiempo de ejecución máximo + buffer de 2ms double idealInterval = maxSimExecutionTime + SIM_BUFFER_TIME; // Si el tiempo de ejecución + buffer > intervalo actual, necesitamos más tiempo if (idealInterval > currentInterval) { // Incrementar intervalo para evitar que el sistema se vuelva inusable newInterval = Math.Min(idealInterval + 1, MAX_SIM_INTERVAL); // +1ms adicional de seguridad //Debug.WriteLine($"Simulación Timer: Aumentando intervalo a {newInterval:F1}ms (ejecución máxima: {maxSimExecutionTime:F1}ms)"); } // Si hemos estado muy por debajo del umbral, podemos reducir el intervalo else if (idealInterval < currentInterval * 0.8) { newInterval = Math.Max(idealInterval, MIN_SIM_INTERVAL); //Debug.WriteLine($"Simulación Timer: Reduciendo intervalo a {newInterval:F1}ms (ejecución máxima: {maxSimExecutionTime:F1}ms)"); } // Aplicar el nuevo intervalo si cambió significativamente if (Math.Abs(newInterval - currentInterval) > 0.5) { _timerSimulacion.Interval = newInterval; //Debug.WriteLine($"Simulación Timer: Intervalo actualizado de {currentInterval:F1}ms a {newInterval:F1}ms"); } // Reset para el próximo período de evaluación maxSimExecutionTime = 0; simTimingAdaptationCounter = 0; } } /// /// Actualiza el promedio móvil de velocidad de simulación en una ventana de 500ms /// private void UpdateSimulationSpeedAverage(double timeBetweenCalls) { // Agregar la nueva muestra simSpeedSamples.Enqueue(timeBetweenCalls); totalSimSpeedTime += timeBetweenCalls; // Remover muestras antiguas que estén fuera de la ventana de 500ms while (simSpeedSamples.Count > 0 && totalSimSpeedTime > SIM_SPEED_WINDOW_MS) { var oldestSample = simSpeedSamples.Dequeue(); totalSimSpeedTime -= oldestSample; } // Calcular y actualizar el promedio if (simSpeedSamples.Count > 0) { SimulationSpeed = totalSimSpeedTime / simSpeedSamples.Count; } } /// /// Resetea todos los contadores adaptativos de timing para empezar con mediciones limpias /// private void ResetAdaptiveTimingCounters() { // Reset contadores de simulación lastSimExecutionTime = 0; maxSimExecutionTime = 0; simTimingAdaptationCounter = 0; SimulationSpeed = 0.0; // Reset del display de velocidad de simulación // Reset promedio móvil de SimulationSpeed simSpeedSamples.Clear(); totalSimSpeedTime = 0; // Reset contadores de PLC lastPlcExecutionTime = 0; maxPlcExecutionTime = 0; plcTimingAdaptationCounter = 0; // Restablecer intervalos iniciales conservadores _timerSimulacion.Interval = 15; // 15ms inicial _timerPLCUpdate.Interval = 10; // 10ms inicial Debug.WriteLine("Timing adaptativo: Contadores y intervalos reseteados"); } private void OpenWorkDirectory() { var dialog = new VistaFolderBrowserDialog(); if (dialog.ShowDialog() == true) // Mostrar el diálogo y comprobar si el resultado es positivo { directorioTrabajo = dialog.SelectedPath; // Actualizar la propiedad que también actualiza el estado persistente } } private void OpenRecentDirectory(string path) { if (Directory.Exists(path)) { directorioTrabajo = path; } else { MessageBox.Show($"Directory not found: {path}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); RecentDirectories.Remove(path); UpdateRecentDirectories(); } } private void UpdateRecentDirectories() { EstadoPersistente.Instance.RecentDirectories = RecentDirectories.ToList(); EstadoPersistente.Instance.GuardarEstado(); } private void AddToRecentDirectories(string path) { // Remove the path if it already exists RecentDirectories.Remove(path); // Add the new path at the beginning RecentDirectories.Insert(0, path); // Keep only the last 10 entries while (RecentDirectories.Count > 10) { RecentDirectories.RemoveAt(RecentDirectories.Count - 1); } UpdateRecentDirectories(); } // // Lista de osBase // public void Save() { SaveStateObjetosSimulables(); } public void SaveStateObjetosSimulables() { if (SelectedImage != null) { _stateSerializer.SaveState(SelectedImage); HasUnsavedChanges = false; } } // Se cargan los datos de cada UserControl en el StackPanel public void CargarPropiedadesosDatos(osBase selectedObject, Controls.PanelEdicionControl PanelEdicion, ResourceDictionary Resources) { if (selectedObject == null) { // Clear the property panel when no object is selected PanelEdicion.ClearProperties(); } else { PanelEdicion.CargarPropiedades(selectedObject); } } private RelayCommand saveCommand; public ICommand SaveCommand => saveCommand ??= new RelayCommand(Save); private void Save(object commandParameter) { } private RelayCommand exitCommand; public ICommand ExitCommand => exitCommand ??= new RelayCommand(Exit); private void Exit() { if (HasUnsavedChanges) { var result = MessageBox.Show("There are unsaved changes. Do you want to save them?", "Save Changes", MessageBoxButton.YesNoCancel); if (result == MessageBoxResult.Cancel) return; else if (result == MessageBoxResult.Yes) SaveStateObjetosSimulables(); } Application.Current.Shutdown(); } private async void MultiPageMatrixCommand() { if (HasUnsavedChanges) { var result = MessageBox.Show("There are unsaved changes. Do you want to save them?", "Save Changes", MessageBoxButton.YesNoCancel); if (result == MessageBoxResult.Cancel) return; else if (result == MessageBoxResult.Yes) SaveStateObjetosSimulables(); } var ImagenesSeleccionadas = new ObservableCollection { SelectedImage }; StopSimulation(); var selectPagesWindow = new SelectPages(); var selectPagesViewModel = new SelectPagesViewModel(); selectPagesViewModel.Initialize(this, selectPagesWindow, ref ImagenesSeleccionadas); selectPagesWindow.DataContext = selectPagesViewModel; selectPagesWindow.ShowDialog(); inhibitSaveChangesControl = true; try { if (selectPagesWindow.DataContext is SelectPagesViewModel dialog && dialog.CloseOK) { var matrixPreviewWindow = new MatrixPreviewWindow(); var matrixPreviewViewModel = new MatrixPreviewViewModel(); matrixPreviewViewModel.Initialize(this, matrixPreviewWindow, ImagenesSeleccionadas); matrixPreviewWindow.DataContext = matrixPreviewViewModel; matrixPreviewWindow.ShowDialog(); } } finally { inhibitSaveChangesControl = false; } } [ObservableProperty] private double simulationSpeed; [ObservableProperty] private double plcUpdateSpeed; [ObservableProperty] private bool is3DUpdateEnabled = true; public ICommand TBToggle3DUpdateCommand { get; } partial void OnIs3DUpdateEnabledChanged(bool value) { // Sincronizar la propiedad con el simulationManager if (simulationManager != null) { simulationManager.Is3DUpdateEnabled = value; } } // Reemplazar la propiedad _multiPropertyEditorWindow por un diccionario private Dictionary _propertyEditorWindows = new(); public void ShowMultiPropertyEditor(IEnumerable selectedObjects) { var objectsList = selectedObjects.ToList(); // Crear una clave única basada en los IDs de los objetos seleccionados string key = string.Join("_", objectsList.Select(o => o.Id.Value).OrderBy(id => id)); // Verificar si ya existe una ventana para esta selección if (_propertyEditorWindows.TryGetValue(key, out var existingWindow) && existingWindow.IsVisible) { existingWindow.Activate(); return; } // Crear nueva ventana var window = new Windows.MultiPropertyEditorWindow(objectsList, MainWindow); window.Closed += (s, e) => _propertyEditorWindows.Remove(key); _propertyEditorWindows[key] = window; window.Show(); HasUnsavedChanges = true; _objectManager.UpdateSelectionVisuals(); } // Add this method to MainViewModel class public void NotifySelectionChanged() { OnPropertyChanged(nameof(SelectedItemOsList)); } // Diccionario para manejar la ventana de configuración de escala private PopUps.ScaleConfigWindow? _scaleConfigWindow; // Método para configurar la escala desde el menú contextual public void ConfigureScale() { // Verificar si ya existe una ventana de configuración de escala if (_scaleConfigWindow != null && _scaleConfigWindow.IsVisible) { _scaleConfigWindow.Activate(); return; } var currentScale = PixelToMeter.Instance.calc.Scale; _scaleConfigWindow = new PopUps.ScaleConfigWindow(currentScale, this); _scaleConfigWindow.Owner = MainWindow; // Manejar el cierre de la ventana _scaleConfigWindow.Closed += (s, e) => _scaleConfigWindow = null; // Mostrar como modeless (no modal) _scaleConfigWindow.Show(); } // Método público para aplicar la escala desde la ventana modeless public void ApplyScale(float newScale) { // Detener simulaciones antes de cambiar la escala StopSimulation(); DisconnectPLC(); // Actualizar la escala en el UnitConverter PixelToMeter.Instance.calc.SetScale(newScale); // Forzar redibujo completo del canvas y todos los objetos ForceCanvasRedraw(); // Marcar como cambios no guardados ya que esto afecta la configuración de la imagen HasUnsavedChanges = true; // Limpiar historial de undo después de cambiar la escala MainWindow?.ClearUndoHistory(); } // Método para forzar el redibujo completo del canvas private void ForceCanvasRedraw() { // Forzar actualización de todos los objetos simulables mediante PropertyChanged foreach (var obj in ObjetosSimulables) { // Forzar la notificación de PropertyChanged para las propiedades de posición y tamaño // Esto hará que los bindings y converters se recalculen automáticamente obj.ForceUpdatePositionBindings(); } // Forzar actualización del layout del canvas principal MainWindow?.ImagenEnTrabajoCanvas?.InvalidateVisual(); MainWindow?.ImagenEnTrabajoCanvas?.UpdateLayout(); // Actualizar selecciones visuales si existen _objectManager?.UpdateSelectionVisuals(); // Forzar actualización global del layout con prioridad de renderizado Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => { MainWindow?.ImagenEnTrabajoCanvas?.InvalidateVisual(); MainWindow?.ImagenEnTrabajoCanvas?.UpdateLayout(); })); } // Diccionario para manejar ventanas de biblioteca múltiples private Dictionary _libraryWindows = new(); private void ShowLibraryManager() { const string libraryWindowKey = "LibraryManager"; // Verificar si ya existe una ventana de biblioteca if (_libraryWindows.TryGetValue(libraryWindowKey, out var existingWindow) && existingWindow.IsVisible) { existingWindow.Activate(); return; } // Crear nueva ventana modeless var libraryWindow = new PopUps.LibraryWindow(); var libraryViewModel = new PopUps.LibraryWindowViewModel(this); libraryWindow.DataContext = libraryViewModel; libraryWindow.Owner = MainWindow; // Manejar el cierre de la ventana libraryWindow.Closed += (s, e) => _libraryWindows.Remove(libraryWindowKey); // Registrar la ventana _libraryWindows[libraryWindowKey] = libraryWindow; // Mostrar como modeless (no modal) libraryWindow.Show(); } #region Hydraulic Simulation Management /// /// Registra un objeto en el simulador hidráulico si tiene componentes hidráulicos /// private void RegisterHydraulicObjectIfNeeded(osBase obj) { if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents) { // Evitar doble registro - solo registrar si no está ya registrado if (!hydraulicSimulationManager.HydraulicObjects.Contains(obj)) { hydraulicSimulationManager.RegisterHydraulicObject(obj); Debug.WriteLine($"Objeto hidráulico registrado: {obj.Nombre}"); } else { Debug.WriteLine($"Objeto hidráulico ya registrado (omitido): {obj.Nombre}"); } } } /// /// Desregistra un objeto del simulador hidráulico si estaba registrado /// private void UnregisterHydraulicObjectIfNeeded(osBase obj) { // Desregistrar independientemente de si implementa la interfaz actualmente // (podría haber cambiado desde que se registró) hydraulicSimulationManager.UnregisterHydraulicObject(obj); Debug.WriteLine($"Objeto desregistrado del simulador hidráulico: {obj.Nombre}"); } /// /// Reinicia el simulador hidráulico /// public void ResetHydraulicSimulation() { hydraulicSimulationManager.Reset(); Debug.WriteLine("Simulador hidráulico reiniciado"); } /// /// Obtiene estadísticas del simulador hidráulico /// public string GetHydraulicSimulationStats() { return hydraulicSimulationManager.GetSimulationStats(); } /// /// Habilita/deshabilita la simulación hidráulica /// public void SetHydraulicSimulationEnabled(bool enabled) { hydraulicSimulationManager.IsHydraulicSimulationEnabled = enabled; Debug.WriteLine($"Simulación hidráulica {(enabled ? "habilitada" : "deshabilitada")}"); } /// /// Registra todos los objetos hidráulicos existentes después de cargar un proyecto /// private void RegisterLoadedHydraulicObjects() { // Limpiar registros previos hydraulicSimulationManager.ClearHydraulicObjects(); // Crear una lista temporal para evitar duplicados durante la carga var objectsToRegister = new HashSet(); // Registrar todos los objetos cargados que tengan componentes hidráulicos foreach (var obj in ObjetosSimulables) { if (obj is IHydraulicComponent hydraulicComponent && hydraulicComponent.HasHydraulicComponents) { objectsToRegister.Add(obj); } } // Registrar los objetos únicos foreach (var obj in objectsToRegister) { hydraulicSimulationManager.RegisterHydraulicObject(obj); Debug.WriteLine($"Objeto hidráulico registrado en carga: {obj.Nombre}"); } Debug.WriteLine($"Registrados {hydraulicSimulationManager.HydraulicObjects.Count} objetos hidráulicos únicos tras cargar proyecto"); } #endregion #region Hydraulic Network Management /// /// Invalidates the hydraulic network to force recalculation /// public void InvalidateHydraulicNetwork() { try { if (hydraulicSimulationManager != null) { hydraulicSimulationManager.InvalidateNetwork(); Debug.WriteLine("Red hidráulica invalidada y marcada para recálculo"); } } catch (Exception ex) { Debug.WriteLine($"Error al invalidar red hidráulica: {ex.Message}"); } } #endregion #region MCP Server Management /// /// Inicia el servidor MCP /// private async void StartMcpServer() { try { if (_mcpServer != null) { _mcpServer.Dispose(); } _mcpServer = new MCPServer(this, McpServerPort); await _mcpServer.StartAsync(); Debug.WriteLine($"[MCP] Servidor MCP iniciado en puerto {McpServerPort}"); // Iniciar también el servidor de debug console StartDebugConsoleServer(); } catch (Exception ex) { Debug.WriteLine($"[MCP] Error iniciando servidor MCP: {ex.Message}"); MessageBox.Show($"Error iniciando servidor MCP: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// Detiene el servidor MCP /// private void StopMcpServer() { try { _mcpServer?.Stop(); Debug.WriteLine("[MCP] Servidor MCP detenido"); // Detener también el servidor de debug console StopDebugConsoleServer(); } catch (Exception ex) { Debug.WriteLine($"[MCP] Error deteniendo servidor MCP: {ex.Message}"); } } /// /// Limpia el servidor MCP al cerrar la aplicación /// private void CleanupMcpServer() { try { _mcpServer?.Dispose(); _mcpServer = null; // Limpiar también el servidor de debug console CleanupDebugConsoleServer(); } catch (Exception ex) { Debug.WriteLine($"[MCP] Error limpiando servidor MCP: {ex.Message}"); } } /// /// Inicia el servidor de debug console /// private void StartDebugConsoleServer() { try { if (_debugConsoleServer != null) { _debugConsoleServer.Dispose(); } _debugConsoleServer = new DebugConsoleServer(5007); // Puerto fijo para debug _debugConsoleServer.Start(); Debug.WriteLine("[Debug Console] Servidor de debug console iniciado en puerto 5007"); } catch (Exception ex) { Debug.WriteLine($"[Debug Console] Error iniciando servidor de debug console: {ex.Message}"); } } /// /// Detiene el servidor de debug console /// private void StopDebugConsoleServer() { try { _debugConsoleServer?.Stop(); Debug.WriteLine("[Debug Console] Servidor de debug console detenido"); } catch (Exception ex) { Debug.WriteLine($"[Debug Console] Error deteniendo servidor de debug console: {ex.Message}"); } } /// /// Limpia el servidor de debug console al cerrar la aplicación /// private void CleanupDebugConsoleServer() { try { _debugConsoleServer?.Dispose(); _debugConsoleServer = null; } catch (Exception ex) { Debug.WriteLine($"[Debug Console] Error limpiando servidor de debug console: {ex.Message}"); } } #endregion #region TSNet Integration Methods /// /// Versión simplificada para testing /// public void TestTSNetIntegrationSync() { try { Debug.WriteLine("Iniciando prueba de integración TSNet..."); MessageBox.Show("Prueba TSNet iniciada - revisar Debug console", "TSNet Test", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { Debug.WriteLine($"Error en prueba TSNet: {ex.Message}"); MessageBox.Show($"Error en prueba TSNet: {ex.Message}", "Error TSNet", MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// Versión mejorada para simulación TSNet sin fallbacks /// IMPORTANTE: Solo usa TSNet para todos los cálculos hidráulicos /// public void RunTSNetSimulationSync() { try { Debug.WriteLine("=== Iniciando simulación TSNet completa ==="); // 1. Resetear todos los valores calculados anteriores this.tsnetSimulationManager.ResetAllCalculatedValues(); Debug.WriteLine("TSNet: Valores calculados anteriores reseteados"); // 2. Registrar objetos hidráulicos con adaptadores var hydraulicCount = 0; foreach (var obj in ObjetosSimulables) { // Solo registrar objetos hidráulicos específicos if (obj.GetType().Name.Contains("osHyd")) { this.tsnetSimulationManager.RegisterHydraulicObject(obj); hydraulicCount++; Debug.WriteLine($"TSNet: Registrado {obj.Nombre} ({obj.GetType().Name})"); } } // 3. Validar configuración de todos los adaptadores var configErrors = this.tsnetSimulationManager.ValidateAllConfigurations(); if (configErrors.Count > 0) { var errorMsg = $"Errores de configuración encontrados:\n{string.Join("\n", configErrors)}"; Debug.WriteLine(errorMsg); MessageBox.Show(errorMsg, "Errores de Configuración TSNet", MessageBoxButton.OK, MessageBoxImage.Warning); return; } // 4. Reconstruir red this.tsnetSimulationManager.RebuildNetwork(); Debug.WriteLine("TSNet: Red hidráulica reconstruida"); // 5. Mostrar resumen de adaptadores creados var summary = GetAdaptersSummary(); Debug.WriteLine(summary); Debug.WriteLine($"=== Simulación TSNet preparada exitosamente ==="); MessageBox.Show($"Simulación TSNet lista:\n{summary}\n\nTodos los cálculos hidráulicos se realizarán exclusivamente en TSNet.", "TSNet Fase 2 - Sin Fallbacks", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { Debug.WriteLine($"Error ejecutando simulación TSNet: {ex.Message}"); MessageBox.Show($"Error ejecutando simulación TSNet: {ex.Message}", "Error TSNet", MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// Obtiene un resumen de los adaptadores creados /// private string GetAdaptersSummary() { var summary = new StringBuilder(); summary.AppendLine("Adaptadores TSNet creados:"); var tankCount = 0; var pumpCount = 0; var pipeCount = 0; foreach (var obj in ObjetosSimulables) { var objectId = obj.Id.Value.ToString(); if (this.tsnetSimulationManager.GetTankAdapter(objectId) != null) { tankCount++; summary.AppendLine($" - Tanque: {obj.Nombre}"); } else if (this.tsnetSimulationManager.GetPumpAdapter(objectId) != null) { pumpCount++; summary.AppendLine($" - Bomba: {obj.Nombre}"); } else if (this.tsnetSimulationManager.GetPipeAdapter(objectId) != null) { pipeCount++; summary.AppendLine($" - Tubería: {obj.Nombre}"); } } summary.AppendLine($"Total: {tankCount} tanques, {pumpCount} bombas, {pipeCount} tuberías"); return summary.ToString(); } /// /// Versión simplificada para generar INP /// private void GenerateINPFileSync() { try { Debug.WriteLine("Generando archivo INP..."); var hydraulicCount = 0; foreach (var obj in ObjetosSimulables) { if (obj is IHydraulicComponent) { hydraulicCount++; } } Debug.WriteLine($"INP generado con {hydraulicCount} objetos hidráulicos"); MessageBox.Show($"INP preparado con {hydraulicCount} objetos hidráulicos", "INP Test", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { Debug.WriteLine($"Error generando archivo INP: {ex.Message}"); MessageBox.Show($"Error generando archivo INP: {ex.Message}", "Error INP", MessageBoxButton.OK, MessageBoxImage.Error); } } #endregion } public class SimulationData { public ObservableCollection? ObjetosSimulables { get; set; } public UnitConverter? UnitConverter { get; set; } public PLCViewModel? PLC_ConnectionData { get; set; } // Nueva propiedad para almacenar los datos locales de objetos globales public ObservableCollection? DatosLocalesObjetos { get; set; } // 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; } } public class TipoSimulable { public string? Nombre { get; set; } public Type? Tipo { get; set; } public string? Categoria { get; set; } } public partial class CategoriaNode : ObservableObject { [ObservableProperty] private string nombre; [ObservableProperty] private ObservableCollection elementos; [ObservableProperty] private bool isExpanded = true; public CategoriaNode(string nombre) { this.nombre = nombre; elementos = new ObservableCollection(); } } public class TickSimulacionEventArgs : EventArgs { // Aquí puedes agregar propiedades o campos para pasar información adicional // en el evento TickSimulacion } }