Compare commits
9 Commits
380bc14b69
...
99248e9112
Author | SHA1 | Date |
---|---|---|
|
99248e9112 | |
|
f85c707cfc | |
|
88e6de77cb | |
|
94b11cf068 | |
|
20467c88ae | |
|
e935efb0cb | |
|
16f5131803 | |
|
883620b69d | |
|
9c4e87be9b |
|
@ -447,6 +447,9 @@ namespace CtrEditor
|
||||||
{
|
{
|
||||||
OnVisFilterChanged(MainWindow.VisFilter.FilterViewModel);
|
OnVisFilterChanged(MainWindow.VisFilter.FilterViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limpiar historial de undo al cargar un estado desde archivo
|
||||||
|
MainWindow?.ClearUndoHistory();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,6 +458,9 @@ namespace CtrEditor
|
||||||
{
|
{
|
||||||
// Suponiendo que "SelectedImage" es una propiedad que al establecerse dispara "ImageSelected"
|
// Suponiendo que "SelectedImage" es una propiedad que al establecerse dispara "ImageSelected"
|
||||||
directorioTrabajo = EstadoPersistente.Instance.directorio;
|
directorioTrabajo = EstadoPersistente.Instance.directorio;
|
||||||
|
|
||||||
|
// Limpiar historial de undo al cargar datos iniciales
|
||||||
|
MainWindow?.ClearUndoHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear un nuevo Objeto
|
// Crear un nuevo Objeto
|
||||||
|
@ -794,6 +800,9 @@ namespace CtrEditor
|
||||||
|
|
||||||
// Restaurar los rectángulos de selección si hay objetos seleccionados
|
// Restaurar los rectángulos de selección si hay objetos seleccionados
|
||||||
_objectManager.UpdateSelectionVisuals();
|
_objectManager.UpdateSelectionVisuals();
|
||||||
|
|
||||||
|
// Limpiar historial de undo al detener la simulación
|
||||||
|
MainWindow?.ClearUndoHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -869,6 +878,8 @@ namespace CtrEditor
|
||||||
foreach (var objetoSimulable in ObjetosSimulables)
|
foreach (var objetoSimulable in ObjetosSimulables)
|
||||||
objetoSimulable.SetPLC(null);
|
objetoSimulable.SetPLC(null);
|
||||||
|
|
||||||
|
// Limpiar historial de undo al desconectar del PLC
|
||||||
|
MainWindow?.ClearUndoHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<osBase> objetosSimulablesLlamados = new List<osBase>();
|
private List<osBase> objetosSimulablesLlamados = new List<osBase>();
|
||||||
|
|
|
@ -11,6 +11,11 @@ using System.Windows.Threading;
|
||||||
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
|
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
|
||||||
using UserControl = System.Windows.Controls.UserControl;
|
using UserControl = System.Windows.Controls.UserControl;
|
||||||
using CtrEditor.Controls; // Add this using statement
|
using CtrEditor.Controls; // Add this using statement
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace CtrEditor
|
namespace CtrEditor
|
||||||
{
|
{
|
||||||
|
@ -407,6 +412,9 @@ namespace CtrEditor
|
||||||
|
|
||||||
private void MoveSelectedObjects(float deltaX, float deltaY)
|
private void MoveSelectedObjects(float deltaX, float deltaY)
|
||||||
{
|
{
|
||||||
|
// Capturar estado antes de mover
|
||||||
|
_objectManager.CaptureUndoState();
|
||||||
|
|
||||||
// Mover todos los objetos primero
|
// Mover todos los objetos primero
|
||||||
foreach (var obj in _objectManager.SelectedObjects)
|
foreach (var obj in _objectManager.SelectedObjects)
|
||||||
{
|
{
|
||||||
|
@ -426,6 +434,23 @@ namespace CtrEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LockSelectedObjects(bool lockState)
|
||||||
|
{
|
||||||
|
foreach (var selectedObject in _objectManager.SelectedObjects)
|
||||||
|
{
|
||||||
|
selectedObject.Lock_movement = lockState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar los visuales de selección para reflejar el cambio de estado
|
||||||
|
_objectManager.UpdateSelectionVisuals();
|
||||||
|
|
||||||
|
// Marcar como cambios sin guardar
|
||||||
|
if (DataContext is MainViewModel viewModel)
|
||||||
|
{
|
||||||
|
viewModel.HasUnsavedChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Canvas_MouseEnter(object sender, MouseEventArgs e)
|
private void Canvas_MouseEnter(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.OriginalSource == ImagenEnTrabajoCanvas)
|
if (e.OriginalSource == ImagenEnTrabajoCanvas)
|
||||||
|
@ -498,6 +523,11 @@ namespace CtrEditor
|
||||||
debugWindow.Show();
|
debugWindow.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClearUndoHistory()
|
||||||
|
{
|
||||||
|
_objectManager.ClearUndoHistory();
|
||||||
|
}
|
||||||
|
|
||||||
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
// Aceptar el evento si viene del canvas o de la imagen de fondo
|
// Aceptar el evento si viene del canvas o de la imagen de fondo
|
||||||
|
@ -628,8 +658,72 @@ namespace CtrEditor
|
||||||
contextMenu.Items.Add(editPropertiesItem);
|
contextMenu.Items.Add(editPropertiesItem);
|
||||||
|
|
||||||
contextMenu.Items.Add(new Separator());
|
contextMenu.Items.Add(new Separator());
|
||||||
|
|
||||||
|
// Agregar opción de copiar
|
||||||
|
var copyMenuItem = new MenuItem { Header = "Copiar (Ctrl+C)" };
|
||||||
|
copyMenuItem.Click += (s, e) => CopySelectedObjectsAsJson();
|
||||||
|
contextMenu.Items.Add(copyMenuItem);
|
||||||
|
|
||||||
|
contextMenu.Items.Add(new Separator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agregar opciones de pegado
|
||||||
|
if (CanPasteFromClipboard())
|
||||||
|
{
|
||||||
|
var pasteMenuItem = new MenuItem { Header = "Pegar (Ctrl+V)" };
|
||||||
|
pasteMenuItem.Click += (s, e) => PasteObjectsFromJson();
|
||||||
|
contextMenu.Items.Add(pasteMenuItem);
|
||||||
|
|
||||||
|
var pasteReplaceMenuItem = new MenuItem { Header = "Pegar con Reemplazo" };
|
||||||
|
pasteReplaceMenuItem.Click += (s, e) => PasteObjectsFromJson(true);
|
||||||
|
contextMenu.Items.Add(pasteReplaceMenuItem);
|
||||||
|
|
||||||
|
contextMenu.Items.Add(new Separator());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opciones de bloqueo/desbloqueo - mostrar siempre que haya objetos seleccionados
|
||||||
|
if (_objectManager.SelectedObjects.Count > 0)
|
||||||
|
{
|
||||||
|
var lockSubmenu = new MenuItem { Header = "Bloqueo" };
|
||||||
|
|
||||||
|
// Verificar el estado actual de los objetos
|
||||||
|
int totalObjects = _objectManager.SelectedObjects.Count;
|
||||||
|
int lockedObjects = _objectManager.SelectedObjects.Count(obj => obj.Lock_movement);
|
||||||
|
int unlockedObjects = totalObjects - lockedObjects;
|
||||||
|
|
||||||
|
var lockAllItem = new MenuItem { Header = $"Bloquear Objetos ({unlockedObjects} desbloqueados)" };
|
||||||
|
lockAllItem.Click += (s, e) => LockSelectedObjects(true);
|
||||||
|
lockAllItem.IsEnabled = unlockedObjects > 0;
|
||||||
|
lockSubmenu.Items.Add(lockAllItem);
|
||||||
|
|
||||||
|
var unlockAllItem = new MenuItem { Header = $"Desbloquear Objetos ({lockedObjects} bloqueados)" };
|
||||||
|
unlockAllItem.Click += (s, e) => LockSelectedObjects(false);
|
||||||
|
unlockAllItem.IsEnabled = lockedObjects > 0;
|
||||||
|
lockSubmenu.Items.Add(unlockAllItem);
|
||||||
|
|
||||||
|
// Mostrar estado actual si hay mezcla
|
||||||
|
if (lockedObjects > 0 && unlockedObjects > 0)
|
||||||
|
{
|
||||||
|
lockSubmenu.Items.Add(new Separator());
|
||||||
|
var statusItem = new MenuItem { Header = $"Estado: {lockedObjects} bloqueados, {unlockedObjects} libres", IsEnabled = false };
|
||||||
|
lockSubmenu.Items.Add(statusItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
contextMenu.Items.Add(lockSubmenu);
|
||||||
|
contextMenu.Items.Add(new Separator());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar información del sistema de undo
|
||||||
|
var undoHistoryCount = _objectManager.GetUndoHistoryCount();
|
||||||
|
var canUndo = _objectManager.CanUndo();
|
||||||
|
var undoInfoItem = new MenuItem
|
||||||
|
{
|
||||||
|
Header = $"Undo: {undoHistoryCount}/3 estados ({(canUndo ? "Ctrl+Z disponible" : "No disponible")})",
|
||||||
|
IsEnabled = false,
|
||||||
|
FontStyle = FontStyles.Italic
|
||||||
|
};
|
||||||
|
contextMenu.Items.Add(undoInfoItem);
|
||||||
|
|
||||||
contextMenu.IsOpen = true;
|
contextMenu.IsOpen = true;
|
||||||
contextMenu.PlacementTarget = ImagenEnTrabajoCanvas;
|
contextMenu.PlacementTarget = ImagenEnTrabajoCanvas;
|
||||||
contextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
|
contextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
|
||||||
|
@ -672,6 +766,21 @@ namespace CtrEditor
|
||||||
_objectManager.RemoveResizeRectangles();
|
_objectManager.RemoveResizeRectangles();
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
else if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.C)
|
||||||
|
{
|
||||||
|
CopySelectedObjectsAsJson();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.V)
|
||||||
|
{
|
||||||
|
PasteObjectsFromJson();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
else if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.Z)
|
||||||
|
{
|
||||||
|
_objectManager.PerformUndo();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
else if (_objectManager.SelectedObjects.Any())
|
else if (_objectManager.SelectedObjects.Any())
|
||||||
{
|
{
|
||||||
const float moveDistance = 0.01f;
|
const float moveDistance = 0.01f;
|
||||||
|
@ -699,6 +808,314 @@ namespace CtrEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
public Image ImagenDeFondo => imagenDeFondo;
|
public Image ImagenDeFondo => imagenDeFondo;
|
||||||
|
|
||||||
|
private void CopySelectedObjectsAsJson()
|
||||||
|
{
|
||||||
|
if (!_objectManager.SelectedObjects.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (DataContext is MainViewModel viewModel)
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
// Reutilizar la lógica existente de DuplicarObjeto sin añadir a ObjetosSimulables
|
||||||
|
var tempCopy = CreateCopyUsingExistingLogic(viewModel, originalObj);
|
||||||
|
if (tempCopy != null)
|
||||||
|
{
|
||||||
|
objectsCopy.Add(tempCopy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error al crear copia de {originalObj.Nombre}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectsCopy.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show("No se pudieron copiar los objetos seleccionados.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serializar usando las mismas configuraciones que DuplicarObjeto
|
||||||
|
var settings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
TypeNameHandling = TypeNameHandling.All // Igual que en DuplicarObjeto
|
||||||
|
};
|
||||||
|
|
||||||
|
string jsonString = JsonConvert.SerializeObject(objectsCopy, settings);
|
||||||
|
Clipboard.SetText(jsonString);
|
||||||
|
|
||||||
|
Console.WriteLine($"Copiados {objectsCopy.Count} objeto(s) al portapapeles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Error al copiar objetos: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private osBase CreateCopyUsingExistingLogic(MainViewModel viewModel, osBase original)
|
||||||
|
{
|
||||||
|
// Usar exactamente la misma lógica que DuplicarObjeto pero sin añadir a la colección
|
||||||
|
osBase copy = null;
|
||||||
|
|
||||||
|
original.SalvarDatosNoSerializables();
|
||||||
|
|
||||||
|
var settings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
TypeNameHandling = TypeNameHandling.All // Igual que en DuplicarObjeto
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var serializedData = JsonConvert.SerializeObject(original, settings);
|
||||||
|
copy = JsonConvert.DeserializeObject<osBase>(serializedData, settings);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
original.RestaurarDatosNoSerializables();
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PasteObjectsFromJson(bool replaceExistingIds = false)
|
||||||
|
{
|
||||||
|
if (!Clipboard.ContainsText())
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string jsonString = Clipboard.GetText();
|
||||||
|
|
||||||
|
// Validación básica del JSON
|
||||||
|
if (string.IsNullOrWhiteSpace(jsonString) || (!jsonString.TrimStart().StartsWith("[") && !jsonString.TrimStart().StartsWith("{")))
|
||||||
|
{
|
||||||
|
MessageBox.Show("El contenido del portapapeles no parece ser un JSON válido.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear configuración de deserialización
|
||||||
|
var settings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
TypeNameHandling = TypeNameHandling.Auto,
|
||||||
|
ObjectCreationHandling = ObjectCreationHandling.Replace,
|
||||||
|
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
|
||||||
|
};
|
||||||
|
|
||||||
|
List<osBase> pastedObjects = null;
|
||||||
|
|
||||||
|
// Intentar deserializar como lista de objetos
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pastedObjects = JsonConvert.DeserializeObject<List<osBase>>(jsonString, settings);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Si falla, intentar como objeto individual
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var singleObject = JsonConvert.DeserializeObject<osBase>(jsonString, settings);
|
||||||
|
if (singleObject != null)
|
||||||
|
{
|
||||||
|
pastedObjects = new List<osBase> { singleObject };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
MessageBox.Show("No se pudo deserializar el contenido del portapapeles como objetos válidos.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pastedObjects == null || !pastedObjects.Any())
|
||||||
|
{
|
||||||
|
MessageBox.Show("No se encontraron objetos válidos en el portapapeles.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener posición del mouse en el canvas
|
||||||
|
var mousePosition = Mouse.GetPosition(ImagenEnTrabajoCanvas);
|
||||||
|
var mousePositionMeters = (
|
||||||
|
PixelToMeter.Instance.calc.PixelsToMeters((float)mousePosition.X),
|
||||||
|
PixelToMeter.Instance.calc.PixelsToMeters((float)mousePosition.Y)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calcular el centro de los objetos pegados para posicionarlos en el mouse
|
||||||
|
var centerX = pastedObjects.Average(obj => obj.Left);
|
||||||
|
var centerY = pastedObjects.Average(obj => obj.Top);
|
||||||
|
var offsetX = mousePositionMeters.Item1 - centerX;
|
||||||
|
var offsetY = mousePositionMeters.Item2 - centerY;
|
||||||
|
|
||||||
|
if (DataContext is MainViewModel viewModel)
|
||||||
|
{
|
||||||
|
viewModel.StopSimulation();
|
||||||
|
viewModel.DisconnectPLC();
|
||||||
|
|
||||||
|
// Limpiar selección actual
|
||||||
|
_objectManager.ClearSelection();
|
||||||
|
|
||||||
|
var newlyPastedObjects = new List<osBase>();
|
||||||
|
|
||||||
|
foreach (var obj in pastedObjects)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Aplicar offset de posición
|
||||||
|
obj.Left += offsetX;
|
||||||
|
obj.Top += offsetY;
|
||||||
|
|
||||||
|
// Manejar IDs usando la misma lógica que DuplicarObjeto
|
||||||
|
if (!replaceExistingIds)
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
// Reemplazar objeto existente con mismo ID
|
||||||
|
var existingObj = viewModel.ObjetosSimulables.FirstOrDefault(o => o.Id.Value == obj.Id.Value);
|
||||||
|
if (existingObj != null)
|
||||||
|
{
|
||||||
|
viewModel.RemoverObjetoSimulable(existingObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.CheckData();
|
||||||
|
viewModel.ObjetosSimulables.Add(obj);
|
||||||
|
viewModel.CrearUserControlDesdeObjetoSimulable(obj);
|
||||||
|
viewModel.HasUnsavedChanges = true;
|
||||||
|
|
||||||
|
newlyPastedObjects.Add(obj);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Error al procesar objeto {obj.Nombre}: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(() =>
|
||||||
|
{
|
||||||
|
// Seleccionar todos los objetos pegados
|
||||||
|
foreach (var newObj in newlyPastedObjects)
|
||||||
|
{
|
||||||
|
if (newObj.VisualRepresentation != null)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
_objectManager.SelectObject(newObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forzar otra actualización del layout antes de actualizar visuales
|
||||||
|
ImagenEnTrabajoCanvas.UpdateLayout();
|
||||||
|
|
||||||
|
// Actualizar SelectedItemOsList si hay objetos pegados
|
||||||
|
if (newlyPastedObjects.Count > 0)
|
||||||
|
{
|
||||||
|
if (DataContext is MainViewModel vm)
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Error al pegar objetos: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanPasteFromClipboard()
|
||||||
|
{
|
||||||
|
if (!Clipboard.ContainsText())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string jsonString = Clipboard.GetText();
|
||||||
|
|
||||||
|
// Validación básica del JSON
|
||||||
|
if (string.IsNullOrWhiteSpace(jsonString) ||
|
||||||
|
(!jsonString.TrimStart().StartsWith("[") && !jsonString.TrimStart().StartsWith("{")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intentar una deserialización rápida para validar
|
||||||
|
var settings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
TypeNameHandling = TypeNameHandling.Auto,
|
||||||
|
ObjectCreationHandling = ObjectCreationHandling.Replace,
|
||||||
|
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intentar deserializar como lista de objetos
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pastedObjects = JsonConvert.DeserializeObject<List<osBase>>(jsonString, settings);
|
||||||
|
return pastedObjects != null && pastedObjects.Any();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Si falla como lista, intentar como objeto individual
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var singleObject = JsonConvert.DeserializeObject<osBase>(jsonString, settings);
|
||||||
|
return singleObject != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FloatValidationRule : ValidationRule
|
public class FloatValidationRule : ValidationRule
|
||||||
|
|
|
@ -10,6 +10,58 @@ using Color = System.Windows.Media.Color;
|
||||||
|
|
||||||
namespace CtrEditor
|
namespace CtrEditor
|
||||||
{
|
{
|
||||||
|
// Clase para almacenar el estado básico de un objeto para undo
|
||||||
|
public class ObjectState
|
||||||
|
{
|
||||||
|
public int ObjectId { get; set; }
|
||||||
|
public float Left { get; set; }
|
||||||
|
public float Top { get; set; }
|
||||||
|
public float Angle { get; set; }
|
||||||
|
public float Width { get; set; }
|
||||||
|
public float Height { get; set; }
|
||||||
|
|
||||||
|
public ObjectState(osBase obj)
|
||||||
|
{
|
||||||
|
ObjectId = obj.Id.Value;
|
||||||
|
Left = obj.Left;
|
||||||
|
Top = obj.Top;
|
||||||
|
Angle = obj.Angulo;
|
||||||
|
Width = obj.Ancho;
|
||||||
|
Height = obj.Alto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectState(ObjectState other)
|
||||||
|
{
|
||||||
|
ObjectId = other.ObjectId;
|
||||||
|
Left = other.Left;
|
||||||
|
Top = other.Top;
|
||||||
|
Angle = other.Angle;
|
||||||
|
Width = other.Width;
|
||||||
|
Height = other.Height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clase para almacenar un estado completo del workspace
|
||||||
|
public class UndoState
|
||||||
|
{
|
||||||
|
public List<ObjectState> ObjectStates { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
|
||||||
|
public UndoState()
|
||||||
|
{
|
||||||
|
ObjectStates = new List<ObjectState>();
|
||||||
|
Timestamp = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UndoState(IEnumerable<osBase> objects) : this()
|
||||||
|
{
|
||||||
|
foreach (var obj in objects)
|
||||||
|
{
|
||||||
|
ObjectStates.Add(new ObjectState(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum AlignmentType
|
public enum AlignmentType
|
||||||
{
|
{
|
||||||
Left,
|
Left,
|
||||||
|
@ -27,6 +79,19 @@ namespace CtrEditor
|
||||||
JoinVertically
|
JoinVertically
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestor de manipulación de objetos que maneja la selección, movimiento, rotación y redimensionamiento de objetos.
|
||||||
|
///
|
||||||
|
/// MEJORAS PARA OBJETOS ROTADOS:
|
||||||
|
/// - HandleResizeForObject: Aplica transformaciones matemáticas para convertir los cambios del mouse
|
||||||
|
/// desde coordenadas del canvas a coordenadas locales del objeto rotado.
|
||||||
|
/// - DetermineHandleMode: Adapta el comportamiento de los handles según la rotación del objeto.
|
||||||
|
/// - GetAdaptiveCursors: Cambia los cursores de los handles para reflejar la orientación actual del objeto.
|
||||||
|
///
|
||||||
|
/// Estas mejoras hacen que el redimensionamiento sea más intuitivo cuando los objetos están rotados,
|
||||||
|
/// ya que los handles responden de acuerdo a la orientación del objeto en lugar de las coordenadas
|
||||||
|
/// absolutas del canvas.
|
||||||
|
/// </summary>
|
||||||
public class ObjectManipulationManager
|
public class ObjectManipulationManager
|
||||||
{
|
{
|
||||||
private enum ResizeMode
|
private enum ResizeMode
|
||||||
|
@ -76,6 +141,11 @@ namespace CtrEditor
|
||||||
private bool _isRectangleSelectionActive;
|
private bool _isRectangleSelectionActive;
|
||||||
private bool _selectedObjectsAreVisible;
|
private bool _selectedObjectsAreVisible;
|
||||||
|
|
||||||
|
// Sistema de Undo
|
||||||
|
private Stack<UndoState> _undoHistory = new Stack<UndoState>();
|
||||||
|
private const int MaxUndoSteps = 3;
|
||||||
|
private bool _isApplyingUndo = false;
|
||||||
|
|
||||||
public bool IsRectangleSelectionActive
|
public bool IsRectangleSelectionActive
|
||||||
{
|
{
|
||||||
get => _isRectangleSelectionActive;
|
get => _isRectangleSelectionActive;
|
||||||
|
@ -314,19 +384,22 @@ namespace CtrEditor
|
||||||
// Calcular el offset para posicionar los manejadores fuera del rectángulo
|
// Calcular el offset para posicionar los manejadores fuera del rectángulo
|
||||||
double offset = rectSize / 2;
|
double offset = rectSize / 2;
|
||||||
|
|
||||||
|
// Determinar cursores adaptativos según la rotación del objeto
|
||||||
|
var cursors = GetAdaptiveCursors();
|
||||||
|
|
||||||
var positions = new List<(Point Position, string Tag, Cursor Cursor, Brush Stroke)>
|
var positions = new List<(Point Position, string Tag, Cursor Cursor, Brush Stroke)>
|
||||||
{
|
{
|
||||||
// Esquinas - mover hacia afuera del rectángulo
|
// Esquinas - mover hacia afuera del rectángulo
|
||||||
(new Point(rectBox.Left - offset, rectBox.Top - offset), "TopLeft", Cursors.Arrow, Brushes.Gray),
|
(new Point(rectBox.Left - offset, rectBox.Top - offset), "TopLeft", Cursors.Arrow, Brushes.Gray),
|
||||||
(new Point(rectBox.Right + offset, rectBox.Top - offset), "TopRight", rotationCursorRx, Brushes.Red),
|
(new Point(rectBox.Right + offset, rectBox.Top - offset), "TopRight", rotationCursorRx, Brushes.Red),
|
||||||
(new Point(rectBox.Left - offset, rectBox.Bottom + offset), "BottomLeft", rotationCursorSx, Brushes.DarkRed),
|
(new Point(rectBox.Left - offset, rectBox.Bottom + offset), "BottomLeft", rotationCursorSx, Brushes.DarkRed),
|
||||||
(new Point(rectBox.Right + offset, rectBox.Bottom + offset), "BottomRight", Cursors.SizeNWSE, Brushes.Blue),
|
(new Point(rectBox.Right + offset, rectBox.Bottom + offset), "BottomRight", cursors.ResizeBoth, Brushes.Blue),
|
||||||
|
|
||||||
// Centros de los bordes - mover hacia afuera del rectángulo
|
// Centros de los bordes - mover hacia afuera del rectángulo
|
||||||
(new Point(rectBox.Left + rectBox.Width / 2, rectBox.Top - offset), "TopCenter", Cursors.Arrow, Brushes.Gray),
|
(new Point(rectBox.Left + rectBox.Width / 2, rectBox.Top - offset), "TopCenter", Cursors.Arrow, Brushes.Gray),
|
||||||
(new Point(rectBox.Left + rectBox.Width / 2, rectBox.Bottom + offset), "BottomCenter", Cursors.SizeNS, Brushes.Blue),
|
(new Point(rectBox.Left + rectBox.Width / 2, rectBox.Bottom + offset), "BottomCenter", cursors.ResizeVertical, Brushes.Blue),
|
||||||
(new Point(rectBox.Left - offset, rectBox.Top + rectBox.Height / 2), "CenterLeft", rotationCursorRx, Brushes.Red),
|
(new Point(rectBox.Left - offset, rectBox.Top + rectBox.Height / 2), "CenterLeft", rotationCursorRx, Brushes.Red),
|
||||||
(new Point(rectBox.Right + offset, rectBox.Top + rectBox.Height / 2), "CenterRight", Cursors.SizeWE, Brushes.Blue)
|
(new Point(rectBox.Right + offset, rectBox.Top + rectBox.Height / 2), "CenterRight", cursors.ResizeHorizontal, Brushes.Blue)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var (Position, Tag, Cursor, Stroke) in positions)
|
foreach (var (Position, Tag, Cursor, Stroke) in positions)
|
||||||
|
@ -338,6 +411,43 @@ namespace CtrEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private (Cursor ResizeHorizontal, Cursor ResizeVertical, Cursor ResizeBoth) GetAdaptiveCursors()
|
||||||
|
{
|
||||||
|
// Para un solo objeto o múltiples objetos con la misma rotación
|
||||||
|
var firstObject = _selectedObjects.FirstOrDefault();
|
||||||
|
if (firstObject == null)
|
||||||
|
return (Cursors.SizeWE, Cursors.SizeNS, Cursors.SizeNWSE);
|
||||||
|
|
||||||
|
double angle = Math.Abs(firstObject.Angulo % 360);
|
||||||
|
|
||||||
|
// Si el objeto no está significativamente rotado, usar cursores estándar
|
||||||
|
if (angle < 15 || (angle > 345 && angle < 360) ||
|
||||||
|
(angle > 165 && angle < 195))
|
||||||
|
{
|
||||||
|
return (Cursors.SizeWE, Cursors.SizeNS, Cursors.SizeNWSE);
|
||||||
|
}
|
||||||
|
else if ((angle > 75 && angle < 105) || (angle > 255 && angle < 285))
|
||||||
|
{
|
||||||
|
// Objeto rotado 90 grados - intercambiar cursores
|
||||||
|
return (Cursors.SizeNS, Cursors.SizeWE, Cursors.SizeNESW);
|
||||||
|
}
|
||||||
|
else if ((angle > 30 && angle < 60) || (angle > 210 && angle < 240))
|
||||||
|
{
|
||||||
|
// Objeto rotado aproximadamente 45 grados
|
||||||
|
return (Cursors.SizeNESW, Cursors.SizeNWSE, Cursors.SizeAll);
|
||||||
|
}
|
||||||
|
else if ((angle > 120 && angle < 150) || (angle > 300 && angle < 330))
|
||||||
|
{
|
||||||
|
// Objeto rotado aproximadamente 135 grados
|
||||||
|
return (Cursors.SizeNWSE, Cursors.SizeNESW, Cursors.SizeAll);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Para otros ángulos, usar cursores generales
|
||||||
|
return (Cursors.SizeAll, Cursors.SizeAll, Cursors.SizeAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Rectangle CreateResizeHandle(double rectSize, string tag, Cursor cursor, Brush stroke)
|
private Rectangle CreateResizeHandle(double rectSize, string tag, Cursor cursor, Brush stroke)
|
||||||
{
|
{
|
||||||
var handle = new Rectangle
|
var handle = new Rectangle
|
||||||
|
@ -615,6 +725,9 @@ namespace CtrEditor
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capturar estado antes de iniciar redimensionamiento
|
||||||
|
CaptureUndoState();
|
||||||
|
|
||||||
_currentDraggingRectangle = sender as Rectangle;
|
_currentDraggingRectangle = sender as Rectangle;
|
||||||
_lastMousePosition = e.GetPosition(_canvas);
|
_lastMousePosition = e.GetPosition(_canvas);
|
||||||
_currentDraggingRectangle.CaptureMouse();
|
_currentDraggingRectangle.CaptureMouse();
|
||||||
|
@ -625,25 +738,49 @@ namespace CtrEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
var userControl = sender as UserControl;
|
var userControl = sender as UserControl;
|
||||||
if (userControl != null)
|
if (userControl != null && userControl.DataContext is osBase datos)
|
||||||
{
|
{
|
||||||
if (userControl.DataContext is osBase datos)
|
bool isControlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
|
||||||
{
|
|
||||||
HandleObjectSelection(userControl, datos);
|
|
||||||
|
|
||||||
// Actualizar el estado de objetos bloqueados después de la selección
|
// Determinar qué objetos serán afectados por el movimiento
|
||||||
hasLockedObjects = _selectedObjects.Any(obj => obj.Lock_movement);
|
List<osBase> objectsToMove = new List<osBase>();
|
||||||
|
|
||||||
|
if (!isControlPressed)
|
||||||
|
{
|
||||||
|
// Si no está Ctrl presionado y vamos a hacer dragging
|
||||||
|
if (_selectedObjects.Contains(datos))
|
||||||
|
{
|
||||||
|
// Si el objeto ya está seleccionado, vamos a mover todos los seleccionados
|
||||||
|
objectsToMove.AddRange(_selectedObjects);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Si no está seleccionado, solo vamos a mover este objeto
|
||||||
|
objectsToMove.Add(datos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Solo iniciar el arrastre si no se presionó Ctrl y no hay objetos bloqueados
|
// Verificar si alguno de los objetos a mover está bloqueado
|
||||||
if (!(Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && !hasLockedObjects)
|
bool willMoveLockedObjects = objectsToMove.Any(obj => obj.Lock_movement);
|
||||||
|
|
||||||
|
if (!willMoveLockedObjects)
|
||||||
{
|
{
|
||||||
|
// Capturar estado antes de iniciar movimiento
|
||||||
|
CaptureUndoStateForObjects(objectsToMove);
|
||||||
|
|
||||||
userControl.CaptureMouse();
|
userControl.CaptureMouse();
|
||||||
_currentDraggingControl = userControl;
|
_currentDraggingControl = userControl;
|
||||||
_isMovingUserControl = true;
|
_isMovingUserControl = true;
|
||||||
InitializeDrag(e);
|
InitializeDrag(e);
|
||||||
}
|
}
|
||||||
else if (hasLockedObjects)
|
}
|
||||||
|
|
||||||
|
// Manejar la selección después de capturar el estado
|
||||||
|
HandleObjectSelection(userControl, datos);
|
||||||
|
|
||||||
|
// Actualizar el estado de objetos bloqueados después de la selección
|
||||||
|
hasLockedObjects = _selectedObjects.Any(obj => obj.Lock_movement);
|
||||||
|
|
||||||
|
if (hasLockedObjects && !isControlPressed)
|
||||||
{
|
{
|
||||||
// Si hay objetos bloqueados, no permitir el inicio del arrastre
|
// Si hay objetos bloqueados, no permitir el inicio del arrastre
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
@ -837,6 +974,39 @@ namespace CtrEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
private HandleMode DetermineHandleMode(string resizeDirection)
|
private HandleMode DetermineHandleMode(string resizeDirection)
|
||||||
|
{
|
||||||
|
// Si tenemos múltiples objetos seleccionados con diferentes rotaciones,
|
||||||
|
// usar el comportamiento por defecto
|
||||||
|
if (_selectedObjects.Count > 1)
|
||||||
|
{
|
||||||
|
var angles = _selectedObjects.Select(obj => Math.Abs(obj.Angulo % 360)).Distinct().ToList();
|
||||||
|
if (angles.Count > 1) // Diferentes ángulos
|
||||||
|
{
|
||||||
|
return DetermineHandleModeDefault(resizeDirection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para un solo objeto o múltiples objetos con la misma rotación
|
||||||
|
var firstObject = _selectedObjects.FirstOrDefault();
|
||||||
|
if (firstObject == null)
|
||||||
|
return HandleMode.None;
|
||||||
|
|
||||||
|
double angle = Math.Abs(firstObject.Angulo % 360);
|
||||||
|
|
||||||
|
// Si el objeto no está significativamente rotado, usar el comportamiento por defecto
|
||||||
|
if (angle < 15 || (angle > 345 && angle < 360) ||
|
||||||
|
(angle > 75 && angle < 105) ||
|
||||||
|
(angle > 165 && angle < 195) ||
|
||||||
|
(angle > 255 && angle < 285))
|
||||||
|
{
|
||||||
|
return DetermineHandleModeDefault(resizeDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para objetos rotados significativamente, adaptar los handles
|
||||||
|
return DetermineHandleModeRotated(resizeDirection, angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HandleMode DetermineHandleModeDefault(string resizeDirection)
|
||||||
{
|
{
|
||||||
return resizeDirection switch
|
return resizeDirection switch
|
||||||
{
|
{
|
||||||
|
@ -850,6 +1020,21 @@ namespace CtrEditor
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HandleMode DetermineHandleModeRotated(string resizeDirection, double angle)
|
||||||
|
{
|
||||||
|
// Para objetos rotados, hacer que todos los handles de redimensionamiento funcionen como los corners
|
||||||
|
// Esto es mucho más intuitivo ya que el usuario puede redimensionar en cualquier dirección
|
||||||
|
// sin preocuparse por la orientación específica del objeto
|
||||||
|
|
||||||
|
return resizeDirection switch
|
||||||
|
{
|
||||||
|
"TopLeft" => HandleMode.None,
|
||||||
|
"TopRight" or "BottomLeft" or "CenterLeft" => HandleMode.Rotate,
|
||||||
|
"BottomRight" or "BottomCenter" or "CenterRight" or "TopCenter" => HandleMode.ResizeBoth,
|
||||||
|
_ => HandleMode.None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleResize(Point currentPosition, HandleMode mode)
|
private void HandleResize(Point currentPosition, HandleMode mode)
|
||||||
{
|
{
|
||||||
PurgeDeletedObjects();
|
PurgeDeletedObjects();
|
||||||
|
@ -876,8 +1061,53 @@ namespace CtrEditor
|
||||||
|
|
||||||
private void HandleResizeForObject(osBase obj, Point currentPosition, bool activateSizeWidth, bool activateSizeHeight)
|
private void HandleResizeForObject(osBase obj, Point currentPosition, bool activateSizeWidth, bool activateSizeHeight)
|
||||||
{
|
{
|
||||||
double widthChange = activateSizeWidth ? currentPosition.X - _startPointUserControl.X : 0;
|
// Calcular el cambio del mouse en coordenadas del canvas
|
||||||
double heightChange = activateSizeHeight ? currentPosition.Y - _startPointUserControl.Y : 0;
|
double deltaX = currentPosition.X - _startPointUserControl.X;
|
||||||
|
double deltaY = currentPosition.Y - _startPointUserControl.Y;
|
||||||
|
|
||||||
|
// Inicializar variables de cambio
|
||||||
|
double widthChange = 0;
|
||||||
|
double heightChange = 0;
|
||||||
|
|
||||||
|
// Si el objeto no está rotado, usar el método original
|
||||||
|
if (Math.Abs(obj.Angulo) < 0.1) // Umbral pequeño para considerar "sin rotación"
|
||||||
|
{
|
||||||
|
widthChange = activateSizeWidth ? deltaX : 0;
|
||||||
|
heightChange = activateSizeHeight ? deltaY : 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Para objetos rotados, transformar los cambios del mouse según la orientación del objeto
|
||||||
|
// Convertir ángulo de grados a radianes
|
||||||
|
double angleRad = obj.Angulo * Math.PI / 180.0;
|
||||||
|
|
||||||
|
// Calcular los componentes de cambio transformados según la rotación
|
||||||
|
// Rotar el vector de cambio por el ángulo negativo del objeto para obtener
|
||||||
|
// los cambios en el sistema de coordenadas local del objeto
|
||||||
|
double cos = Math.Cos(-angleRad);
|
||||||
|
double sin = Math.Sin(-angleRad);
|
||||||
|
|
||||||
|
double localDeltaX = deltaX * cos - deltaY * sin;
|
||||||
|
double localDeltaY = deltaX * sin + deltaY * cos;
|
||||||
|
|
||||||
|
// Determinar qué componente aplicar según el tipo de redimensionamiento
|
||||||
|
if (activateSizeWidth && activateSizeHeight)
|
||||||
|
{
|
||||||
|
// Redimensionamiento proporcional - usar la componente más grande transformada
|
||||||
|
widthChange = localDeltaX;
|
||||||
|
heightChange = localDeltaY;
|
||||||
|
}
|
||||||
|
else if (activateSizeWidth)
|
||||||
|
{
|
||||||
|
// Solo redimensionar ancho - usar componente X transformada
|
||||||
|
widthChange = localDeltaX;
|
||||||
|
}
|
||||||
|
else if (activateSizeHeight)
|
||||||
|
{
|
||||||
|
// Solo redimensionar alto - usar componente Y transformada
|
||||||
|
heightChange = localDeltaY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
obj.Resize(widthChange, heightChange);
|
obj.Resize(widthChange, heightChange);
|
||||||
}
|
}
|
||||||
|
@ -929,6 +1159,9 @@ namespace CtrEditor
|
||||||
return; // No alinear si hay objetos bloqueados
|
return; // No alinear si hay objetos bloqueados
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capturar estado antes de alinear
|
||||||
|
CaptureUndoState();
|
||||||
|
|
||||||
var alignment = new ObjectAlignment(_selectedObjects, _selectedObjects.FirstOrDefault());
|
var alignment = new ObjectAlignment(_selectedObjects, _selectedObjects.FirstOrDefault());
|
||||||
|
|
||||||
switch (alignmentType)
|
switch (alignmentType)
|
||||||
|
@ -1090,5 +1323,149 @@ namespace CtrEditor
|
||||||
UpdateSelectionRectangle(currentPoint);
|
UpdateSelectionRectangle(currentPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Métodos del sistema de Undo
|
||||||
|
public void CaptureUndoState()
|
||||||
|
{
|
||||||
|
// Solo capturar estado si no estamos aplicando un undo y si es permitido
|
||||||
|
if (_isApplyingUndo || !CanPerformUndo())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Solo capturar estado si hay objetos seleccionados
|
||||||
|
if (_selectedObjects.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_mainWindow.DataContext is MainViewModel viewModel)
|
||||||
|
{
|
||||||
|
// Capturar solo el estado de los objetos seleccionados
|
||||||
|
var undoState = new UndoState(_selectedObjects);
|
||||||
|
|
||||||
|
// Mantener solo los últimos MaxUndoSteps estados
|
||||||
|
if (_undoHistory.Count >= MaxUndoSteps)
|
||||||
|
{
|
||||||
|
// Convertir a lista, remover el más antiguo, y volver a crear el stack
|
||||||
|
var tempList = _undoHistory.ToList();
|
||||||
|
tempList.Reverse(); // Invertir para mantener el orden correcto
|
||||||
|
tempList.RemoveAt(0); // Remover el más antiguo
|
||||||
|
_undoHistory.Clear();
|
||||||
|
foreach (var state in tempList)
|
||||||
|
{
|
||||||
|
_undoHistory.Push(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_undoHistory.Push(undoState);
|
||||||
|
Console.WriteLine($"Estado capturado para undo de {_selectedObjects.Count} objetos. Historial: {_undoHistory.Count} estados");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para capturar el estado de objetos específicos
|
||||||
|
public void CaptureUndoStateForObjects(IEnumerable<osBase> objects)
|
||||||
|
{
|
||||||
|
// Solo capturar estado si no estamos aplicando un undo y si es permitido
|
||||||
|
if (_isApplyingUndo || !CanPerformUndo())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var objectsList = objects.ToList();
|
||||||
|
if (objectsList.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_mainWindow.DataContext is MainViewModel viewModel)
|
||||||
|
{
|
||||||
|
// Capturar solo el estado de los objetos especificados
|
||||||
|
var undoState = new UndoState(objectsList);
|
||||||
|
|
||||||
|
// Mantener solo los últimos MaxUndoSteps estados
|
||||||
|
if (_undoHistory.Count >= MaxUndoSteps)
|
||||||
|
{
|
||||||
|
// Convertir a lista, remover el más antiguo, y volver a crear el stack
|
||||||
|
var tempList = _undoHistory.ToList();
|
||||||
|
tempList.Reverse(); // Invertir para mantener el orden correcto
|
||||||
|
tempList.RemoveAt(0); // Remover el más antiguo
|
||||||
|
_undoHistory.Clear();
|
||||||
|
foreach (var state in tempList)
|
||||||
|
{
|
||||||
|
_undoHistory.Push(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_undoHistory.Push(undoState);
|
||||||
|
Console.WriteLine($"Estado capturado para undo de {objectsList.Count} objetos específicos. Historial: {_undoHistory.Count} estados");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanPerformUndo()
|
||||||
|
{
|
||||||
|
if (_mainWindow.DataContext is MainViewModel viewModel)
|
||||||
|
{
|
||||||
|
// Solo permitir undo si no estamos en simulación ni conectados
|
||||||
|
return !viewModel.IsSimulationRunning && !viewModel.IsConnected;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanUndo()
|
||||||
|
{
|
||||||
|
return CanPerformUndo() && _undoHistory.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PerformUndo()
|
||||||
|
{
|
||||||
|
if (!CanUndo())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_mainWindow.DataContext is MainViewModel viewModel)
|
||||||
|
{
|
||||||
|
_isApplyingUndo = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var undoState = _undoHistory.Pop();
|
||||||
|
Console.WriteLine($"Aplicando undo. Estados restantes: {_undoHistory.Count}");
|
||||||
|
|
||||||
|
// Aplicar el estado a cada objeto
|
||||||
|
foreach (var objectState in undoState.ObjectStates)
|
||||||
|
{
|
||||||
|
var obj = viewModel.ObjetosSimulables.FirstOrDefault(o => o.Id.Value == objectState.ObjectId);
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
// Aplicar solo los parámetros básicos
|
||||||
|
obj.Left = objectState.Left;
|
||||||
|
obj.Top = objectState.Top;
|
||||||
|
obj.Angulo = objectState.Angle;
|
||||||
|
obj.Ancho = objectState.Width;
|
||||||
|
obj.Alto = objectState.Height;
|
||||||
|
|
||||||
|
// Actualizar la posición visual
|
||||||
|
obj.OnMoveResizeRotate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar los visuales de selección
|
||||||
|
UpdateSelectionVisuals();
|
||||||
|
|
||||||
|
// Marcar como cambios sin guardar
|
||||||
|
viewModel.HasUnsavedChanges = true;
|
||||||
|
|
||||||
|
Console.WriteLine("Undo aplicado exitosamente");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isApplyingUndo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearUndoHistory()
|
||||||
|
{
|
||||||
|
_undoHistory.Clear();
|
||||||
|
Console.WriteLine("Historial de undo limpiado");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método de conveniencia para obtener información del historial
|
||||||
|
public int GetUndoHistoryCount()
|
||||||
|
{
|
||||||
|
return _undoHistory.Count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,12 +5,15 @@
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
|
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<UserControl.DataContext>
|
<UserControl.DataContext>
|
||||||
<vm:osFramePlate Color_Titulo="Black" Alto_Titulo="0.1" Color="WhiteSmoke" />
|
<vm:osFramePlate Color_Titulo="Black" Alto_Titulo="0.1" Color="WhiteSmoke" />
|
||||||
</UserControl.DataContext>
|
</UserControl.DataContext>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<RotateTransform Angle="{Binding Angulo}"
|
||||||
|
CenterX="{Binding RenderTransformCenterX}"
|
||||||
|
CenterY="{Binding RenderTransformCenterY}" />
|
||||||
|
</Grid.RenderTransform>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
@ -24,6 +27,5 @@
|
||||||
Fill="{Binding Color, Converter={StaticResource ColorToBrushConverter}}" Stroke="Blue"
|
Fill="{Binding Color, Converter={StaticResource ColorToBrushConverter}}" Stroke="Blue"
|
||||||
StrokeThickness="0.2"
|
StrokeThickness="0.2"
|
||||||
Visibility="{Binding ShowPlate, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
Visibility="{Binding ShowPlate, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
|
@ -24,6 +24,8 @@ namespace CtrEditor.ObjetosSim
|
||||||
public float offsetY;
|
public float offsetY;
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public float offsetX;
|
public float offsetX;
|
||||||
|
[JsonIgnore]
|
||||||
|
public float offsetAngulo;
|
||||||
|
|
||||||
public static string NombreClase()
|
public static string NombreClase()
|
||||||
{
|
{
|
||||||
|
@ -103,6 +105,29 @@ namespace CtrEditor.ObjetosSim
|
||||||
[property: Category("Encoders:")]
|
[property: Category("Encoders:")]
|
||||||
public float offset_encoder_Y;
|
public float offset_encoder_Y;
|
||||||
|
|
||||||
|
// Encoder Rotation
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Description("This is a link to a Encoder for Rotation.")]
|
||||||
|
[property: Category("Encoders:")]
|
||||||
|
[property: ItemsSource(typeof(osBaseItemsSource<osEncoderMotorLineal>))]
|
||||||
|
private string encoder_Rotation;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Description("K Pulses per degree for Rotation")]
|
||||||
|
[property: Category("Encoders:")]
|
||||||
|
public float k_encoder_Rotation;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: ReadOnly(true)]
|
||||||
|
[property: Description("Actual Rotation Angle")]
|
||||||
|
[property: Category("Encoders:")]
|
||||||
|
public float encoder_Rotation_Angle;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Description("Angle in degrees offset. Angle when the encoder is 0")]
|
||||||
|
[property: Category("Encoders:")]
|
||||||
|
public float offset_encoder_Rotation;
|
||||||
|
|
||||||
partial void OnK_encoder_YChanged(float value)
|
partial void OnK_encoder_YChanged(float value)
|
||||||
{
|
{
|
||||||
UpdatePosition();
|
UpdatePosition();
|
||||||
|
@ -123,18 +148,34 @@ namespace CtrEditor.ObjetosSim
|
||||||
UpdatePosition();
|
UpdatePosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnK_encoder_RotationChanged(float value)
|
||||||
|
{
|
||||||
|
UpdatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnOffset_encoder_RotationChanged(float value)
|
||||||
|
{
|
||||||
|
UpdatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private osEncoderMotorLineal EncoderX;
|
private osEncoderMotorLineal EncoderX;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private osEncoderMotorLineal EncoderY;
|
private osEncoderMotorLineal EncoderY;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
private osEncoderMotorLineal EncoderRotation;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private bool isUpdatingFromEncoderX = false;
|
private bool isUpdatingFromEncoderX = false;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private bool isUpdatingFromEncoderY = false;
|
private bool isUpdatingFromEncoderY = false;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
private bool isUpdatingFromEncoderRotation = false;
|
||||||
|
|
||||||
partial void OnEncoder_XChanged(string value)
|
partial void OnEncoder_XChanged(string value)
|
||||||
{
|
{
|
||||||
if (EncoderX != null)
|
if (EncoderX != null)
|
||||||
|
@ -159,6 +200,18 @@ namespace CtrEditor.ObjetosSim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnEncoder_RotationChanged(string value)
|
||||||
|
{
|
||||||
|
if (EncoderRotation != null)
|
||||||
|
EncoderRotation.PropertyChanged -= OnEncoderRotationPropertyChanged;
|
||||||
|
if (_mainViewModel != null && value != null && value.Length > 0)
|
||||||
|
{
|
||||||
|
EncoderRotation = (osEncoderMotorLineal)_mainViewModel.ObjetosSimulables.FirstOrDefault(s => (s is osEncoderMotorLineal && s.Nombre == value), null);
|
||||||
|
if (EncoderRotation != null)
|
||||||
|
EncoderRotation.PropertyChanged += OnEncoderRotationPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnEncoderXPropertyChanged(object sender, PropertyChangedEventArgs e)
|
private void OnEncoderXPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!isUpdatingFromEncoderX)
|
if (!isUpdatingFromEncoderX)
|
||||||
|
@ -195,12 +248,30 @@ namespace CtrEditor.ObjetosSim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnEncoderRotationPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!isUpdatingFromEncoderRotation)
|
||||||
|
{
|
||||||
|
isUpdatingFromEncoderRotation = true;
|
||||||
|
// Actualizamos el nombre si este fue modificado
|
||||||
|
if (e.PropertyName == nameof(osEncoderMotorLineal.Nombre))
|
||||||
|
Encoder_Rotation = ((osEncoderMotorLineal)sender).Nombre;
|
||||||
|
|
||||||
|
if (e.PropertyName == nameof(osEncoderMotorLineal.Valor_Actual))
|
||||||
|
{
|
||||||
|
UpdatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdatingFromEncoderRotation = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdatePosition()
|
public void UpdatePosition()
|
||||||
{
|
{
|
||||||
// Update X position if encoder is available
|
// Update X position if encoder is available
|
||||||
if (EncoderX != null && K_encoder_X != 0)
|
if (EncoderX != null && K_encoder_X != 0)
|
||||||
{
|
{
|
||||||
Encoder_Y_Position = EncoderY.Valor_Actual;
|
Encoder_X_Position = EncoderX.Valor_Actual;
|
||||||
Left = (Encoder_X_Position / k_encoder_X) + offset_encoder_X;
|
Left = (Encoder_X_Position / k_encoder_X) + offset_encoder_X;
|
||||||
}
|
}
|
||||||
// Update Y position if encoder is available
|
// Update Y position if encoder is available
|
||||||
|
@ -209,6 +280,12 @@ namespace CtrEditor.ObjetosSim
|
||||||
Encoder_Y_Position = EncoderY.Valor_Actual;
|
Encoder_Y_Position = EncoderY.Valor_Actual;
|
||||||
Top = (Encoder_Y_Position / k_encoder_Y) + offset_encoder_Y;
|
Top = (Encoder_Y_Position / k_encoder_Y) + offset_encoder_Y;
|
||||||
}
|
}
|
||||||
|
// Update rotation angle if encoder is available
|
||||||
|
if (EncoderRotation != null && K_encoder_Rotation != 0)
|
||||||
|
{
|
||||||
|
Encoder_Rotation_Angle = EncoderRotation.Valor_Actual;
|
||||||
|
Angulo = (Encoder_Rotation_Angle / k_encoder_Rotation) + offset_encoder_Rotation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void TopChanging(float oldValue, float newValue)
|
public override void TopChanging(float oldValue, float newValue)
|
||||||
|
@ -220,11 +297,44 @@ namespace CtrEditor.ObjetosSim
|
||||||
offsetX = newValue - oldValue;
|
offsetX = newValue - oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void AnguloChanging(float oldValue, float newValue)
|
||||||
|
{
|
||||||
|
offsetAngulo = newValue - oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[property: Description("Show/Hide the plate background")]
|
[property: Description("Show/Hide the plate background")]
|
||||||
[property: Category("Appearance:")]
|
[property: Category("Appearance:")]
|
||||||
private bool showPlate;
|
private bool showPlate;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Description("Use horizontal center as pivot point for rotation")]
|
||||||
|
[property: Category("Pivot:")]
|
||||||
|
private bool pivotCenterX;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Description("Use vertical center as pivot point for rotation")]
|
||||||
|
[property: Category("Pivot:")]
|
||||||
|
private bool pivotCenterY;
|
||||||
|
|
||||||
|
partial void OnPivotCenterXChanged(bool value)
|
||||||
|
{
|
||||||
|
// Notificar cambio de pivot para actualizar objetos conectados
|
||||||
|
OnPropertyChanged(nameof(PivotCenterX));
|
||||||
|
// Notificar cambios en las propiedades de pivot para el RenderTransform
|
||||||
|
OnPropertyChanged(nameof(RenderTransformCenterX));
|
||||||
|
OnPropertyChanged(nameof(RenderTransformCenterY));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnPivotCenterYChanged(bool value)
|
||||||
|
{
|
||||||
|
// Notificar cambio de pivot para actualizar objetos conectados
|
||||||
|
OnPropertyChanged(nameof(PivotCenterY));
|
||||||
|
// Notificar cambios en las propiedades de pivot para el RenderTransform
|
||||||
|
OnPropertyChanged(nameof(RenderTransformCenterX));
|
||||||
|
OnPropertyChanged(nameof(RenderTransformCenterY));
|
||||||
|
}
|
||||||
|
|
||||||
public osFramePlate()
|
public osFramePlate()
|
||||||
{
|
{
|
||||||
Ancho = 0.5f;
|
Ancho = 0.5f;
|
||||||
|
@ -236,12 +346,30 @@ namespace CtrEditor.ObjetosSim
|
||||||
ShowPlate = true; // Default value
|
ShowPlate = true; // Default value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void AnchoChanged(float value)
|
||||||
|
{
|
||||||
|
base.AnchoChanged(value);
|
||||||
|
OnPropertyChanged(nameof(RenderTransformCenterX));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void AltoChanged(float value)
|
||||||
|
{
|
||||||
|
base.AltoChanged(value);
|
||||||
|
OnPropertyChanged(nameof(RenderTransformCenterY));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnAlto_TituloChanged(float value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(RenderTransformCenterY));
|
||||||
|
}
|
||||||
|
|
||||||
public override void ucLoaded()
|
public override void ucLoaded()
|
||||||
{
|
{
|
||||||
base.ucLoaded();
|
base.ucLoaded();
|
||||||
// El UserControl se ha cargado
|
// El UserControl se ha cargado
|
||||||
OnEncoder_XChanged(Encoder_X);
|
OnEncoder_XChanged(Encoder_X);
|
||||||
OnEncoder_YChanged(Encoder_Y);
|
OnEncoder_YChanged(Encoder_Y);
|
||||||
|
OnEncoder_RotationChanged(Encoder_Rotation);
|
||||||
UpdateZIndex(Zindex_FramePlate);
|
UpdateZIndex(Zindex_FramePlate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,6 +380,63 @@ namespace CtrEditor.ObjetosSim
|
||||||
// eliminar el objeto de simulacion
|
// eliminar el objeto de simulacion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula el punto de pivot basado en los checkboxes PivotCenterX y PivotCenterY
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Punto de pivot en coordenadas absolutas</returns>
|
||||||
|
public (float X, float Y) GetPivotPoint()
|
||||||
|
{
|
||||||
|
float pivotX = Left + (PivotCenterX ? Ancho / 2 : 0);
|
||||||
|
float pivotY = Top + (PivotCenterY ? Alto / 2 : 0);
|
||||||
|
return (pivotX, pivotY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Centro X para el RenderTransform en píxeles del control
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public double RenderTransformCenterX
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (PivotCenterX)
|
||||||
|
{
|
||||||
|
// Centro horizontal del rectángulo principal
|
||||||
|
return PixelToMeter.Instance.calc.MetersToPixels(Ancho) / 2.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Left del rectángulo principal (0)
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Centro Y para el RenderTransform en píxeles del control
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public double RenderTransformCenterY
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Altura del título y del rectángulo principal
|
||||||
|
double titleHeight = PixelToMeter.Instance.calc.MetersToPixels(Alto_Titulo);
|
||||||
|
double frameHeight = PixelToMeter.Instance.calc.MetersToPixels(Alto);
|
||||||
|
|
||||||
|
if (PivotCenterY)
|
||||||
|
{
|
||||||
|
// Centro vertical de todo el control (título + rectángulo)
|
||||||
|
return (titleHeight + frameHeight) / 2.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Top absoluto del control (0)
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class ucFramePlate : UserControl, IDataContainer
|
public partial class ucFramePlate : UserControl, IDataContainer
|
||||||
|
|
|
@ -51,6 +51,12 @@ namespace CtrEditor.ObjetosSim
|
||||||
|
|
||||||
partial void OnDiametroChanged(float value)
|
partial void OnDiametroChanged(float value)
|
||||||
{
|
{
|
||||||
|
// Asegurar que el diámetro sea al menos 0.01m para evitar errores en la simulación
|
||||||
|
if (value < 0.01f)
|
||||||
|
{
|
||||||
|
diametro = 0.01f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
SimGeometria?.SetDiameter(Diametro);
|
SimGeometria?.SetDiameter(Diametro);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,6 +133,12 @@ namespace CtrEditor.ObjetosSim
|
||||||
TiempoRestante -= elapsedMilliseconds / 1000.0f;
|
TiempoRestante -= elapsedMilliseconds / 1000.0f;
|
||||||
if (TiempoRestante <= 0)
|
if (TiempoRestante <= 0)
|
||||||
{
|
{
|
||||||
|
// Validar que el diámetro sea al menos 0.01m para evitar errores en la simulación
|
||||||
|
if (Diametro_botella < 0.01f)
|
||||||
|
{
|
||||||
|
Diametro_botella = 0.01f;
|
||||||
|
}
|
||||||
|
|
||||||
var X = Left + OffsetLeftSalida;
|
var X = Left + OffsetLeftSalida;
|
||||||
var Y = Top + OffsetTopSalida;
|
var Y = Top + OffsetTopSalida;
|
||||||
|
|
||||||
|
|
|
@ -106,7 +106,11 @@ namespace CtrEditor.ObjetosSim
|
||||||
LeftChanging(oldValue, newValue);
|
LeftChanging(oldValue, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void LeftChanged(float value) { }
|
public virtual void LeftChanged(float value)
|
||||||
|
{
|
||||||
|
// Actualizar posición relativa si el movimiento no viene del FramePlate
|
||||||
|
UpdateFramePlateRelativePosition();
|
||||||
|
}
|
||||||
public virtual void LeftChanging(float oldValue, float newValue) { }
|
public virtual void LeftChanging(float oldValue, float newValue) { }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
@ -124,7 +128,11 @@ namespace CtrEditor.ObjetosSim
|
||||||
TopChanging(oldValue, newValue);
|
TopChanging(oldValue, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void TopChanged(float value) { }
|
public virtual void TopChanged(float value)
|
||||||
|
{
|
||||||
|
// Actualizar posición relativa si el movimiento no viene del FramePlate
|
||||||
|
UpdateFramePlateRelativePosition();
|
||||||
|
}
|
||||||
public virtual void TopChanging(float oldValue, float newValue) { }
|
public virtual void TopChanging(float oldValue, float newValue) { }
|
||||||
|
|
||||||
|
|
||||||
|
@ -160,7 +168,13 @@ namespace CtrEditor.ObjetosSim
|
||||||
{
|
{
|
||||||
AnguloChanged(value);
|
AnguloChanged(value);
|
||||||
}
|
}
|
||||||
|
partial void OnAnguloChanging(float oldValue, float newValue)
|
||||||
|
{
|
||||||
|
AnguloChanging(oldValue, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void AnguloChanged(float value) { }
|
public virtual void AnguloChanged(float value) { }
|
||||||
|
public virtual void AnguloChanging(float oldValue, float newValue) { }
|
||||||
|
|
||||||
|
|
||||||
public void Resize(double width, double height)
|
public void Resize(double width, double height)
|
||||||
|
@ -283,6 +297,32 @@ namespace CtrEditor.ObjetosSim
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private bool isUpdatingFromFramePlate = false;
|
private bool isUpdatingFromFramePlate = false;
|
||||||
|
|
||||||
|
// Variables para rotación orbital alrededor del FramePlate
|
||||||
|
[JsonIgnore]
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Hidden]
|
||||||
|
private float framePlate_RelativeX;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Hidden]
|
||||||
|
private float framePlate_RelativeY;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Hidden]
|
||||||
|
private float framePlate_InitialAngle;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Hidden]
|
||||||
|
private float framePlate_PivotX;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[ObservableProperty]
|
||||||
|
[property: Hidden]
|
||||||
|
private float framePlate_PivotY;
|
||||||
|
|
||||||
partial void OnGroup_FramePanelChanged(string value)
|
partial void OnGroup_FramePanelChanged(string value)
|
||||||
{
|
{
|
||||||
if (FramePlate != null)
|
if (FramePlate != null)
|
||||||
|
@ -294,8 +334,25 @@ namespace CtrEditor.ObjetosSim
|
||||||
{
|
{
|
||||||
FramePlate.PropertyChanged += OnFramePlatePropertyChanged;
|
FramePlate.PropertyChanged += OnFramePlatePropertyChanged;
|
||||||
UpdateZIndex(FramePlate.Zindex_FramePlate);
|
UpdateZIndex(FramePlate.Zindex_FramePlate);
|
||||||
|
|
||||||
|
// Calcular punto de pivot y posición relativa inicial
|
||||||
|
var pivotPoint = FramePlate.GetPivotPoint();
|
||||||
|
FramePlate_PivotX = pivotPoint.X;
|
||||||
|
FramePlate_PivotY = pivotPoint.Y;
|
||||||
|
FramePlate_RelativeX = Left - FramePlate_PivotX;
|
||||||
|
FramePlate_RelativeY = Top - FramePlate_PivotY;
|
||||||
|
FramePlate_InitialAngle = FramePlate.Angulo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Reset relative position when disconnecting
|
||||||
|
FramePlate_RelativeX = 0;
|
||||||
|
FramePlate_RelativeY = 0;
|
||||||
|
FramePlate_InitialAngle = 0;
|
||||||
|
FramePlate_PivotX = 0;
|
||||||
|
FramePlate_PivotY = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void UpdateZIndex(int zIndex)
|
protected void UpdateZIndex(int zIndex)
|
||||||
|
@ -316,16 +373,25 @@ namespace CtrEditor.ObjetosSim
|
||||||
if (e.PropertyName == nameof(osFramePlate.Nombre))
|
if (e.PropertyName == nameof(osFramePlate.Nombre))
|
||||||
Group_Panel = ((osFramePlate)sender).Nombre;
|
Group_Panel = ((osFramePlate)sender).Nombre;
|
||||||
|
|
||||||
if (e.PropertyName == nameof(osFramePlate.Top))
|
if (e.PropertyName == nameof(osFramePlate.Top) ||
|
||||||
|
e.PropertyName == nameof(osFramePlate.Left))
|
||||||
{
|
{
|
||||||
Top += ((osFramePlate)sender).offsetY;
|
UpdateOrbitalPosition();
|
||||||
OnMoveResizeRotate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.PropertyName == nameof(osFramePlate.Left))
|
if (e.PropertyName == nameof(osFramePlate.Angulo))
|
||||||
{
|
{
|
||||||
Left += ((osFramePlate)sender).offsetX;
|
// Primero actualizar el ángulo del objeto
|
||||||
OnMoveResizeRotate();
|
Angulo += ((osFramePlate)sender).offsetAngulo;
|
||||||
|
// Luego actualizar la posición orbital con el ángulo ya actualizado
|
||||||
|
UpdateOrbitalPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.PropertyName == nameof(osFramePlate.PivotCenterX) ||
|
||||||
|
e.PropertyName == nameof(osFramePlate.PivotCenterY))
|
||||||
|
{
|
||||||
|
// Cuando cambia el pivot, recalcular posición relativa
|
||||||
|
RecalculateRelativePositionForNewPivot();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.PropertyName == nameof(osFramePlate.Zindex_FramePlate))
|
if (e.PropertyName == nameof(osFramePlate.Zindex_FramePlate))
|
||||||
|
@ -335,6 +401,96 @@ namespace CtrEditor.ObjetosSim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateOrbitalPosition()
|
||||||
|
{
|
||||||
|
if (FramePlate == null) return;
|
||||||
|
|
||||||
|
// Actualizar punto de pivot actual (puede haber cambiado por los checkboxes)
|
||||||
|
var currentPivot = FramePlate.GetPivotPoint();
|
||||||
|
|
||||||
|
// Calcular el ángulo de rotación total desde la posición inicial
|
||||||
|
float deltaAngle = FramePlate.Angulo - FramePlate_InitialAngle;
|
||||||
|
|
||||||
|
// Convertir ángulo a radianes
|
||||||
|
float angleRad = deltaAngle * (float)Math.PI / 180.0f;
|
||||||
|
|
||||||
|
// Calcular la nueva posición orbital usando rotación de matriz
|
||||||
|
float cosAngle = (float)Math.Cos(angleRad);
|
||||||
|
float sinAngle = (float)Math.Sin(angleRad);
|
||||||
|
|
||||||
|
// Rotar la posición relativa
|
||||||
|
float rotatedX = cosAngle * FramePlate_RelativeX - sinAngle * FramePlate_RelativeY;
|
||||||
|
float rotatedY = sinAngle * FramePlate_RelativeX + cosAngle * FramePlate_RelativeY;
|
||||||
|
|
||||||
|
// Calcular nueva posición absoluta usando el punto de pivot actual
|
||||||
|
float newLeft = currentPivot.X + rotatedX;
|
||||||
|
float newTop = currentPivot.Y + rotatedY;
|
||||||
|
|
||||||
|
// Actualizar directamente los campos sin disparar eventos de cambio
|
||||||
|
SetProperty(ref left, newLeft);
|
||||||
|
SetProperty(ref top, newTop);
|
||||||
|
|
||||||
|
// Forzar actualización de la posición visual
|
||||||
|
ActualizarLeftTop();
|
||||||
|
|
||||||
|
// Llamar a OnMoveResizeRotate para actualizar la física
|
||||||
|
OnMoveResizeRotate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFramePlateRelativePosition()
|
||||||
|
{
|
||||||
|
// Solo actualizar si está conectado a un FramePlate y el movimiento NO viene del FramePlate
|
||||||
|
if (FramePlate != null && !isUpdatingFromFramePlate)
|
||||||
|
{
|
||||||
|
// Obtener punto de pivot actual
|
||||||
|
var currentPivot = FramePlate.GetPivotPoint();
|
||||||
|
|
||||||
|
// Recalcular posición relativa considerando la rotación actual del FramePlate
|
||||||
|
float deltaAngle = FramePlate.Angulo - FramePlate_InitialAngle;
|
||||||
|
float angleRad = deltaAngle * (float)Math.PI / 180.0f;
|
||||||
|
|
||||||
|
// Calcular la posición relativa actual respecto al pivot del FramePlate
|
||||||
|
float currentRelativeX = Left - currentPivot.X;
|
||||||
|
float currentRelativeY = Top - currentPivot.Y;
|
||||||
|
|
||||||
|
// Si el FramePlate está rotado, necesitamos "desrotar" la posición para obtener
|
||||||
|
// la posición relativa en el sistema de coordenadas original
|
||||||
|
float cosAngle = (float)Math.Cos(-angleRad);
|
||||||
|
float sinAngle = (float)Math.Sin(-angleRad);
|
||||||
|
|
||||||
|
FramePlate_RelativeX = cosAngle * currentRelativeX - sinAngle * currentRelativeY;
|
||||||
|
FramePlate_RelativeY = sinAngle * currentRelativeX + cosAngle * currentRelativeY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecalculateRelativePositionForNewPivot()
|
||||||
|
{
|
||||||
|
if (FramePlate == null) return;
|
||||||
|
|
||||||
|
// Obtener posición absoluta actual del objeto (antes del cambio de pivot)
|
||||||
|
float currentLeft = Left;
|
||||||
|
float currentTop = Top;
|
||||||
|
|
||||||
|
// Obtener nuevo punto de pivot
|
||||||
|
var newPivot = FramePlate.GetPivotPoint();
|
||||||
|
|
||||||
|
// Calcular nueva posición relativa respecto al nuevo pivot
|
||||||
|
// Considerar la rotación actual para obtener posición relativa en sistema original
|
||||||
|
float deltaAngle = FramePlate.Angulo - FramePlate_InitialAngle;
|
||||||
|
float angleRad = deltaAngle * (float)Math.PI / 180.0f;
|
||||||
|
|
||||||
|
// Posición relativa actual respecto al nuevo pivot
|
||||||
|
float currentRelativeX = currentLeft - newPivot.X;
|
||||||
|
float currentRelativeY = currentTop - newPivot.Y;
|
||||||
|
|
||||||
|
// "Desrotar" para obtener coordenadas en el sistema original
|
||||||
|
float cosAngle = (float)Math.Cos(-angleRad);
|
||||||
|
float sinAngle = (float)Math.Sin(-angleRad);
|
||||||
|
|
||||||
|
FramePlate_RelativeX = cosAngle * currentRelativeX - sinAngle * currentRelativeY;
|
||||||
|
FramePlate_RelativeY = sinAngle * currentRelativeX + cosAngle * currentRelativeY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void ShowPreviewWindow(Stream imageStream)
|
private void ShowPreviewWindow(Stream imageStream)
|
||||||
{
|
{
|
||||||
|
@ -992,6 +1148,8 @@ namespace CtrEditor.ObjetosSim
|
||||||
CanvasSetTopinMeter(Top);
|
CanvasSetTopinMeter(Top);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public bool LeerBitTag(string Tag)
|
public bool LeerBitTag(string Tag)
|
||||||
{
|
{
|
||||||
if (this._plc == null) return false;
|
if (this._plc == null) return false;
|
||||||
|
|
|
@ -138,23 +138,58 @@ namespace CtrEditor.Simulacion
|
||||||
Vector2 centerToBottle = bottle.Body.Position - Body.Position;
|
Vector2 centerToBottle = bottle.Body.Position - Body.Position;
|
||||||
// Calculate angle of bottle relative to curve center (in radians)
|
// Calculate angle of bottle relative to curve center (in radians)
|
||||||
float bottleAngle = (float)Math.Atan2(centerToBottle.Y, centerToBottle.X);
|
float bottleAngle = (float)Math.Atan2(centerToBottle.Y, centerToBottle.X);
|
||||||
// Normalize angle to be positive
|
|
||||||
if (bottleAngle < 0)
|
// Normalize all angles to the same range [0, 2π]
|
||||||
bottleAngle += 2 * (float)Math.PI;
|
float normalizedBottleAngle = bottleAngle < 0 ? bottleAngle + 2 * (float)Math.PI : bottleAngle;
|
||||||
// If bottle is outside the angle range, return 0
|
float normalizedStartAngle = _startAngle < 0 ? _startAngle + 2 * (float)Math.PI : _startAngle;
|
||||||
if (bottleAngle < _startAngle || bottleAngle > _endAngle)
|
float normalizedEndAngle = _endAngle < 0 ? _endAngle + 2 * (float)Math.PI : _endAngle;
|
||||||
|
|
||||||
|
// Handle case where the arc crosses the 0/2π boundary
|
||||||
|
if (normalizedStartAngle > normalizedEndAngle)
|
||||||
|
{
|
||||||
|
// Arc crosses 0/2π boundary - check if bottle is in either part of the arc
|
||||||
|
if (!(normalizedBottleAngle >= normalizedStartAngle || normalizedBottleAngle <= normalizedEndAngle))
|
||||||
return 0;
|
return 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Normal case - arc doesn't cross boundary
|
||||||
|
if (normalizedBottleAngle < normalizedStartAngle || normalizedBottleAngle > normalizedEndAngle)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
// Calculate distance from center
|
// Calculate distance from center
|
||||||
float distanceToCenter = centerToBottle.Length();
|
float distanceToCenter = centerToBottle.Length();
|
||||||
// If bottle is outside radius range, return 0
|
// If bottle is outside radius range, return 0
|
||||||
if (distanceToCenter < _innerRadius || distanceToCenter > _outerRadius)
|
if (distanceToCenter < _innerRadius || distanceToCenter > _outerRadius)
|
||||||
return 0;
|
return 0;
|
||||||
// Calculate how far the bottle is from the edges
|
// Calculate how far the bottle is from the edges using normalized angles
|
||||||
float angleFromStart = bottleAngle - _startAngle;
|
float angleFromStart, angleToEnd, totalAngle;
|
||||||
float angleToEnd = _endAngle - bottleAngle;
|
|
||||||
|
if (normalizedStartAngle > normalizedEndAngle)
|
||||||
|
{
|
||||||
|
// Arc crosses 0/2π boundary
|
||||||
|
totalAngle = (2 * (float)Math.PI - normalizedStartAngle) + normalizedEndAngle;
|
||||||
|
if (normalizedBottleAngle >= normalizedStartAngle)
|
||||||
|
{
|
||||||
|
angleFromStart = normalizedBottleAngle - normalizedStartAngle;
|
||||||
|
angleToEnd = (2 * (float)Math.PI - normalizedBottleAngle) + normalizedEndAngle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
angleFromStart = (2 * (float)Math.PI - normalizedStartAngle) + normalizedBottleAngle;
|
||||||
|
angleToEnd = normalizedEndAngle - normalizedBottleAngle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Normal case
|
||||||
|
angleFromStart = normalizedBottleAngle - normalizedStartAngle;
|
||||||
|
angleToEnd = normalizedEndAngle - normalizedBottleAngle;
|
||||||
|
totalAngle = normalizedEndAngle - normalizedStartAngle;
|
||||||
|
}
|
||||||
|
|
||||||
// Use the minimum distance to either edge to calculate overlap
|
// Use the minimum distance to either edge to calculate overlap
|
||||||
float minAngleDistance = Math.Min(angleFromStart, angleToEnd);
|
float minAngleDistance = Math.Min(angleFromStart, angleToEnd);
|
||||||
float totalAngle = _endAngle - _startAngle;
|
|
||||||
// Calculate overlap percentage based on angle proximity to edges
|
// Calculate overlap percentage based on angle proximity to edges
|
||||||
float overlapPercentage = Math.Min(minAngleDistance / (totalAngle * 0.1f), 1.0f);
|
float overlapPercentage = Math.Min(minAngleDistance / (totalAngle * 0.1f), 1.0f);
|
||||||
return overlapPercentage;
|
return overlapPercentage;
|
||||||
|
@ -485,6 +520,8 @@ namespace CtrEditor.Simulacion
|
||||||
_world = world;
|
_world = world;
|
||||||
ListOnTransports = new List<simBase>();
|
ListOnTransports = new List<simBase>();
|
||||||
_deferredActions = deferredActions;
|
_deferredActions = deferredActions;
|
||||||
|
// Asegurar que el diámetro sea al menos 0.01m para evitar errores
|
||||||
|
diameter = Min(diameter, 0.01f);
|
||||||
Radius = diameter / 2;
|
Radius = diameter / 2;
|
||||||
_mass = mass;
|
_mass = mass;
|
||||||
_activeJoint = null;
|
_activeJoint = null;
|
||||||
|
@ -564,6 +601,8 @@ namespace CtrEditor.Simulacion
|
||||||
|
|
||||||
public void SetDiameter(float diameter)
|
public void SetDiameter(float diameter)
|
||||||
{
|
{
|
||||||
|
// Asegurar que el diámetro sea al menos 0.01m para evitar errores
|
||||||
|
diameter = Min(diameter, 0.01f);
|
||||||
Radius = diameter / 2;
|
Radius = diameter / 2;
|
||||||
Create(Body.Position); // Recrear el círculo con el nuevo tamaño
|
Create(Body.Position); // Recrear el círculo con el nuevo tamaño
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue