Compare commits

...

7 Commits

Author SHA1 Message Date
Miguel 75c507be4e Se añadieron nuevas propiedades relacionadas con el encoder en la clase ucVMmotorSim, permitiendo la lectura del valor actual de la posición del encoder y la habilitación de su uso. Se eliminaron instancias innecesarias de Stopwatch en las clases ucEncoderMotor y ucEncoderMotorLineal, optimizando el código. Además, se realizaron ajustes en la interfaz de usuario de ucBoolTag para incluir una opción de visualización de descripción. 2025-06-23 15:29:40 +02:00
Miguel fefc0a700d Se añadió un nuevo método para verificar si un objeto visual es un hijo visual de otro, mejorando la gestión de transformaciones en la clase osBase. Además, se implementaron mejoras en la obtención de coordenadas de rectángulos, incluyendo validaciones para asegurar que los rectángulos estén cargados y disponibles antes de realizar transformaciones. Se mejoró la lógica de creación de geometría en ucTransporteTTop, asegurando que el layout esté actualizado y manejando excepciones para evitar fallos en la creación de simulaciones. 2025-06-22 16:44:19 +02:00
Miguel 58781c13a3 Se implementó una nueva ventana de configuración de escala que permite a los usuarios ajustar la escala de simulación de manera modeless. Se añadió un temporizador para aplicar automáticamente los cambios de escala después de 0.5 segundos. Además, se mejoró la gestión de la escala en el ViewModel principal y se actualizaron los bindings de posición en osBase para asegurar una correcta visualización. Se incluyó un botón de "Aplicar" en la interfaz de usuario para facilitar la aplicación de cambios. 2025-06-18 21:23:39 +02:00
Miguel b48dbeb76e Se añadió un nuevo método para configurar la escala desde el menú contextual en MainViewModel, permitiendo a los usuarios ajustar la escala de simulación. Se implementó la lógica para detener simulaciones, actualizar la escala en el convertidor de unidades y forzar el redibujo del canvas. Además, se agregó una opción en el menú contextual de MainWindow para acceder a esta funcionalidad. Se mejoró la gestión de bindings de posición y tamaño en osBase para asegurar actualizaciones adecuadas tras cambios de escala. 2025-06-18 19:54:51 +02:00
Miguel ca70f66ff1 Se añadió la funcionalidad para cargar datos de imágenes desde archivos JSON en la clase DatosDeTrabajo, mejorando la gestión de imágenes. Se implementó un nuevo método para obtener configuraciones de serialización JSON y se mejoró la lógica de carga de datos, incluyendo compatibilidad con versiones anteriores. Además, se actualizó el método de obtención de nombres de imágenes en MainViewModel para incluir etiquetas, y se ajustó el convertidor correspondiente en ImageDisplayNameConverter. 2025-06-18 18:55:04 +02:00
Miguel 354b4a8acf Se mejoró la funcionalidad de renombrado de imágenes en la interfaz, integrando un PropertyGrid para editar propiedades de imágenes, incluyendo etiquetas. Se actualizó la lógica para eliminar entradas vacías y se modificó el diseño de la ventana de renombrado para una mejor experiencia de usuario. Además, se implementó un editor de etiquetas que permite gestionar etiquetas de manera más eficiente. 2025-06-18 15:20:26 +02:00
Miguel 909e438f5b Se añadió la capacidad de gestionar datos de imágenes en la clase DatosDeTrabajo, permitiendo la carga de datos desde archivos JSON y la integración con MainViewModel. Se implementó un nuevo método para establecer el ViewModel principal y se mejoró la lógica de renombrado de imágenes en la interfaz de usuario, incluyendo un comando para renombrar imágenes desde el contexto del ListBox. Además, se incorporó un convertidor para mostrar nombres de imágenes personalizados en la interfaz. 2025-06-18 13:40:49 +02:00
26 changed files with 1248 additions and 108 deletions

26
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net8.0-windows8.0/CtrEditor.dll",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

41
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/CtrEditor.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/CtrEditor.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/CtrEditor.sln"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@ -0,0 +1,28 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace CtrEditor.Converters
{
public class ImageDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string imageName)
{
// Buscar el MainViewModel en el Application.Current.MainWindow.DataContext
if (Application.Current?.MainWindow?.DataContext is MainViewModel viewModel)
{
return viewModel.GetImageDisplayNameWithTags(imageName);
}
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace CtrEditor.Converters
{
public class RegionalFloatConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is float floatValue)
{
// Usar la cultura actual para mostrar el número con el separador decimal correcto
return floatValue.ToString("N4", CultureInfo.CurrentCulture);
}
return value?.ToString() ?? string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string stringValue)
{
// Intentar parsing con la cultura actual primero
if (float.TryParse(stringValue, NumberStyles.Float, CultureInfo.CurrentCulture, out float result))
{
return result;
}
// Si falla, intentar con cultura invariante (punto como separador)
if (float.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out result))
{
return result;
}
// Si ambos fallan, intentar reemplazar punto por coma o viceversa
string adjustedString = stringValue.Replace(',', '.').Replace(".", CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);
if (float.TryParse(adjustedString, NumberStyles.Float, CultureInfo.CurrentCulture, out result))
{
return result;
}
}
return 0.0f; // Valor por defecto si no se puede parsear
}
}
}

View File

@ -118,8 +118,7 @@ namespace CtrEditor
_mainViewModel.PLCViewModel = _globalState.PLCConfiguration ?? new PLCViewModel();
if (_globalState.UnitConverter != null)
PixelToMeter.Instance.calc = _globalState.UnitConverter;
else
PixelToMeter.Instance.calc = new UnitConverter(1.0f); // Valor por defecto
// No crear un nuevo UnitConverter si no hay uno guardado, mantener el actual
// Restaurar objetos globales
foreach (var obj in _globalState.SharedObjects)

View File

@ -5,12 +5,14 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace CtrEditor
{
public class DatosDeTrabajo
{
public Dictionary<string, string> Imagenes { get; private set; }
private MainViewModel _mainViewModel;
public DatosDeTrabajo()
{
@ -18,6 +20,11 @@ namespace CtrEditor
CargarImagenes(); // Inicializar la carga de imágenes basada en el directorio persistente
}
public void SetMainViewModel(MainViewModel mainViewModel)
{
_mainViewModel = mainViewModel;
}
public string ObtenerPathImagenConExtension(string key, string extension)
{
if (Imagenes.TryGetValue(key, out string imagePath))
@ -51,7 +58,85 @@ namespace CtrEditor
Imagenes[nombreArchivo] = archivo;
}
}
// Cargar datos de imágenes existentes desde archivos JSON
if (_mainViewModel != null)
{
CargarDatosImagenesExistentes();
}
}
}
private void CargarDatosImagenesExistentes()
{
var jsonSerializerSettings = GetJsonSerializerSettings();
foreach (var imageName in Imagenes.Keys)
{
string jsonPath = ObtenerPathImagenConExtension(imageName, ".json");
if (!string.IsNullOrEmpty(jsonPath) && File.Exists(jsonPath))
{
try
{
// Cargar datos completos del archivo JSON
string jsonString = File.ReadAllText(jsonPath);
var simulationData = Newtonsoft.Json.JsonConvert.DeserializeObject<SimulationData>(jsonString, jsonSerializerSettings);
// Cargar datos de imágenes si existen en el archivo
if (simulationData?.ImageDataDictionary != null)
{
foreach (var imageDataEntry in simulationData.ImageDataDictionary)
{
// Solo cargar si no existe ya en el diccionario principal
if (!_mainViewModel._imageDataDictionary.ContainsKey(imageDataEntry.Key))
{
_mainViewModel._imageDataDictionary[imageDataEntry.Key] = imageDataEntry.Value;
}
}
}
// Compatibilidad con versiones anteriores (ImageCustomNames)
#pragma warning disable CS0618 // Type or member is obsolete
if (simulationData?.ImageCustomNames != null)
{
foreach (var customName in simulationData.ImageCustomNames)
{
var imageData = _mainViewModel.GetOrCreateImageData(customName.Key);
if (string.IsNullOrEmpty(imageData.CustomName)) // Solo actualizar si no tiene nombre personalizado
{
imageData.CustomName = customName.Value;
}
}
}
#pragma warning restore CS0618
}
catch (Exception ex)
{
// Log del error pero no fallar la carga completa
System.Diagnostics.Debug.WriteLine($"Error al cargar datos de imagen desde {jsonPath}: {ex.Message}");
// Como fallback, crear una instancia vacía
_mainViewModel.GetOrCreateImageData(imageName);
}
}
else
{
// Si no existe archivo JSON, crear instancia vacía
_mainViewModel.GetOrCreateImageData(imageName);
}
}
}
private Newtonsoft.Json.JsonSerializerSettings GetJsonSerializerSettings()
{
return new Newtonsoft.Json.JsonSerializerSettings
{
Formatting = Newtonsoft.Json.Formatting.Indented,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto,
ObjectCreationHandling = Newtonsoft.Json.ObjectCreationHandling.Replace,
ConstructorHandling = Newtonsoft.Json.ConstructorHandling.AllowNonPublicDefaultConstructor
};
}
}
}

View File

@ -81,6 +81,12 @@ namespace CtrEditor
[ObservableProperty]
public ObservableCollection<TipoSimulable> listaOsBase;
// Diccionario para almacenar datos expandidos de imágenes
public Dictionary<string, Models.ImageData> _imageDataDictionary = new Dictionary<string, Models.ImageData>();
// Comando para renombrar imagen
public ICommand RenameImageCommand { get; private set; }
public ICommand StartSimulationCommand { get; }
public ICommand StopSimulationCommand { get; }
public ICommand ItemDoubleClickCommand { get; private set; }
@ -383,6 +389,7 @@ namespace CtrEditor
TBMultiPageAnalizeCommand = new RelayCommand(MultiPageAnalizeCommand);
TBMultiPageMatrixCommand = new RelayCommand(MultiPageMatrixCommand);
TBLibraryManagerCommand = new RelayCommand(ShowLibraryManager);
RenameImageCommand = new RelayCommand<string>(RenameImage);
stopwatch_Sim = new Stopwatch();
stopwatch_Sim.Start();
@ -398,6 +405,73 @@ namespace CtrEditor
_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);
}
// 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
@ -1162,6 +1236,77 @@ namespace CtrEditor
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();
StopFluidSimulation();
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<string, PopUps.LibraryWindow> _libraryWindows = new();
@ -1202,6 +1347,13 @@ namespace CtrEditor
// Nueva propiedad para almacenar los datos locales de objetos globales
public ObservableCollection<DatosLocales>? DatosLocalesObjetos { get; set; }
// Diccionario para almacenar datos expandidos de imágenes
public Dictionary<string, Models.ImageData>? ImageDataDictionary { get; set; }
// Compatibilidad con versiones anteriores - OBSOLETO
[System.Obsolete("Use ImageDataDictionary instead")]
public Dictionary<string, string>? ImageCustomNames { get; set; }
}
public class TipoSimulable

View File

@ -6,6 +6,7 @@
xmlns:local="clr-namespace:CtrEditor"
xmlns:controls="clr-namespace:CtrEditor.Controls"
xmlns:uc="clr-namespace:CtrEditor.ObjetosSim.UserControls"
xmlns:converters="clr-namespace:CtrEditor.Converters"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ObjetosSim="clr-namespace:CtrEditor.ObjetosSim"
@ -48,6 +49,9 @@
<!-- Converter for PLC Connect/Disconnect image -->
<local:ConnectStateToImageConverter x:Key="ConnectStateToImageConverter" />
<!-- Converter for Image Display Names -->
<converters:ImageDisplayNameConverter x:Key="ImageDisplayNameConverter" />
</Window.Resources>
<Grid>
@ -87,7 +91,18 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox x:Name="ListaImagenes" Grid.Row="0" Margin="5" ItemsSource="{Binding ListaImagenes}"
SelectedItem="{Binding SelectedImage}" />
SelectedItem="{Binding SelectedImage}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource ImageDisplayNameConverter}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Propiedades" Command="{Binding RenameImageCommand}" CommandParameter="{Binding PlacementTarget.SelectedItem, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<ListBox x:Name="ListaFunciones" Grid.Row="1" Margin="5" ItemsSource="{Binding ListaOsBase}"
DisplayMemberPath="Nombre" SelectedItem="{Binding SelectedItem}">
<i:Interaction.Triggers>

View File

@ -713,6 +713,13 @@ namespace CtrEditor
contextMenu.Items.Add(new Separator());
}
// Agregar opción de configurar escala
var scaleConfigItem = new MenuItem { Header = "Configurar Escala..." };
scaleConfigItem.Click += (s, e) => viewModel.ConfigureScale();
contextMenu.Items.Add(scaleConfigItem);
contextMenu.Items.Add(new Separator());
// Agregar información del sistema de undo
var undoHistoryCount = _objectManager.GetUndoHistoryCount();
var canUndo = _objectManager.CanUndo();

110
Models/ImageData.cs Normal file
View File

