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.

This commit is contained in:
Miguel 2025-06-18 19:54:51 +02:00
parent ca70f66ff1
commit b48dbeb76e
9 changed files with 357 additions and 27 deletions

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

@ -1236,6 +1236,62 @@ namespace CtrEditor
OnPropertyChanged(nameof(SelectedItemOsList));
}
// Método para configurar la escala desde el menú contextual
public void ConfigureScale()
{
var currentScale = PixelToMeter.Instance.calc.Scale;
var scaleWindow = new PopUps.ScaleConfigWindow(currentScale);
scaleWindow.Owner = MainWindow;
if (scaleWindow.ShowDialog() == true)
{
var newScale = scaleWindow.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();

View File

@ -328,7 +328,7 @@ namespace CtrEditor
{
// Siempre trabajar con selección única para las propiedades
CargarPropiedadesosDatos(selectedObject);
_objectManager.SelectObject(selectedObject);
}
else if (e.RemovedItems.Count > 0 && e.AddedItems.Count == 0)
@ -414,7 +414,7 @@ namespace CtrEditor
{
// Capturar estado antes de mover
_objectManager.CaptureUndoState();
// Mover todos los objetos primero
foreach (var obj in _objectManager.SelectedObjects)
{
@ -472,7 +472,7 @@ namespace CtrEditor
if (DataContext is MainViewModel viewModel)
{
viewModel.CargarPropiedadesosDatos(selectedObject, PanelEdicion, Resources);
// If no object is selected, make sure to clear the properties panel
if (selectedObject == null)
{
@ -712,12 +712,19 @@ namespace CtrEditor
contextMenu.Items.Add(lockSubmenu);
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();
var undoInfoItem = new MenuItem
{
var undoInfoItem = new MenuItem
{
Header = $"Undo: {undoHistoryCount}/3 estados ({(canUndo ? "Ctrl+Z disponible" : "No disponible")})",
IsEnabled = false,
FontStyle = FontStyles.Italic
@ -820,7 +827,7 @@ namespace CtrEditor
{
// Usar la misma lógica que DuplicarObjeto pero solo para crear copias para serialización
var objectsCopy = new List<osBase>();
foreach (var originalObj in _objectManager.SelectedObjects)
{
try
@ -854,7 +861,7 @@ namespace CtrEditor
string jsonString = JsonConvert.SerializeObject(objectsCopy, settings);
Clipboard.SetText(jsonString);
Console.WriteLine($"Copiados {objectsCopy.Count} objeto(s) al portapapeles");
}
}
@ -899,7 +906,7 @@ namespace CtrEditor
try
{
string jsonString = Clipboard.GetText();
// Validación básica del JSON
if (string.IsNullOrWhiteSpace(jsonString) || (!jsonString.TrimStart().StartsWith("[") && !jsonString.TrimStart().StartsWith("{")))
{
@ -984,11 +991,11 @@ namespace CtrEditor
{
// Generar nuevo ID y actualizar nombre (igual que en DuplicarObjeto)
obj.Id.ObtenerNuevaID();
string nombre = Regex.IsMatch(obj.Nombre, @"_\d+$")
? Regex.Replace(obj.Nombre, @"_\d+$", $"_{obj.Id.Value}")
: obj.Nombre + "_" + obj.Id.Value;
obj.Nombre = nombre;
}
else
@ -1005,7 +1012,7 @@ namespace CtrEditor
viewModel.ObjetosSimulables.Add(obj);
viewModel.CrearUserControlDesdeObjetoSimulable(obj);
viewModel.HasUnsavedChanges = true;
newlyPastedObjects.Add(obj);
}
catch (Exception ex)
@ -1017,10 +1024,10 @@ namespace CtrEditor
// Usar la misma lógica de selección que DuplicarUserControl
// Limpiar selección antes de seleccionar los nuevos objetos
_objectManager.ClearSelection();
// Forzar actualización completa del layout
ImagenEnTrabajoCanvas.UpdateLayout();
// Usar dispatcher con la misma prioridad que DuplicarUserControl
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() =>
{
@ -1031,7 +1038,7 @@ namespace CtrEditor
{
double left = Canvas.GetLeft(newObj.VisualRepresentation);
double top = Canvas.GetTop(newObj.VisualRepresentation);
// Solo añadir a selección si el objeto tiene coordenadas válidas
if (!double.IsNaN(left) && !double.IsNaN(top) && !double.IsInfinity(left) && !double.IsInfinity(top))
{
@ -1039,10 +1046,10 @@ namespace CtrEditor
}
}
}
// Forzar otra actualización del layout antes de actualizar visuales
ImagenEnTrabajoCanvas.UpdateLayout();
// Actualizar SelectedItemOsList si hay objetos pegados
if (newlyPastedObjects.Count > 0)
{
@ -1051,13 +1058,13 @@ namespace CtrEditor
vm.SelectedItemOsList = newlyPastedObjects.LastOrDefault();
}
}
// Actualizar visuales de selección
_objectManager.UpdateSelectionVisuals();
Console.WriteLine($"Pegados y seleccionados {newlyPastedObjects.Count} objeto(s)");
}));
Console.WriteLine($"Pegados {newlyPastedObjects.Count} objeto(s) desde el portapapeles");
}
}
@ -1075,9 +1082,9 @@ namespace CtrEditor
try
{
string jsonString = Clipboard.GetText();
// Validación básica del JSON
if (string.IsNullOrWhiteSpace(jsonString) ||
if (string.IsNullOrWhiteSpace(jsonString) ||
(!jsonString.TrimStart().StartsWith("[") && !jsonString.TrimStart().StartsWith("{")))
{
return false;

View File

@ -1195,6 +1195,23 @@ 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
OnPropertyChanged(nameof(Left));
OnPropertyChanged(nameof(Top));
// 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)

View File

@ -0,0 +1,104 @@
<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"/>
</TextBlock>
</StackPanel>
</Border>
<!-- Botones -->
<StackPanel Grid.Row="6"
Orientation="Horizontal"
HorizontalAlignment="Right">
<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,92 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace CtrEditor.PopUps
{
public partial class ScaleConfigWindow : Window
{
public ScaleConfigWindow(float currentScale)
{
InitializeComponent();
DataContext = new ScaleConfigViewModel(currentScale, this);
}
public float NewScale => ((ScaleConfigViewModel)DataContext).NewScale;
}
public partial class ScaleConfigViewModel : ObservableObject
{
private readonly ScaleConfigWindow _window;
[ObservableProperty]
private float currentScale;
[ObservableProperty]
private float newScale;
public ICommand SetPresetCommand { get; }
public ICommand AcceptCommand { get; }
public ICommand CancelCommand { get; }
public ScaleConfigViewModel(float currentScale, ScaleConfigWindow window)
{
_window = window;
CurrentScale = currentScale;
NewScale = currentScale;
SetPresetCommand = new RelayCommand<object>(SetPreset);
AcceptCommand = new RelayCommand(Accept, CanAccept);
CancelCommand = new RelayCommand(Cancel);
}
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;
}
// Debugging: mostrar mensaje para confirmar que se ejecuta
// MessageBox.Show($"Accept ejecutado con escala: {NewScale}", "Debug", MessageBoxButton.OK, MessageBoxImage.Information);
_window.DialogResult = true;
// No necesitamos llamar Close() ya que establecer DialogResult automáticamente cierra la ventana
}
private void Cancel()
{
_window.DialogResult = false;
_window.Close();
}
partial void OnNewScaleChanged(float value)
{
// Forzar la re-evaluación del comando Accept cuando el valor cambia
CommandManager.InvalidateRequerySuggested();
}
}
}

View File

@ -187,7 +187,9 @@ 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)

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