From 304bdb06d4693678096b5096fd0562c886fc0943 Mon Sep 17 00:00:00 2001 From: Miguel Date: Fri, 7 Mar 2025 11:00:27 +0100 Subject: [PATCH] =?UTF-8?q?Agregado=20posibilidad=20al=20Frame=20de=20move?= =?UTF-8?q?rse=20horizontal=20y=20verticalmente.=20Agregado=20TransporteDu?= =?UTF-8?q?alInverter=20para=20que=20sea=20comandado=20por=20dos=20inverte?= =?UTF-8?q?rs=20seleccionables=20desde=20un=20tag.=20Agregada=20funcionali?= =?UTF-8?q?dad=20de=20cambio=20de=20tama=C3=B1o=20a=20las=20curvas.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controls/PanelEdicionControl.xaml | 6 +- ObjetosSim/Decorativos/ucFramePlate.xaml.cs | 80 +++++- .../Estaticos/ucTransporteCurva.xaml.cs | 71 ++++- .../Estaticos/ucTransporteGuias.xaml.cs | 13 +- .../Estaticos/ucTransporteGuiasUnion.xaml.cs | 16 +- ObjetosSim/Estaticos/ucTransporteTTop.xaml.cs | 11 +- .../ucTransporteTTopDualInverter.xaml | 50 ++++ .../ucTransporteTTopDualInverter.xaml.cs | 267 ++++++++++++++++++ ObjetosSim/UserControlFactory.cs | 3 +- ObjetosSim/osBase.cs | 4 +- 10 files changed, 503 insertions(+), 18 deletions(-) create mode 100644 ObjetosSim/Estaticos/ucTransporteTTopDualInverter.xaml create mode 100644 ObjetosSim/Estaticos/ucTransporteTTopDualInverter.xaml.cs diff --git a/Controls/PanelEdicionControl.xaml b/Controls/PanelEdicionControl.xaml index 0524273..4e3d9e4 100644 --- a/Controls/PanelEdicionControl.xaml +++ b/Controls/PanelEdicionControl.xaml @@ -23,7 +23,7 @@ + Text="{Binding Value, UpdateSourceTrigger=LostFocus}" /> @@ -36,7 +36,7 @@ + Text="{Binding Value, Converter={StaticResource floatFormatter}, UpdateSourceTrigger=LostFocus}" /> @@ -48,7 +48,7 @@ - + diff --git a/ObjetosSim/Decorativos/ucFramePlate.xaml.cs b/ObjetosSim/Decorativos/ucFramePlate.xaml.cs index 62bc2c1..61cad55 100644 --- a/ObjetosSim/Decorativos/ucFramePlate.xaml.cs +++ b/ObjetosSim/Decorativos/ucFramePlate.xaml.cs @@ -56,7 +56,7 @@ namespace CtrEditor.ObjetosSim [ObservableProperty] public float alto_Titulo; - // Encoder + // Encoder X [ObservableProperty] [property: Description("This is a link to a Encoder for X.")] [property: Category("Encoders:")] @@ -73,6 +73,33 @@ namespace CtrEditor.ObjetosSim [property: Category("Encoders:")] public float offset_encoder_X; + // Encoder Y + [ObservableProperty] + [property: Description("This is a link to a Encoder for Y.")] + [property: Category("Encoders:")] + [property: ItemsSource(typeof(osBaseItemsSource))] + private string encoder_Y; + + [ObservableProperty] + [property: Description("K Pulses per meter for Moving")] + [property: Category("Encoders:")] + public float k_encoder_Y; + + [ObservableProperty] + [property: Description("Y in meter offset Top. Position when the encoder is 0")] + [property: Category("Encoders:")] + public float offset_encoder_Y; + + partial void OnK_encoder_YChanged(float value) + { + UpdatePosition(); + } + + partial void OnOffset_encoder_YChanged(float value) + { + UpdatePosition(); + } + partial void OnK_encoder_XChanged(float value) { UpdatePosition(); @@ -86,9 +113,15 @@ namespace CtrEditor.ObjetosSim [JsonIgnore] private osEncoderMotorLineal EncoderX; + [JsonIgnore] + private osEncoderMotorLineal EncoderY; + [JsonIgnore] private bool isUpdatingFromEncoderX = false; + [JsonIgnore] + private bool isUpdatingFromEncoderY = false; + partial void OnEncoder_XChanged(string value) { if (EncoderX != null) @@ -101,6 +134,18 @@ namespace CtrEditor.ObjetosSim } } + partial void OnEncoder_YChanged(string value) + { + if (EncoderY != null) + EncoderY.PropertyChanged -= OnEncoderYPropertyChanged; + if (_mainViewModel != null && value != null && value.Length > 0) + { + EncoderY = (osEncoderMotorLineal)_mainViewModel.ObjetosSimulables.FirstOrDefault(s => (s is osEncoderMotorLineal && s.Nombre == value), null); + if (EncoderY != null) + EncoderY.PropertyChanged += OnEncoderYPropertyChanged; + } + } + private void OnEncoderXPropertyChanged(object sender, PropertyChangedEventArgs e) { if (!isUpdatingFromEncoderX) @@ -108,7 +153,7 @@ namespace CtrEditor.ObjetosSim isUpdatingFromEncoderX = true; // Actualizamos el nombre si este fue modificado if (e.PropertyName == nameof(osEncoderMotorLineal.Nombre)) - Group_Panel = ((osEncoderMotorLineal)sender).Nombre; + Encoder_X = ((osEncoderMotorLineal)sender).Nombre; if (e.PropertyName == nameof(osEncoderMotorLineal.Valor_Actual)) { @@ -119,13 +164,33 @@ namespace CtrEditor.ObjetosSim } } + private void OnEncoderYPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (!isUpdatingFromEncoderY) + { + isUpdatingFromEncoderY = true; + // Actualizamos el nombre si este fue modificado + if (e.PropertyName == nameof(osEncoderMotorLineal.Nombre)) + Encoder_Y = ((osEncoderMotorLineal)sender).Nombre; + + if (e.PropertyName == nameof(osEncoderMotorLineal.Valor_Actual)) + { + UpdatePosition(); + } + + isUpdatingFromEncoderY = false; + } + } + public void UpdatePosition() { - if (EncoderX == null) - return; - if (K_encoder_X == 0) - return; - Left = (EncoderX.Valor_Actual / k_encoder_X) + offset_encoder_X; + // Update X position if encoder is available + if (EncoderX != null && K_encoder_X != 0) + Left = (EncoderX.Valor_Actual / k_encoder_X) + offset_encoder_X; + + // Update Y position if encoder is available + if (EncoderY != null && K_encoder_Y != 0) + Top = (EncoderY.Valor_Actual / k_encoder_Y) + offset_encoder_Y; } public override void TopChanging(float oldValue, float newValue) @@ -158,6 +223,7 @@ namespace CtrEditor.ObjetosSim base.ucLoaded(); // El UserControl se ha cargado OnEncoder_XChanged(Encoder_X); + OnEncoder_YChanged(Encoder_Y); UpdateZIndex(Zindex_FramePlate); } diff --git a/ObjetosSim/Estaticos/ucTransporteCurva.xaml.cs b/ObjetosSim/Estaticos/ucTransporteCurva.xaml.cs index 117be10..1e08de4 100644 --- a/ObjetosSim/Estaticos/ucTransporteCurva.xaml.cs +++ b/ObjetosSim/Estaticos/ucTransporteCurva.xaml.cs @@ -58,9 +58,32 @@ namespace CtrEditor.ObjetosSim [ObservableProperty] private float radioExterno; + + partial void OnRadioExternoChanged(float value) + { + // Update ancho and alto based on radioExterno + Ancho = value * 2; + Alto = value * 2; + + // Ensure radioInterno maintains proper proportion if needed + if (RadioInterno >= value) + { + RadioInterno = value * 0.75f; // Default proportion + } + + ActualizarGeometrias(); + } + [ObservableProperty] private float radioInterno; + + + [ObservableProperty] + [property: Description("Bit to enable Link to Motor")] + [property: Category("PLC link:")] + string tag_ReleActivatedMotor; + [ObservableProperty] [property: Description("Link to Motor")] [property: Category("PLC link:")] @@ -130,11 +153,52 @@ namespace CtrEditor.ObjetosSim [ObservableProperty] public bool esMarcha; + public override void OnResize(float Delta_Width, float Delta_Height) + { + // Calculate the proportional change factor + float widthChangeFactor = (Ancho + Delta_Width) / Ancho; + float heightChangeFactor = (Alto + Delta_Height) / Alto; + + // Use the average or minimum change factor to maintain aspect ratio + float changeFactor = Math.Min(widthChangeFactor, heightChangeFactor); + + // Save the original radiuses for calculating position adjustments + float originalRadioExterno = RadioExterno; + + // Apply the change factor to both radios + RadioExterno *= changeFactor; + RadioInterno *= changeFactor; + + // Calculate position adjustment to keep the component centered + float radiusDifference = RadioExterno - originalRadioExterno; + + // Adjust Left and Top to maintain center position + // We move by negative half the difference because the component expands outward + Left -= radiusDifference; + Top -= radiusDifference; + + // Ensure minimums + if (RadioExterno < 0.1f) + RadioExterno = 0.1f; + + if (RadioInterno < 0.05f) + RadioInterno = 0.05f; + + // Ensure radioInterno is always less than radioExterno + if (RadioInterno >= RadioExterno) + RadioInterno = RadioExterno * 0.75f; + } + + + public osTransporteCurva() { RadioExterno = 1.3f; - RadioInterno = 1; + RadioInterno = 1f; + Ancho = RadioExterno * 2; // Set initial width based on external radius + Alto = RadioExterno * 2; // Set initial height based on external radius Arco_en_grados = 90; + Tag_ReleActivatedMotor = "1"; } public override void UpdateGeometryStart() @@ -149,7 +213,10 @@ namespace CtrEditor.ObjetosSim if (Motor != null) { if (Motor is osVMmotorSim motor) - VelocidadActual = motor.Velocidad; + if (LeerBitTag(Tag_ReleActivatedMotor)) + VelocidadActual = motor.Velocidad; + else + VelocidadActual = 0; } } diff --git a/ObjetosSim/Estaticos/ucTransporteGuias.xaml.cs b/ObjetosSim/Estaticos/ucTransporteGuias.xaml.cs index 29e0ce6..a90d02d 100644 --- a/ObjetosSim/Estaticos/ucTransporteGuias.xaml.cs +++ b/ObjetosSim/Estaticos/ucTransporteGuias.xaml.cs @@ -69,6 +69,11 @@ namespace CtrEditor.ObjetosSim [ObservableProperty] Color color = Colors.Blue; + [ObservableProperty] + [property: Description("Bit to enable Link to Motor")] + [property: Category("PLC link:")] + string tag_ReleActivatedMotor; + [ObservableProperty] [property: Description("Link to Motor")] [property: Category("PLC link:")] @@ -152,6 +157,7 @@ namespace CtrEditor.ObjetosSim Alto = 0.10f; AltoGuia = 0.03f; Distance = 0.01f; + Tag_ReleActivatedMotor = "1"; } public override void UpdateGeometryStart() @@ -159,16 +165,21 @@ namespace CtrEditor.ObjetosSim // Se llama antes de la simulacion ActualizarGeometrias(); } + public override void SimulationStop() { // Se llama al detener la simulacion ActualizarAnimacionStoryBoardTransporte(VelocidadActual); } + public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds) { if (Motor != null) if (Motor is osVMmotorSim id_motor) - VelocidadActual = id_motor.Velocidad; + if (LeerBitTag(Tag_ReleActivatedMotor)) + VelocidadActual = id_motor.Velocidad; + else + VelocidadActual = 0; } public override void ucLoaded() diff --git a/ObjetosSim/Estaticos/ucTransporteGuiasUnion.xaml.cs b/ObjetosSim/Estaticos/ucTransporteGuiasUnion.xaml.cs index e0d2708..d2f686a 100644 --- a/ObjetosSim/Estaticos/ucTransporteGuiasUnion.xaml.cs +++ b/ObjetosSim/Estaticos/ucTransporteGuiasUnion.xaml.cs @@ -41,6 +41,11 @@ namespace CtrEditor.ObjetosSim [ObservableProperty] Color color; + [ObservableProperty] + [property: Description("Bit to enable Link to Motor")] + [property: Category("PLC link:")] + string tag_ReleActivatedMotor; + [ObservableProperty] [property: Description("Link to Motor A")] [property: Category("PLC link:")] @@ -245,6 +250,7 @@ namespace CtrEditor.ObjetosSim Storyboards = new Dictionary(); TransportsDirection = new Dictionary(); TransportsVelocidad = new Dictionary(); + Tag_ReleActivatedMotor = "1"; } public override void UpdateGeometryStart() @@ -268,13 +274,19 @@ namespace CtrEditor.ObjetosSim if (_osMotorA != null) { if (_osMotorA is osVMmotorSim motor) - VelocidadActualA = motor.Velocidad; + if (LeerBitTag(Tag_ReleActivatedMotor)) + VelocidadActualA = motor.Velocidad; + else + VelocidadActualA = 0; } if (_osMotorB != null) { if (_osMotorB is osVMmotorSim motor) - VelocidadActualB = motor.Velocidad; + if (LeerBitTag(Tag_ReleActivatedMotor)) + VelocidadActualB = motor.Velocidad; + else + VelocidadActualB = 0; } } diff --git a/ObjetosSim/Estaticos/ucTransporteTTop.xaml.cs b/ObjetosSim/Estaticos/ucTransporteTTop.xaml.cs index 207d38c..64b659d 100644 --- a/ObjetosSim/Estaticos/ucTransporteTTop.xaml.cs +++ b/ObjetosSim/Estaticos/ucTransporteTTop.xaml.cs @@ -66,6 +66,11 @@ namespace CtrEditor.ObjetosSim ActualizarAnimacionStoryBoardTransporte(VelocidadActual); } + [ObservableProperty] + [property: Description("Bit to enable Link to Motor")] + [property: Category("PLC link:")] + string tag_ReleActivatedMotor; + [ObservableProperty] [property: Description("Link to Motor")] [property: Category("PLC link:")] @@ -131,6 +136,7 @@ namespace CtrEditor.ObjetosSim { Ancho = 1; Alto = 0.10f; + Tag_ReleActivatedMotor = "1"; } public override void SimulationStop() @@ -148,7 +154,10 @@ namespace CtrEditor.ObjetosSim { if (Motor != null) if (Motor is osVMmotorSim motor) - VelocidadActual = motor.Velocidad; + if (LeerBitTag(Tag_ReleActivatedMotor)) + VelocidadActual = motor.Velocidad; + else + VelocidadActual = 0; } public override void ucLoaded() diff --git a/ObjetosSim/Estaticos/ucTransporteTTopDualInverter.xaml b/ObjetosSim/Estaticos/ucTransporteTTopDualInverter.xaml new file mode 100644 index 0000000..d7ed302 --- /dev/null +++ b/ObjetosSim/Estaticos/ucTransporteTTopDualInverter.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ObjetosSim/Estaticos/ucTransporteTTopDualInverter.xaml.cs b/ObjetosSim/Estaticos/ucTransporteTTopDualInverter.xaml.cs new file mode 100644 index 0000000..adc66dc --- /dev/null +++ b/ObjetosSim/Estaticos/ucTransporteTTopDualInverter.xaml.cs @@ -0,0 +1,267 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CtrEditor.FuncionesBase; +using System.Windows; +using System.Windows.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using LibS7Adv; +using CtrEditor.Simulacion; +using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; +using System.ComponentModel; +using CtrEditor.FuncionesBase; +using System.Text.Json.Serialization; + +namespace CtrEditor.ObjetosSim +{ + /// + /// Interaction logic for ucTransporteTTop.xaml + /// + /// + + public partial class osTransporteTTopDualInverter : osBase, IosBase + { + + private simTransporte SimGeometria; + private osVMmotorSim MotorA; + private osVMmotorSim MotorB; + + public static string NombreClase() + { + return "Transporte Dual Inverter"; + } + private string nombre = "Transporte TTOP Dual Inverter"; + + [property: Category("Id:")] + public override string Nombre + { + get => nombre; + set => SetProperty(ref nombre, value); + } + + [ObservableProperty] + [property: Category("Simulation:")] + public float velocidadActual; + + partial void OnVelocidadActualChanged(float value) + { + SetSpeed(); + } + + [ObservableProperty] + [property: Category("Simulation:")] + bool invertirDireccion; + + partial void OnInvertirDireccionChanged(bool value) + { + SetSpeed(); + if (_visualRepresentation is ucTransporteTTop uc) + { + CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion); + ActualizarAnimacionStoryBoardTransporte(VelocidadActual); + } + } + + void SetSpeed() + { + if (InvertirDireccion) + SimGeometria?.SetSpeed(-VelocidadActual); + else + SimGeometria?.SetSpeed(VelocidadActual); + ActualizarAnimacionStoryBoardTransporte(VelocidadActual); + } + + [ObservableProperty] + [property: Description("Bit to enable Link to Inverter A")] + [property: Category("PLC link:")] + string tag_ReleActivatedMotor_A; + + [ObservableProperty] + [property: Description("Bit to enable Link to Inverter B")] + [property: Category("PLC link:")] + string tag_ReleActivatedMotor_B; + + [ObservableProperty] + [property: Description("Link to Inverter A")] + [property: Category("PLC link:")] + [property: ItemsSource(typeof(osBaseItemsSource))] + string id_Motor_A; + + [ObservableProperty] + [property: Description("Link to Inverter B")] + [property: Category("PLC link:")] + [property: ItemsSource(typeof(osBaseItemsSource))] + string id_Motor_B; + + + [JsonIgnore] + private PropertyChangedEventHandler motorPropertyChangedHandler; + + partial void OnId_Motor_AChanged(string value) + { + if (MotorA != null && motorPropertyChangedHandler != null) + MotorA.PropertyChanged -= motorPropertyChangedHandler; + + if (_mainViewModel != null && !string.IsNullOrEmpty(value)) + { + MotorA = (osVMmotorSim)_mainViewModel.ObjetosSimulables.FirstOrDefault(s => s is osVMmotorSim motor && motor.Nombre == value); + if (MotorA != null) + { + motorPropertyChangedHandler = (sender, e) => + { + if (e.PropertyName == nameof(osVMmotorSim.Nombre)) + { + Id_Motor_A = ((osVMmotorSim)sender).Nombre; + } + }; + MotorA.PropertyChanged += motorPropertyChangedHandler; + } + } + } + + partial void OnId_Motor_BChanged(string value) + { + if (MotorB != null && motorPropertyChangedHandler != null) + MotorB.PropertyChanged -= motorPropertyChangedHandler; + + if (_mainViewModel != null && !string.IsNullOrEmpty(value)) + { + MotorB = (osVMmotorSim)_mainViewModel.ObjetosSimulables.FirstOrDefault(s => s is osVMmotorSim motor && motor.Nombre == value); + if (MotorB != null) + { + motorPropertyChangedHandler = (sender, e) => + { + if (e.PropertyName == nameof(osVMmotorSim.Nombre)) + { + Id_Motor_B = ((osVMmotorSim)sender).Nombre; + } + }; + MotorB.PropertyChanged += motorPropertyChangedHandler; + } + } + } + + + [ObservableProperty] + [property: Category("Setup:")] + public float frictionCoefficient; + [ObservableProperty] + [property: Category("Setup:")] + public float velMax50hz; + [ObservableProperty] + [property: Category("Setup:")] + public float tiempoRampa; + [ObservableProperty] + [property: Category("Setup:")] + public bool esMarcha; + + + private void ActualizarGeometrias() + { + if (_visualRepresentation is ucTransporteTTop uc) + { + UpdateRectangle(SimGeometria, uc.Transporte, Alto, Ancho, Angulo); + SetSpeed(); + } + ActualizarAnimacionStoryBoardTransporte(VelocidadActual); + } + + public override void OnMoveResizeRotate() + { + ActualizarGeometrias(); + } + + public osTransporteTTopDualInverter() + { + Ancho = 1; + Alto = 0.10f; + Tag_ReleActivatedMotor_A = "1"; + Tag_ReleActivatedMotor_B = "1"; + } + + public override void SimulationStop() + { + // Se llama al detener la simulacion + ActualizarAnimacionStoryBoardTransporte(VelocidadActual); + } + public override void UpdateGeometryStart() + { + // Se llama antes de la simulacion + ActualizarGeometrias(); + } + + public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds) + { + if (LeerBitTag(Tag_ReleActivatedMotor_A)) + { + if (MotorA != null) + if (MotorA is osVMmotorSim motor) + VelocidadActual = motor.Velocidad; + else + VelocidadActual = 0; + } + else if (LeerBitTag(Tag_ReleActivatedMotor_B)) + { + if (MotorB != null) + if (MotorB is osVMmotorSim motor) + VelocidadActual = motor.Velocidad; + else + VelocidadActual = 0; + } + } + + public override void ucLoaded() + { + // El UserControl ya se ha cargado y podemos obtener las coordenadas para + // crear el objeto de simulacion + base.ucLoaded(); + OnId_Motor_AChanged(Id_Motor_A); // Link Id_Motor = Motor + OnId_Motor_BChanged(Id_Motor_B); // Link Id_Motor = Motor + + if (_visualRepresentation is ucTransporteTTop uc) + { + SimGeometria = AddRectangle(simulationManager, uc.Transporte, Alto, Ancho, Angulo); + CrearAnimacionStoryBoardTrasnporte(uc.Transporte, InvertirDireccion); + } + } + public override void ucUnLoaded() + { + // El UserControl se esta eliminando + // eliminar el objeto de simulacion + simulationManager.Remove(SimGeometria); + } + + } + + public partial class ucTransporteTTopDualInverter : UserControl, IDataContainer + { + public osBase? Datos { get; set; } + public int zIndex_fromFrames { get; set; } + + public ucTransporteTTopDualInverter() + { + InitializeComponent(); + this.Loaded += OnLoaded; + this.Unloaded += OnUnloaded; + } + private void OnLoaded(object sender, RoutedEventArgs e) + { + Datos?.ucLoaded(); + } + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Datos?.ucUnLoaded(); + } + public void Highlight(bool State) { } + public ZIndexEnum ZIndex_Base() + { + return ZIndexEnum.Estaticos; + } + + + } + + +} + + + + diff --git a/ObjetosSim/UserControlFactory.cs b/ObjetosSim/UserControlFactory.cs index 749d96c..2885d98 100644 --- a/ObjetosSim/UserControlFactory.cs +++ b/ObjetosSim/UserControlFactory.cs @@ -94,8 +94,9 @@ namespace CtrEditor.ObjetosSim } - public static void CargarPropiedadesosDatos(object selectedObject, PropertyGrid propertyGrid) + public static void CargarPropiedadesosDatos(osBase selectedObject, PropertyGrid propertyGrid) { + // Limpia las propiedades previas propertyGrid.SelectedObject = null; propertyGrid.PropertyDefinitions.Clear(); diff --git a/ObjetosSim/osBase.cs b/ObjetosSim/osBase.cs index 7bb9c15..51d480c 100644 --- a/ObjetosSim/osBase.cs +++ b/ObjetosSim/osBase.cs @@ -413,7 +413,7 @@ namespace CtrEditor.ObjetosSim using (var engine = new TesseractEngine(tesseractPath, "eng", EngineMode.Default)) { // Configuraciones para mejorar el OCR de una sola letra - engine.SetVariable("tessedit_char_whitelist", " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-."); // Lista blanca de caracteres + engine.SetVariable("tessedit_char_whitelist", " ABCDEFGHIJKLMNÑOPQRSTUVWXYZabcdefghijklmnñopqrstuvwxyz0123456789-."); // Lista blanca de caracteres var result = engine.Process(img); return result.GetText(); } @@ -519,6 +519,8 @@ namespace CtrEditor.ObjetosSim UniqueId cloned_from; [ObservableProperty] + [JsonIgnore] + [property: Hidden] private bool isSelected; private async void TimerCallback(object state)