@ -0,0 +1,110 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using CtrEditor.ObjetosSim;
using Xceed.Wpf.Toolkit.PropertyGrid.Editors;
using System.Linq;
namespace CtrEditor.Models
{
public class ImageData : INotifyPropertyChanged
{
private string _customName;
private List<string> _tags;
private string _etiquetas;
public string FileName { get; set; }
[DisplayName("Nombre personalizado")]
public string CustomName
{
get => _customName;
set
{
_customName = value;
OnPropertyChanged();
}
}
[Browsable(false)]
public List<string> Tags
{
get => _tags ?? new List<string>();
set
{
_tags = value;
_etiquetas = string.Join(" ", value.Select(tag => tag.StartsWith("#") ? tag : "#" + tag));
OnPropertyChanged();
OnPropertyChanged(nameof(Etiquetas));
}
}
[DisplayName("Etiquetas")]
[property: Editor(typeof(TagPropertyEditor), typeof(TagPropertyEditor))]
public string Etiquetas
{
get => _etiquetas ?? string.Empty;
set
{
_etiquetas = value ?? string.Empty;
// Convertir string de etiquetas a List<string>
if (string.IsNullOrWhiteSpace(_etiquetas))
{
_tags = new List<string>();
}
else
{
_tags = _etiquetas.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Select(tag => tag.StartsWith("#") ? tag.Substring(1) : tag)
.Where(tag => !string.IsNullOrWhiteSpace(tag))
.ToList();
}
OnPropertyChanged();
OnPropertyChanged(nameof(Tags));
}
}
/// <summary>
/// Lista de etiquetas sin el prefijo #, compatible con osBase
/// </summary>
[Browsable(false)]
public List<string> ListaEtiquetas
{
get
{
if (string.IsNullOrWhiteSpace(Etiquetas))
return new List<string>();
return Etiquetas.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Select(tag => tag.StartsWith("#") ? tag.Substring(1) : tag)
.Where(tag => !string.IsNullOrWhiteSpace(tag))
.ToList();
}
}
public string DisplayName => !string.IsNullOrEmpty(CustomName) ? CustomName : FileName;
public ImageData()
{
FileName = string.Empty;
_customName = string.Empty;
_tags = new List<string>();
_etiquetas = string.Empty;
}
public ImageData(string fileName, string customName = null, List<string> tags = null)
{
FileName = fileName;
_customName = customName ?? string.Empty;
_tags = tags ?? new List<string>();
_etiquetas = string.Join(" ", _tags.Select(tag => tag.StartsWith("#") ? tag : "#" + tag));
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -169,8 +169,54 @@ namespace CtrEditor.ObjetosSim
if (_visualRepresentation is ucTransporteTTop uc)
{
SimGeometria = AddRectangle(simulationManager, uc.Transporte, Alto, Ancho, Angulo);
CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion);
try
{
// Asegurar que el layout esté actualizado antes de crear la geometría
uc.UpdateLayout();
// Validar que el rectángulo esté disponible y tenga dimensiones válidas
if (uc.Transporte != null &&
(!double.IsNaN(uc.Transporte.ActualWidth) && uc.Transporte.ActualWidth > 0) ||
(!double.IsNaN(uc.Transporte.Width) && uc.Transporte.Width > 0))
{
SimGeometria = AddRectangle(simulationManager, uc.Transporte, Alto, Ancho, Angulo);
CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion);
}
else
{
// Si el rectángulo no está listo, intentar después del próximo layout
uc.Dispatcher.BeginInvoke(new Action(() =>
{
if (uc.Transporte != null && simulationManager != null)
{
SimGeometria = AddRectangle(simulationManager, uc.Transporte, Alto, Ancho, Angulo);
CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion);
}
}), System.Windows.Threading.DispatcherPriority.Loaded);
}
}
catch (Exception ex)
{
// Log del error para diagnóstico pero continuar sin fallar
System.Diagnostics.Debug.WriteLine($"Error al crear geometría de simulación: {ex.Message}");
// Intentar crear la geometría más tarde
uc.Dispatcher.BeginInvoke(new Action(() =>
{
try
{
if (uc.Transporte != null && simulationManager != null)
{
SimGeometria = AddRectangle(simulationManager, uc.Transporte, Alto, Ancho, Angulo);
CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion);
}
}
catch (Exception ex2)
{
System.Diagnostics.Debug.WriteLine($"Segundo intento falló: {ex2.Message}");
}
}), System.Windows.Threading.DispatcherPriority.ApplicationIdle);
}
}
}
public override void ucUnLoaded()

View File

