Compare commits

..

9 Commits

Author SHA1 Message Date
Miguel 99248e9112 Se implementaron mejoras en la gestión de copias de objetos seleccionados, reutilizando la lógica de duplicación para crear copias serializables. Se agregó manejo de errores al intentar copiar objetos y se implementaron nuevas propiedades en la clase osBase para gestionar el punto de pivote en la rotación. Además, se aseguraron validaciones para evitar diámetros inválidos en la simulación, garantizando un comportamiento más robusto en la manipulación de objetos. 2025-06-17 17:35:35 +02:00
Miguel f85c707cfc Se añadieron mejoras en la gestión de la manipulación de objetos rotados, incluyendo métodos para adaptar los cursores y el comportamiento de los handles según la rotación del objeto. Se implementó la transformación de cambios del mouse en coordenadas locales para un redimensionamiento más intuitivo. Además, se documentaron las nuevas funcionalidades en la clase ObjectManipulationManager. 2025-06-14 22:48:00 +02:00
Miguel 88e6de77cb Se implementaron métodos para actualizar la posición relativa de los objetos en función de un FramePlate, incluyendo la gestión de la rotación orbital. Se añadieron propiedades para almacenar la posición relativa y el ángulo inicial, y se mejoró la lógica de actualización de posición al cambiar la posición del FramePlate. Además, se implementó un método para recalcular la posición relativa al mover los objetos, asegurando un comportamiento más preciso en la simulación. 2025-06-14 17:07:05 +02:00
Miguel 94b11cf068 Se implementó un sistema de gestión de historial de deshacer (undo) en la aplicación, permitiendo capturar y restaurar estados de objetos seleccionados. Se añadieron métodos para limpiar el historial y se mejoró la interfaz de usuario para mostrar información sobre el estado del historial de deshacer. Además, se realizaron ajustes en la lógica de manipulación de objetos para asegurar la correcta captura de estados antes de movimientos y redimensionamientos. 2025-06-14 16:47:25 +02:00
Miguel 20467c88ae Se agregaron nuevas propiedades y métodos para gestionar el ángulo de rotación en la clase ucFramePlate, incluyendo la implementación de encoders para la rotación. Se mejoró la actualización de la posición en función del valor del encoder de rotación y se implementaron cambios en la gestión de eventos para reflejar adecuadamente las modificaciones en el ángulo. 2025-06-13 23:50:19 +02:00
Miguel e935efb0cb Se agregó la función CanPasteFromClipboard para validar el contenido del portapapeles antes de pegar objetos, mejorando la gestión de errores y asegurando que solo se seleccionen objetos con representación visual válida. Se implementó un retraso en la selección para permitir el renderizado completo de los objetos pegados. 2025-06-13 23:28:50 +02:00
Miguel 16f5131803 Implementada la funcionalidad de copiar y pegar objetos seleccionados como JSON desde el portapapeles, incluyendo opciones para reemplazar IDs existentes. Se agregó manejo de errores y validación del contenido del portapapeles para asegurar la correcta deserialización de los objetos. 2025-06-13 22:33:13 +02:00
Miguel 883620b69d Implementada la funcionalidad de bloqueo y desbloqueo de objetos seleccionados en el menú contextual: se agregó un submenú para gestionar el estado de bloqueo de los objetos, actualizando visualmente la selección y marcando cambios sin guardar en el modelo de vista. 2025-06-13 22:07:37 +02:00
Miguel 9c4e87be9b Mejora en la normalización de ángulos en la clase Aether: se implementó un manejo más robusto de los ángulos para casos donde el arco cruza el límite 0/2π, asegurando un cálculo preciso de la superposición y la distancia a los bordes. 2025-06-13 20:59:09 +02:00
9 changed files with 1243 additions and 42 deletions

View File

@ -447,6 +447,9 @@ namespace CtrEditor
{
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"
directorioTrabajo = EstadoPersistente.Instance.directorio;
// Limpiar historial de undo al cargar datos iniciales
MainWindow?.ClearUndoHistory();
}
// Crear un nuevo Objeto
@ -794,6 +800,9 @@ namespace CtrEditor
// Restaurar los rectángulos de selección si hay objetos seleccionados
_objectManager.UpdateSelectionVisuals();
// Limpiar historial de undo al detener la simulación
MainWindow?.ClearUndoHistory();
}
/// <summary>
@ -869,6 +878,8 @@ namespace CtrEditor
foreach (var objetoSimulable in ObjetosSimulables)
objetoSimulable.SetPLC(null);
// Limpiar historial de undo al desconectar del PLC
MainWindow?.ClearUndoHistory();
}
private List<osBase> objetosSimulablesLlamados = new List<osBase>();

View File

@ -11,6 +11,11 @@ using System.Windows.Threading;
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
using UserControl = System.Windows.Controls.UserControl;
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
{
@ -407,6 +412,9 @@ namespace CtrEditor
private void MoveSelectedObjects(float deltaX, float deltaY)
{
// Capturar estado antes de mover
_objectManager.CaptureUndoState();
// Mover todos los objetos primero
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)
{
if (e.OriginalSource == ImagenEnTrabajoCanvas)
@ -498,6 +523,11 @@ namespace CtrEditor
debugWindow.Show();
}
public void ClearUndoHistory()
{
_objectManager.ClearUndoHistory();
}
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
// 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(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.PlacementTarget = ImagenEnTrabajoCanvas;
contextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
@ -672,6 +766,21 @@ namespace CtrEditor
_objectManager.RemoveResizeRectangles();
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())
{
const float moveDistance = 0.01f;
@ -699,6 +808,314 @@ namespace CtrEditor
}
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

View File

@ -10,6 +10,58 @@ using Color = System.Windows.Media.Color;
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
{
Left,
@ -27,6 +79,19 @@ namespace CtrEditor
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
{
private enum ResizeMode
@ -76,6 +141,11 @@ namespace CtrEditor
private bool _isRectangleSelectionActive;
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
{
get => _isRectangleSelectionActive;
@ -314,19 +384,22 @@ namespace CtrEditor
// Calcular el offset para posicionar los manejadores fuera del rectángulo
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)>
{
// Esquinas - mover hacia afuera del rectángulo
(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.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
(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.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)
@ -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)
{
var handle = new Rectangle
@ -615,6 +725,9 @@ namespace CtrEditor
return;
}
// Capturar estado antes de iniciar redimensionamiento
CaptureUndoState();
_currentDraggingRectangle = sender as Rectangle;
_lastMousePosition = e.GetPosition(_canvas);
_currentDraggingRectangle.CaptureMouse();
@ -625,25 +738,49 @@ namespace CtrEditor
}
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);
// Determinar qué objetos serán afectados por el movimiento
List<osBase> objectsToMove = new List<osBase>();
if (!isControlPressed)
{
HandleObjectSelection(userControl, datos);
// 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);
}
// Actualizar el estado de objetos bloqueados después de la selección
hasLockedObjects = _selectedObjects.Any(obj => obj.Lock_movement);
// Verificar si alguno de los objetos a mover está bloqueado
bool willMoveLockedObjects = objectsToMove.Any(obj => obj.Lock_movement);
if (!willMoveLockedObjects)
{
// Capturar estado antes de iniciar movimiento
CaptureUndoStateForObjects(objectsToMove);
userControl.CaptureMouse();
_currentDraggingControl = userControl;
_isMovingUserControl = true;
InitializeDrag(e);
}
}
// Solo iniciar el arrastre si no se presionó Ctrl y no hay objetos bloqueados
if (!(Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && !hasLockedObjects)
{
userControl.CaptureMouse();
_currentDraggingControl = userControl;
_isMovingUserControl = true;
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
e.Handled = true;
@ -837,6 +974,39 @@ namespace CtrEditor
}
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
{
@ -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)
{
PurgeDeletedObjects();
@ -876,8 +1061,53 @@ namespace CtrEditor
private void HandleResizeForObject(osBase obj, Point currentPosition, bool activateSizeWidth, bool activateSizeHeight)
{
double widthChange = activateSizeWidth ? currentPosition.X - _startPointUserControl.X : 0;
double heightChange = activateSizeHeight ? currentPosition.Y - _startPointUserControl.Y : 0;
// Calcular el cambio del mouse en coordenadas del canvas
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);
}
@ -929,6 +1159,9 @@ namespace CtrEditor
return; // No alinear si hay objetos bloqueados
}
// Capturar estado antes de alinear
CaptureUndoState();
var alignment = new ObjectAlignment(_selectedObjects, _selectedObjects.FirstOrDefault());
switch (alignmentType)
@ -1090,5 +1323,149 @@ namespace CtrEditor
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;
}
}
}

View File

@ -5,12 +5,15 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:vm="clr-namespace:CtrEditor.ObjetosSim"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:osFramePlate Color_Titulo="Black" Alto_Titulo="0.1" Color="WhiteSmoke" />
</UserControl.DataContext>
<Grid>
<Grid.RenderTransform>
<RotateTransform Angle="{Binding Angulo}"
CenterX="{Binding RenderTransformCenterX}"
CenterY="{Binding RenderTransformCenterY}" />
</Grid.RenderTransform>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@ -24,6 +27,5 @@
Fill="{Binding Color, Converter={StaticResource ColorToBrushConverter}}" Stroke="Blue"
StrokeThickness="0.2"
Visibility="{Binding ShowPlate, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
</UserControl>

View File

@ -24,6 +24,8 @@ namespace CtrEditor.ObjetosSim
public float offsetY;
[JsonIgnore]
public float offsetX;
[JsonIgnore]
public float offsetAngulo;
public static string NombreClase()
{
@ -103,6 +105,29 @@ namespace CtrEditor.ObjetosSim
[property: Category("Encoders:")]
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)
{
UpdatePosition();
@ -123,18 +148,34 @@ namespace CtrEditor.ObjetosSim
UpdatePosition();
}
partial void OnK_encoder_RotationChanged(float value)
{
UpdatePosition();
}
partial void OnOffset_encoder_RotationChanged(float value)
{
UpdatePosition();
}
[JsonIgnore]
private osEncoderMotorLineal EncoderX;
[JsonIgnore]
private osEncoderMotorLineal EncoderY;
[JsonIgnore]
private osEncoderMotorLineal EncoderRotation;
[JsonIgnore]
private bool isUpdatingFromEncoderX = false;
[JsonIgnore]
private bool isUpdatingFromEncoderY = false;
[JsonIgnore]
private bool isUpdatingFromEncoderRotation = false;
partial void OnEncoder_XChanged(string value)
{
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)
{
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()
{
// Update X position if encoder is available
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;
}
// Update Y position if encoder is available
@ -209,6 +280,12 @@ namespace CtrEditor.ObjetosSim
Encoder_Y_Position = EncoderY.Valor_Actual;
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)
@ -220,11 +297,44 @@ namespace CtrEditor.ObjetosSim
offsetX = newValue - oldValue;
}
public override void AnguloChanging(float oldValue, float newValue)
{
offsetAngulo = newValue - oldValue;
}
[ObservableProperty]
[property: Description("Show/Hide the plate background")]
[property: Category("Appearance:")]
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()
{
Ancho = 0.5f;
@ -236,12 +346,30 @@ namespace CtrEditor.ObjetosSim
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()
{
base.ucLoaded();
// El UserControl se ha cargado
OnEncoder_XChanged(Encoder_X);
OnEncoder_YChanged(Encoder_Y);
OnEncoder_RotationChanged(Encoder_Rotation);
UpdateZIndex(Zindex_FramePlate);
}
@ -252,6 +380,63 @@ namespace CtrEditor.ObjetosSim
// 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

View File

@ -51,6 +51,12 @@ namespace CtrEditor.ObjetosSim
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);
}

View File

@ -133,6 +133,12 @@ namespace CtrEditor.ObjetosSim
TiempoRestante -= elapsedMilliseconds / 1000.0f;
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 Y = Top + OffsetTopSalida;

View File

@ -106,7 +106,11 @@ namespace CtrEditor.ObjetosSim
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) { }
[ObservableProperty]
@ -124,7 +128,11 @@ namespace CtrEditor.ObjetosSim
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) { }
@ -160,7 +168,13 @@ namespace CtrEditor.ObjetosSim
{
AnguloChanged(value);
}
partial void OnAnguloChanging(float oldValue, float newValue)
{
AnguloChanging(oldValue, newValue);
}
public virtual void AnguloChanged(float value) { }
public virtual void AnguloChanging(float oldValue, float newValue) { }
public void Resize(double width, double height)
@ -283,6 +297,32 @@ namespace CtrEditor.ObjetosSim
[JsonIgnore]
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)
{
if (FramePlate != null)
@ -294,8 +334,25 @@ namespace CtrEditor.ObjetosSim
{
FramePlate.PropertyChanged += OnFramePlatePropertyChanged;
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)
@ -316,16 +373,25 @@ namespace CtrEditor.ObjetosSim
if (e.PropertyName == nameof(osFramePlate.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;
OnMoveResizeRotate();
UpdateOrbitalPosition();
}
if (e.PropertyName == nameof(osFramePlate.Left))
if (e.PropertyName == nameof(osFramePlate.Angulo))
{
Left += ((osFramePlate)sender).offsetX;
OnMoveResizeRotate();
// Primero actualizar el ángulo del objeto
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))
@ -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)
{
@ -992,6 +1148,8 @@ namespace CtrEditor.ObjetosSim
CanvasSetTopinMeter(Top);
}
public bool LeerBitTag(string Tag)
{
if (this._plc == null) return false;

View File

@ -138,23 +138,58 @@ namespace CtrEditor.Simulacion
Vector2 centerToBottle = bottle.Body.Position - Body.Position;
// Calculate angle of bottle relative to curve center (in radians)
float bottleAngle = (float)Math.Atan2(centerToBottle.Y, centerToBottle.X);
// Normalize angle to be positive
if (bottleAngle < 0)
bottleAngle += 2 * (float)Math.PI;
// If bottle is outside the angle range, return 0
if (bottleAngle < _startAngle || bottleAngle > _endAngle)
return 0;
// Normalize all angles to the same range [0, 2π]
float normalizedBottleAngle = bottleAngle < 0 ? bottleAngle + 2 * (float)Math.PI : bottleAngle;
float normalizedStartAngle = _startAngle < 0 ? _startAngle + 2 * (float)Math.PI : _startAngle;
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;
}
else
{
// Normal case - arc doesn't cross boundary
if (normalizedBottleAngle < normalizedStartAngle || normalizedBottleAngle > normalizedEndAngle)
return 0;
}
// Calculate distance from center
float distanceToCenter = centerToBottle.Length();
// If bottle is outside radius range, return 0
if (distanceToCenter < _innerRadius || distanceToCenter > _outerRadius)
return 0;
// Calculate how far the bottle is from the edges
float angleFromStart = bottleAngle - _startAngle;
float angleToEnd = _endAngle - bottleAngle;
// Calculate how far the bottle is from the edges using normalized angles
float angleFromStart, angleToEnd, totalAngle;
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
float minAngleDistance = Math.Min(angleFromStart, angleToEnd);
float totalAngle = _endAngle - _startAngle;
// Calculate overlap percentage based on angle proximity to edges
float overlapPercentage = Math.Min(minAngleDistance / (totalAngle * 0.1f), 1.0f);
return overlapPercentage;
@ -485,6 +520,8 @@ namespace CtrEditor.Simulacion
_world = world;
ListOnTransports = new List<simBase>();
_deferredActions = deferredActions;
// Asegurar que el diámetro sea al menos 0.01m para evitar errores
diameter = Min(diameter, 0.01f);
Radius = diameter / 2;
_mass = mass;
_activeJoint = null;
@ -564,6 +601,8 @@ namespace CtrEditor.Simulacion
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;
Create(Body.Position); // Recrear el círculo con el nuevo tamaño
}