@ -2,11 +2,13 @@
using CtrEditor.FuncionesBase;
using LibS7Adv;
using Newtonsoft.Json;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace CtrEditor.ObjetosSim
{
/// <summary>
@ -59,6 +61,16 @@ namespace CtrEditor.ObjetosSim
[ObservableProperty]
public float tiempoRampa;
[ObservableProperty]
[property: Description("Enable read of the Motor encoder simulated on PLC.")]
[property: Category("Encoder:")]
bool motor_With_Encoder;
[ObservableProperty]
[property: Description("Actual Value of the encoder position.")]
[property: Category("Encoder:")]
public float actual_Position;
partial void OnTiempoRampaChanged(float value)
{
if (value < 0.1f)
@ -205,8 +217,13 @@ namespace CtrEditor.ObjetosSim
// Add timestamp to trace when the read occurs
var timestamp = DateTime.Now;
// Read ControlWord and track the raw response
var rawResponse = plc.LeerTagDInt($"\"DB MotorSimulate\".Motors[{DB_Motor}].ControlWord");
if (Data.Motor_With_Encoder)
Data.Actual_Position = (float)plc.LeerTagDInt($"\"DB MotorSimulate\".Motors[{DB_Motor}].ActualPosition");
else
Data.Actual_Position = 0;
// Read ControlWord and track the raw response
var rawResponse = plc.LeerTagDInt($"\"DB MotorSimulate\".Motors[{DB_Motor}].ControlWord");
int controlWord = rawResponse ?? 0;
var control = VMMotorBitPacker.UnpackControlWord(controlWord);

View File

@ -14,8 +14,6 @@ namespace CtrEditor.ObjetosSim
public partial class osEncoderMotor : osBase, IosBase
{
private osBase Motor = null;
private Stopwatch Stopwatch = new Stopwatch();
private double stopwatch_last = 0;
public static string NombreClase()
{
@ -86,7 +84,6 @@ namespace CtrEditor.ObjetosSim
Pulsos_Por_Vuelta = 360; // Por defecto, un pulso por grado
Ratio_Giros_50hz = 1; // Por defecto, 1 giro por cada 50Hz
Color_oculto = Brushes.Gray;
Stopwatch.Start();
}
public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds)

View File

@ -14,8 +14,6 @@ namespace CtrEditor.ObjetosSim
public partial class osEncoderMotorLineal : osBase, IosBase
{
private osBase Motor = null;
private Stopwatch Stopwatch = new Stopwatch();
private double stopwatch_last = 0;
public static string NombreClase()
{
@ -86,7 +84,6 @@ namespace CtrEditor.ObjetosSim
{
Pulsos_Por_Hz = 3000; // Por defecto, un pulso por grado
Color_oculto = Brushes.Gray;
Stopwatch.Start();
}
public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds)
@ -100,7 +97,8 @@ namespace CtrEditor.ObjetosSim
Valor_Actual = (int)value;
return;
}
} else if (Motor != null && Motor is osVMmotorSim motor)
}
else if (Motor != null && Motor is osVMmotorSim motor)
{
VelocidadActual = motor.Velocidad;
@ -116,7 +114,10 @@ namespace CtrEditor.ObjetosSim
float incrementoPulsos = pulsosPorHz * segundosTranscurridos;
// Actualizar valor del encoder
Valor_Actual = (Valor_Actual + incrementoPulsos);
if (motor.Motor_With_Encoder)
Valor_Actual = motor.Actual_Position;
else
Valor_Actual = (Valor_Actual + incrementoPulsos);
// Actualizar color basado en si está girando
Color_oculto = Math.Abs(pulsosPorHz) > 0.01f ? Brushes.LightGreen : Brushes.Gray;

View File

@ -5,6 +5,7 @@ using System.Windows.Data;
using CtrEditor.PopUps;
using Xceed.Wpf.Toolkit.PropertyGrid;
using Xceed.Wpf.Toolkit.PropertyGrid.Editors;
using CtrEditor.Models;
namespace CtrEditor.ObjetosSim
{
@ -50,18 +51,18 @@ namespace CtrEditor.ObjetosSim
{
try
{
// Obtener el objeto que se está editando
if (propertyItem.Instance is osBase osObject)
{
// Obtener el MainWindow para acceder a todos los objetos
var mainWindow = Application.Current.Windows
.OfType<MainWindow>()
.FirstOrDefault();
// Obtener el MainWindow para acceder a todos los objetos
var mainWindow = Application.Current.Windows
.OfType<MainWindow>()
.FirstOrDefault();
if (mainWindow?.DataContext is MainViewModel mainViewModel)
if (mainWindow?.DataContext is MainViewModel mainViewModel)
{
// Determinar si el objeto es osBase o ImageData
if (propertyItem.Instance is osBase osObject)
{
// Abrir el editor de etiquetas
var tagEditor = new TagEditorWindow(osObject, mainViewModel.ObjetosSimulables);
// Abrir el editor de etiquetas para osBase
var tagEditor = new TagEditorWindow(osObject, mainViewModel.ObjetosSimulables, mainViewModel._imageDataDictionary);
tagEditor.Owner = mainWindow;
if (tagEditor.ShowDialog() == true)
@ -71,6 +72,22 @@ namespace CtrEditor.ObjetosSim
textBox.GetBindingExpression(TextBox.TextProperty)?.UpdateTarget();
}
}
else if (propertyItem.Instance is ImageData imageData)
{
// Para ImageData, crear un osBase temporal para usar el editor
var tempOsBase = new osTextPlate();
tempOsBase.Etiquetas = imageData.Etiquetas;
var tagEditor = new TagEditorWindow(tempOsBase, mainViewModel.ObjetosSimulables, mainViewModel._imageDataDictionary);
tagEditor.Owner = mainWindow;
if (tagEditor.ShowDialog() == true)
{
// Actualizar ImageData con las nuevas etiquetas
imageData.Etiquetas = tempOsBase.Etiquetas;
textBox.GetBindingExpression(TextBox.TextProperty)?.UpdateTarget();
}
}
}
}
catch (Exception ex)

View File

@ -1,17 +1,14 @@
<UserControl x:Class="CtrEditor.ObjetosSim.ucBoolTag"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:ei="http://schemas.microsoft.com/xaml/behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<UserControl x:Class="CtrEditor.ObjetosSim.ucBoolTag" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:ei="http://schemas.microsoft.com/xaml/behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:osBoolTag/>
<vm:osBoolTag Show_Description="True" />
</UserControl.DataContext>
<Canvas RenderTransformOrigin="0.5,0.5">
<Canvas RenderTransformOrigin="0.5,0.5" ToolTip="{Binding Descripcion}">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform />
@ -26,18 +23,14 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border x:Name="BackgroundRectangle"
BorderBrush="Black"
BorderThickness="2"
CornerRadius="10,0,0,10"
Width="30"
Height="40"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Background="{Binding Color, Converter={StaticResource ColorToBrushConverter}}"/>
<Label Content="{Binding Descripcion}" Grid.Column="1" VerticalAlignment="Center" Background="{Binding Color}" />
<Border x:Name="BackgroundRectangle" BorderBrush="Black" BorderThickness="2" CornerRadius="10,0,0,10"
Width="16" Height="25" VerticalAlignment="Center" HorizontalAlignment="Center"
Background="{Binding Color, Converter={StaticResource ColorToBrushConverter}}" />
<Label Content="{Binding Descripcion}" Grid.Column="1" VerticalAlignment="Center"
Background="{Binding Color}"
Visibility="{Binding Show_Description, Converter={StaticResource BooleanToVisibilityConverter}}" />
<CheckBox Grid.Column="2" VerticalAlignment="Center" IsChecked="{Binding Estado}"
Background="{Binding Color_oculto}" />
Background="{Binding Color_oculto}" />
</Grid>
</Canvas>
</UserControl>

View File

@ -39,6 +39,9 @@ namespace CtrEditor.ObjetosSim
[ObservableProperty]
public bool estado;
[ObservableProperty]
public bool show_Description;
partial void OnEstadoChanged(bool value)
{
EscribirBitTag(Tag, value);
@ -69,6 +72,7 @@ namespace CtrEditor.ObjetosSim
tag = "%M50.0";
Descripcion = "Nombre del Tag";
Color = Colors.LightBlue;
Show_Description = true;
}
public override void UpdatePLCPrimerCiclo()

View File

@ -7,7 +7,7 @@ using LibS7Adv;
using nkast.Aether.Physics2D.Common;
using PaddleOCRSharp;
using Siemens.Simatic.Simulation.Runtime;
using System.ComponentModel;
using System.ComponentModel; // Para poder usar [property: Category ...
using System.Diagnostics;
using System.IO;
using System.Windows;
@ -1195,6 +1195,21 @@ namespace CtrEditor.ObjetosSim
CanvasSetTopinMeter(Top);
}
/// <summary>
/// Fuerza la actualización de todos los bindings de posición y tamaño
/// disparando PropertyChanged para Left, Top, Ancho, Alto
/// </summary>
public void ForceUpdatePositionBindings()
{
// Para Left y Top, actualizar directamente el Canvas ya que tienen lógica especial en OnChanged
CanvasSetLeftinMeter(Left);
CanvasSetTopinMeter(Top);
// Para Ancho y Alto, disparar PropertyChanged para que los bindings se actualicen
OnPropertyChanged(nameof(Ancho));
OnPropertyChanged(nameof(Alto));
}
public bool LeerBitTag(string Tag)
@ -1349,13 +1364,65 @@ namespace CtrEditor.ObjetosSim
return transformGroup;
}
private static bool IsVisualChild(Visual child, Visual parent)
{
if (child == null || parent == null)
return false;
try
{
// Intentar obtener el ancestro común
var transform = child.TransformToAncestor(parent);
return transform != null;
}
catch (InvalidOperationException)
{
return false;
}
}
public (Vector2 TopLeft, Vector2 BottomRight) GetRectangleCoordinatesInMeter(Rectangle rect)
{
if (rect != null)
if (rect == null)
return (new Vector2(0, 0), new Vector2(0, 0));
var _canvasLeft = CanvasGetLeftinMeter();
var _canvasTop = CanvasGetTopinMeter();
try
{
var _canvasLeft = CanvasGetLeftinMeter();
var _canvasTop = CanvasGetTopinMeter();
// Verificar que tanto el rectángulo como la representación visual estén cargados
if (!rect.IsLoaded || _visualRepresentation == null || !_visualRepresentation.IsLoaded)
{
// Fallback: usar las propiedades básicas de posición si la transformación no está disponible
float rectWidth = (float)rect.Width;
float rectHeight = (float)rect.Height;
if (double.IsNaN(rectWidth) || rectWidth == 0)
rectWidth = Ancho * PixelToMeter.Instance.calc.MetersToPixels(1);
if (double.IsNaN(rectHeight) || rectHeight == 0)
rectHeight = Alto * PixelToMeter.Instance.calc.MetersToPixels(1);
return (new Vector2(_canvasLeft, _canvasTop),
new Vector2(_canvasLeft + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth),
_canvasTop + PixelToMeter.Instance.calc.PixelsToMeters(rectHeight)));
}
// Forzar actualización del layout
_visualRepresentation.UpdateLayout();
// Verificar que el rectángulo sea un descendiente visual de _visualRepresentation
if (!IsVisualChild(rect, _visualRepresentation))
{
// Fallback: usar las dimensiones básicas
float rectWidth = (float)rect.ActualWidth;
float rectHeight = (float)rect.ActualHeight;
return (new Vector2(_canvasLeft, _canvasTop),
new Vector2(_canvasLeft + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth),
_canvasTop + PixelToMeter.Instance.calc.PixelsToMeters(rectHeight)));
}
// Obtiene la transformada del objeto visual
GeneralTransform transform = rect.TransformToAncestor(_visualRepresentation);
@ -1364,10 +1431,26 @@ namespace CtrEditor.ObjetosSim
Point topLeft = transform.Transform(new Point(0, 0));
Point bottomRight = transform.Transform(new Point(rect.ActualWidth, rect.ActualHeight));
return (new Vector2(PixelToMeter.Instance.calc.PixelsToMeters((float)topLeft.X) + _canvasLeft, PixelToMeter.Instance.calc.PixelsToMeters((float)topLeft.Y) + _canvasTop),
new Vector2(PixelToMeter.Instance.calc.PixelsToMeters((float)bottomRight.X) + _canvasLeft, PixelToMeter.Instance.calc.PixelsToMeters((float)bottomRight.Y) + _canvasTop));
return (new Vector2(PixelToMeter.Instance.calc.PixelsToMeters((float)topLeft.X) + _canvasLeft,
PixelToMeter.Instance.calc.PixelsToMeters((float)topLeft.Y) + _canvasTop),
new Vector2(PixelToMeter.Instance.calc.PixelsToMeters((float)bottomRight.X) + _canvasLeft,
PixelToMeter.Instance.calc.PixelsToMeters((float)bottomRight.Y) + _canvasTop));
}
catch (InvalidOperationException)
{
// Fallback en caso de error de transformación
float rectWidth = (float)rect.ActualWidth;
float rectHeight = (float)rect.ActualHeight;
if (double.IsNaN(rectWidth) || rectWidth == 0)
rectWidth = Ancho * PixelToMeter.Instance.calc.MetersToPixels(1);
if (double.IsNaN(rectHeight) || rectHeight == 0)
rectHeight = Alto * PixelToMeter.Instance.calc.MetersToPixels(1);
return (new Vector2(_canvasLeft, _canvasTop),
new Vector2(_canvasLeft + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth),
_canvasTop + PixelToMeter.Instance.calc.PixelsToMeters(rectHeight)));
}
else return (new Vector2(0, 0), new Vector2(0, 0));
}
public (Vector2 Start, Vector2 End) GetCenterLineVectors(Rectangle rect)
@ -1381,27 +1464,56 @@ namespace CtrEditor.ObjetosSim
// Usar Dispatcher para asegurar la ejecución en el hilo correcto
_visualRepresentation?.Dispatcher.Invoke(() =>
{
// Asegúrate de que el control está en el árbol visual y actualizado
if (_visualRepresentation.IsLoaded && rect.IsLoaded)
try
{
_visualRepresentation.UpdateLayout();
// Asegúrate de que el control está en el árbol visual y actualizado
if (_visualRepresentation.IsLoaded && rect.IsLoaded && IsVisualChild(rect, _visualRepresentation))
{
_visualRepresentation.UpdateLayout();
var _canvasLeft = CanvasGetLeftinMeter();
var _canvasTop = CanvasGetTopinMeter();
var transform = rect.TransformToAncestor(_visualRepresentation);
// Puntos en coordenadas locales del rectángulo no rotado
Point startLocal = new Point(0, rect.ActualHeight / 2);
Point endLocal = new Point(rect.ActualWidth, rect.ActualHeight / 2);
// Transformar estos puntos al sistema de coordenadas del ancestro
Point transformedStart = transform.Transform(startLocal);
Point transformedEnd = transform.Transform(endLocal);
// Convierte a unidades de Farseer (metros en este caso)
start = new Vector2(PixelToMeter.Instance.calc.PixelsToMeters((float)transformedStart.X) + _canvasLeft, PixelToMeter.Instance.calc.PixelsToMeters((float)transformedStart.Y) + _canvasTop);
end = new Vector2(PixelToMeter.Instance.calc.PixelsToMeters((float)transformedEnd.X) + _canvasLeft, PixelToMeter.Instance.calc.PixelsToMeters((float)transformedEnd.Y) + _canvasTop);
}
else
{
// Fallback: usar coordenadas básicas
var _canvasLeft = CanvasGetLeftinMeter();
var _canvasTop = CanvasGetTopinMeter();
float rectWidth = (float)rect.ActualWidth;
if (double.IsNaN(rectWidth) || rectWidth == 0)
rectWidth = Ancho * PixelToMeter.Instance.calc.MetersToPixels(1);
start = new Vector2(_canvasLeft, _canvasTop + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth / 2));
end = new Vector2(_canvasLeft + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth), _canvasTop + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth / 2));
}
}
catch (InvalidOperationException)
{
// Fallback en caso de error de transformación
var _canvasLeft = CanvasGetLeftinMeter();
var _canvasTop = CanvasGetTopinMeter();
var transform = rect.TransformToAncestor(_visualRepresentation);
float rectWidth = (float)rect.ActualWidth;
if (double.IsNaN(rectWidth) || rectWidth == 0)
rectWidth = Ancho * PixelToMeter.Instance.calc.MetersToPixels(1);
// Puntos en coordenadas locales del rectángulo no rotado
Point startLocal = new Point(0, rect.ActualHeight / 2);
Point endLocal = new Point(rect.ActualWidth, rect.ActualHeight / 2);
// Transformar estos puntos al sistema de coordenadas del ancestro
Point transformedStart = transform.Transform(startLocal);
Point transformedEnd = transform.Transform(endLocal);
// Convierte a unidades de Farseer (metros en este caso)
start = new Vector2(PixelToMeter.Instance.calc.PixelsToMeters((float)transformedStart.X) + _canvasLeft, PixelToMeter.Instance.calc.PixelsToMeters((float)transformedStart.Y) + _canvasTop);
end = new Vector2(PixelToMeter.Instance.calc.PixelsToMeters((float)transformedEnd.X) + _canvasLeft, PixelToMeter.Instance.calc.PixelsToMeters((float)transformedEnd.Y) + _canvasTop);
start = new Vector2(_canvasLeft, _canvasTop + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth / 2));
end = new Vector2(_canvasLeft + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth), _canvasTop + PixelToMeter.Instance.calc.PixelsToMeters(rectWidth / 2));
}
});

View File

@ -0,0 +1,57 @@
<Window x:Class="CtrEditor.PopUps.RenameImageWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
Title="Editar Información de Imagen" Height="400" Width="500"
WindowStartupLocation="CenterOwner" ResizeMode="CanResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Archivo original:" FontWeight="Bold" Margin="0,0,0,5"/>
<TextBlock Grid.Row="1" x:Name="OriginalNameTextBlock" Margin="0,0,0,15" Foreground="Gray"/>
<GroupBox Grid.Row="2" Header="Propiedades de la Imagen" Margin="0,0,0,15">
<xctk:PropertyGrid x:Name="PropertyGridControl"
ShowSortOptions="False"
ShowSearchBox="False"
ShowSummary="True"
ShowAdvancedOptions="False"
ShowTitle="False"
Margin="5">
<xctk:PropertyGrid.EditorDefinitions>
<xctk:EditorTemplateDefinition TargetProperties="Etiquetas">
<xctk:EditorTemplateDefinition.EditingTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"
VerticalAlignment="Stretch"/>
<Button Grid.Column="1"
Content="..."
Width="25"
Margin="2,0,0,0"
ToolTip="Abrir editor de etiquetas"
Click="TagEditorButton_Click"/>
</Grid>
</DataTemplate>
</xctk:EditorTemplateDefinition.EditingTemplate>
</xctk:EditorTemplateDefinition>
</xctk:PropertyGrid.EditorDefinitions>
</xctk:PropertyGrid>
</GroupBox>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="OkButton" Content="Aceptar" Width="80" Margin="0,0,10,0" IsDefault="True" Click="OkButton_Click"/>
<Button x:Name="CancelButton" Content="Cancelar" Width="80" IsCancel="True" Click="CancelButton_Click"/>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,82 @@
using System.Windows;
using CtrEditor.Models;
using CtrEditor.ObjetosSim;
using Xceed.Wpf.Toolkit.PropertyGrid;
namespace CtrEditor.PopUps
{
public partial class RenameImageWindow : Window
{
public ImageData ImageData { get; private set; }
public bool IsOkClicked { get; private set; }
public RenameImageWindow(string originalName, ImageData imageData = null)
{
InitializeComponent();
OriginalNameTextBlock.Text = originalName;
// Crear o usar ImageData existente
ImageData = imageData ?? new ImageData
{
FileName = originalName,
CustomName = string.Empty,
Etiquetas = string.Empty
};
// Configurar PropertyGrid
PropertyGridControl.SelectedObject = ImageData;
Focus();
}
private void TagEditorButton_Click(object sender, RoutedEventArgs e)
{
try
{
// Obtener el MainWindow para acceder a todos los objetos
var mainWindow = Application.Current.Windows
.OfType<MainWindow>()
.FirstOrDefault();
if (mainWindow?.DataContext is MainViewModel mainViewModel)
{
// Para ImageData, crear un osBase temporal para usar el editor
var tempOsBase = new osTextPlate();
tempOsBase.Etiquetas = ImageData.Etiquetas;
var tagEditor = new TagEditorWindow(tempOsBase, mainViewModel.ObjetosSimulables, mainViewModel._imageDataDictionary);
tagEditor.Owner = this;
if (tagEditor.ShowDialog() == true)
{
// Actualizar ImageData con las nuevas etiquetas
ImageData.Etiquetas = tempOsBase.Etiquetas;
// Forzar actualización del PropertyGrid
PropertyGridControl.Update();
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Error al abrir el editor de etiquetas: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
IsOkClicked = true;
DialogResult = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
IsOkClicked = false;
DialogResult = false;
Close();
}
}
}

View File

@ -0,0 +1,111 @@
<Window x:Class="CtrEditor.PopUps.ScaleConfigWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:CtrEditor.Converters"
Title="Configurar Escala"
Height="280"
Width="450"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize">
<Window.Resources>
<converters:RegionalFloatConverter x:Key="RegionalFloatConverter"/>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Título -->
<TextBlock Grid.Row="0"
Text="Configuración de Escala de Conversión"
FontWeight="Bold"
FontSize="14"
Margin="0,0,0,15"/>
<!-- Escala actual -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Escala actual: " VerticalAlignment="Center" Width="120"/>
<TextBlock VerticalAlignment="Center" FontWeight="Bold">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} m/pixel">
<Binding Path="CurrentScale" Converter="{StaticResource RegionalFloatConverter}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<!-- Nueva escala -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Nueva escala: " VerticalAlignment="Center" Width="120"/>
<TextBox x:Name="ScaleTextBox"
Text="{Binding NewScale, Converter={StaticResource RegionalFloatConverter}, UpdateSourceTrigger=PropertyChanged}"
Width="100"
VerticalAlignment="Center"
Margin="0,0,5,0"/>
<TextBlock Text="m/pixel" VerticalAlignment="Center"/>
</StackPanel>
<!-- Presets -->
<StackPanel Grid.Row="3" Orientation="Vertical" Margin="0,0,0,15">
<TextBlock Text="Presets comunes:" FontWeight="SemiBold" Margin="0,0,0,5"/>
<WrapPanel>
<Button Content="1:1 (0.01)" Command="{Binding SetPresetCommand}" CommandParameter="0.01" Margin="0,0,5,5"/>
<Button Content="1:10 (0.001)" Command="{Binding SetPresetCommand}" CommandParameter="0.001" Margin="0,0,5,5"/>
<Button Content="1:100 (0.0001)" Command="{Binding SetPresetCommand}" CommandParameter="0.0001" Margin="0,0,5,5"/>
<Button Content="1 px = 1 cm (0.01)" Command="{Binding SetPresetCommand}" CommandParameter="0.01" Margin="0,0,5,5"/>
<Button Content="1 px = 1 mm (0.001)" Command="{Binding SetPresetCommand}" CommandParameter="0.001" Margin="0,0,5,5"/>
</WrapPanel>
</StackPanel>
<!-- Información de ayuda -->
<Border Grid.Row="5"
Background="LightYellow"
BorderBrush="Orange"
BorderThickness="1"
Padding="10"
Margin="0,0,0,15">
<StackPanel>
<TextBlock Text="Información:" FontWeight="Bold" Margin="0,0,0,5"/>
<TextBlock TextWrapping="Wrap">
<Run Text="La escala define cuántos metros representa cada píxel en el canvas."/>
<LineBreak/>
<Run Text="• Valores menores = objetos más pequeños en pantalla"/>
<LineBreak/>
<Run Text="• Valores mayores = objetos más grandes en pantalla"/>
<LineBreak/>
<Run Text="• Ejemplo: 0.01 significa que 1 píxel = 1 centímetro"/>
<LineBreak/>
<Run Text="• Los cambios se aplican automáticamente después de 0.5 segundos"/>
</TextBlock>
</StackPanel>
</Border>
<!-- Botones -->
<StackPanel Grid.Row="6"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="Aplicar"
Command="{Binding ApplyCommand}"
Width="80"
Height="30"
Margin="0,0,10,0"/>
<Button Content="Aceptar"
Command="{Binding AcceptCommand}"
Width="80"
Height="30"
Margin="0,0,10,0"
IsDefault="True"/>
<Button Content="Cancelar"
Command="{Binding CancelCommand}"
Width="80"
Height="30"
IsCancel="True"/>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,146 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace CtrEditor.PopUps
{
public partial class ScaleConfigWindow : Window
{
public ScaleConfigWindow(float currentScale, MainViewModel mainViewModel)
{
InitializeComponent();
DataContext = new ScaleConfigViewModel(currentScale, this, mainViewModel);
}
public float NewScale => ((ScaleConfigViewModel)DataContext).NewScale;
}
public partial class ScaleConfigViewModel : ObservableObject
{
private readonly ScaleConfigWindow _window;
private readonly MainViewModel _mainViewModel;
private float _originalScale;
private DispatcherTimer _autoApplyTimer;
[ObservableProperty]
private float currentScale;
[ObservableProperty]
private float newScale;
public ICommand SetPresetCommand { get; }
public ICommand AcceptCommand { get; }
public ICommand CancelCommand { get; }
public ICommand ApplyCommand { get; }
public ScaleConfigViewModel(float currentScale, ScaleConfigWindow window, MainViewModel mainViewModel)
{
_window = window;
_mainViewModel = mainViewModel;
_originalScale = currentScale;
CurrentScale = currentScale;
NewScale = currentScale;
// Inicializar timer para aplicación automática
_autoApplyTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(500) // 500ms de delay
};
_autoApplyTimer.Tick += (s, e) =>
{
_autoApplyTimer.Stop();
if (NewScale > 0 && NewScale != CurrentScale)
{
Apply();
}
};
SetPresetCommand = new RelayCommand<object>(SetPreset);
AcceptCommand = new RelayCommand(Accept, CanAccept);
CancelCommand = new RelayCommand(Cancel);
ApplyCommand = new RelayCommand(Apply, CanAccept);
}
private void SetPreset(object parameter)
{
if (parameter != null && float.TryParse(parameter.ToString(),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out float presetValue))
{
NewScale = presetValue;
}
}
private bool CanAccept()
{
return NewScale > 0; // Solo verificar que sea positivo
}
private void Accept()
{
if (NewScale <= 0)
{
MessageBox.Show("La escala debe ser un valor positivo mayor que cero.",
"Error de Validación",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
}
// Aplicar la escala una última vez antes de cerrar
Apply();
// Cerrar la ventana
_window.Close();
}
private void Apply()
{
if (NewScale <= 0)
{
MessageBox.Show("La escala debe ser un valor positivo mayor que cero.",
"Error de Validación",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
}
// Aplicar la escala inmediatamente
_mainViewModel.ApplyScale(NewScale);
// Actualizar la escala actual para reflejar el cambio
CurrentScale = NewScale;
}
private void Cancel()
{
// Restaurar la escala original
_mainViewModel.ApplyScale(_originalScale);
// Cerrar la ventana
_window.Close();
}
partial void OnNewScaleChanged(float value)
{
// Forzar la re-evaluación del comando Accept cuando el valor cambia
CommandManager.InvalidateRequerySuggested();
// Reiniciar el timer para aplicación automática (verificar que esté inicializado)
if (_autoApplyTimer != null)
{
_autoApplyTimer.Stop();
if (value > 0 && value != CurrentScale)
{
_autoApplyTimer.Start();
}
}
}
}
}

View File

@ -5,6 +5,7 @@ using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CtrEditor.ObjetosSim;
using CtrEditor.Models;
namespace CtrEditor.PopUps
{
@ -13,10 +14,10 @@ namespace CtrEditor.PopUps
/// </summary>
public partial class TagEditorWindow : Window
{
public TagEditorWindow(osBase objeto, IEnumerable<osBase> todosLosObjetos)
public TagEditorWindow(osBase objeto, IEnumerable<osBase> todosLosObjetos, Dictionary<string, ImageData> imageDataDictionary = null)
{
InitializeComponent();
DataContext = new TagEditorViewModel(objeto, todosLosObjetos, this);
DataContext = new TagEditorViewModel(objeto, todosLosObjetos, this, imageDataDictionary);
}
private void TxtNuevaEtiqueta_KeyDown(object sender, KeyEventArgs e)
@ -46,6 +47,7 @@ namespace CtrEditor.PopUps
{
private readonly osBase _objeto;
private readonly IEnumerable<osBase> _todosLosObjetos;
private readonly Dictionary<string, ImageData> _imageDataDictionary;
private readonly TagEditorWindow _window;
private readonly List<string> _etiquetasOriginales;
@ -64,10 +66,11 @@ namespace CtrEditor.PopUps
public ICommand AplicarCommand { get; }
public ICommand CancelarCommand { get; }
public TagEditorViewModel(osBase objeto, IEnumerable<osBase> todosLosObjetos, TagEditorWindow window)
public TagEditorViewModel(osBase objeto, IEnumerable<osBase> todosLosObjetos, TagEditorWindow window, Dictionary<string, ImageData> imageDataDictionary = null)
{
_objeto = objeto;
_todosLosObjetos = todosLosObjetos;
_imageDataDictionary = imageDataDictionary;
_window = window;
// Guardar las etiquetas originales para poder cancelar
@ -111,9 +114,22 @@ namespace CtrEditor.PopUps
var todasLasEtiquetas = _todosLosObjetos
.SelectMany(obj => obj.ListaEtiquetas)
.Distinct()
.OrderBy(tag => tag)
.ToList();
// Agregar etiquetas de las imágenes si está disponible el diccionario
if (_imageDataDictionary != null)
{
var etiquetasImagenes = _imageDataDictionary.Values
.SelectMany(imageData => imageData.ListaEtiquetas)
.Distinct();
todasLasEtiquetas.AddRange(etiquetasImagenes);
todasLasEtiquetas = todasLasEtiquetas.Distinct().ToList();
}
// Ordenar todas las etiquetas
todasLasEtiquetas = todasLasEtiquetas.OrderBy(tag => tag).ToList();
// Mostrar solo las que no están en el objeto actual
var etiquetasObjetoActual = _objeto.ListaEtiquetas;
foreach (var tag in todasLasEtiquetas.Except(etiquetasObjetoActual))

View File

@ -50,7 +50,7 @@ namespace CtrEditor.Serialization
obj.RestaurarDesdeSnapshotGlobal();
}
// Prepara para serialización
// Prepara para serializaci<EFBFBD>n
obj.SalvarDatosNoSerializables();
// Separa objetos globales y locales
@ -64,13 +64,14 @@ namespace CtrEditor.Serialization
}
}
// Paso 2: Guardar datos de la página actual
// Paso 2: Guardar datos de la p<EFBFBD>gina actual
var dataToSerialize = new SimulationData
{
ObjetosSimulables = objetosSimulables.Count > 0 ? objetosSimulables : null,
UnitConverter = PixelToMeter.Instance.calc,
PLC_ConnectionData = _mainViewModel.PLCViewModel,
DatosLocalesObjetos = datosLocalesObjetos.Count > 0 ? datosLocalesObjetos : null
DatosLocalesObjetos = datosLocalesObjetos.Count > 0 ? datosLocalesObjetos : null,
ImageDataDictionary = _mainViewModel._imageDataDictionary.Count > 0 ? _mainViewModel._imageDataDictionary : null
};
var path = _datosDeTrabajo.ObtenerPathImagenConExtension(selectedImage, ".json");
@ -112,11 +113,11 @@ 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<EFBFBD>gina actual
var simulationData = LoadCurrentPageState(selectedImage, settings);
// Paso 3: Crear snapshots de los objetos globales
// Nota: No filtramos por Enable_Local_Data aquí ya que se establecerá después
// Nota: No filtramos por Enable_Local_Data aqu<EFBFBD> ya que se establecer<65> despu<70>s
foreach (var obj in _mainViewModel.ObjetosSimulables)
{
if (obj.Enable_On_All_Pages)
@ -134,7 +135,7 @@ namespace CtrEditor.Serialization
obj.Enable_Local_Data = false;
}
// Luego aplicar los datos locales, esto activará Enable_Local_Data automáticamente
// Luego aplicar los datos locales, esto activar<EFBFBD> Enable_Local_Data autom<6F>ticamente
foreach (var datosLocales in simulationData.DatosLocalesObjetos)
{
var objetoGlobal = _mainViewModel.ObjetosSimulables.FirstOrDefault(
@ -186,7 +187,31 @@ namespace CtrEditor.Serialization
else
_mainViewModel.PLCViewModel = new PLCViewModel();
PixelToMeter.Instance.calc = simulationData.UnitConverter;
// Solo sobrescribir el UnitConverter si existe uno guardado
if (simulationData.UnitConverter != null)
PixelToMeter.Instance.calc = simulationData.UnitConverter;
// Cargar datos de imágenes
if (simulationData.ImageDataDictionary != null)
{
_mainViewModel._imageDataDictionary.Clear();
foreach (var imageDataEntry in simulationData.ImageDataDictionary)
{
_mainViewModel._imageDataDictionary[imageDataEntry.Key] = imageDataEntry.Value;
}
}
// Compatibilidad con versiones anteriores (ImageCustomNames)
#pragma warning disable CS0618 // Type or member is obsolete
if (simulationData.ImageCustomNames != null)
{
foreach (var customName in simulationData.ImageCustomNames)
{
var imageData = _mainViewModel.GetOrCreateImageData(customName.Key);
imageData.CustomName = customName.Value;
}
}
#pragma warning restore CS0618
}
}
return simulationData;

View File

@ -314,7 +314,7 @@ namespace CtrEditor
{
if (values.Length == 2 && values[0] is float value1 && values[1] is float value2)
{
return (double) (value1 + value2);
return (double)(value1 + value2);
}
return DependencyProperty.UnsetValue;
}
@ -435,7 +435,14 @@ namespace CtrEditor
{
// Instancia privada estática, parte del patrón Singleton
private static PixelToMeter? _instance;
public UnitConverter calc = new UnitConverter(0.01f);
public UnitConverter calc;
// Constructor privado para el patrón Singleton
private PixelToMeter()
{
// Solo inicializar con valor por defecto si no se ha configurado antes
calc = new UnitConverter(0.01f);
}
// Propiedad pública estática para acceder a la instancia
public static PixelToMeter Instance