From 8bc0810784c46055cc530d8ac9b348f60bbb7fc5 Mon Sep 17 00:00:00 2001 From: Yuki Date: Sat, 3 Feb 2024 20:05:16 +1100 Subject: [PATCH 1/6] initial --- Brio.sln | 16 ++++++++++++ Brio/Brio.cs | 4 +++ Brio/Brio.csproj | 1 + Brio/Remote/RemoteService.cs | 49 ++++++++++++++++++++++++++++++++++++ WpfRemote/App.xaml | 9 +++++++ WpfRemote/App.xaml.cs | 17 +++++++++++++ WpfRemote/AssemblyInfo.cs | 10 ++++++++ WpfRemote/Log.cs | 14 +++++++++++ WpfRemote/MainWindow.xaml | 13 ++++++++++ WpfRemote/MainWindow.xaml.cs | 15 +++++++++++ WpfRemote/RemoteService.cs | 42 +++++++++++++++++++++++++++++++ WpfRemote/WpfRemote.csproj | 14 +++++++++++ 12 files changed, 204 insertions(+) create mode 100644 Brio/Remote/RemoteService.cs create mode 100644 WpfRemote/App.xaml create mode 100644 WpfRemote/App.xaml.cs create mode 100644 WpfRemote/AssemblyInfo.cs create mode 100644 WpfRemote/Log.cs create mode 100644 WpfRemote/MainWindow.xaml create mode 100644 WpfRemote/MainWindow.xaml.cs create mode 100644 WpfRemote/RemoteService.cs create mode 100644 WpfRemote/WpfRemote.csproj diff --git a/Brio.sln b/Brio.sln index acfcb1d8..76956c1b 100644 --- a/Brio.sln +++ b/Brio.sln @@ -5,16 +5,32 @@ VisualStudioVersion = 17.8.34316.72 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Brio", "Brio\Brio.csproj", "{6E14631E-8223-427D-8A03-550EEE66B842}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfRemote", "WpfRemote\WpfRemote.csproj", "{7A54ABF2-1053-4446-AA79-4ADB666184D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6E14631E-8223-427D-8A03-550EEE66B842}.Debug|Any CPU.ActiveCfg = Debug|x64 + {6E14631E-8223-427D-8A03-550EEE66B842}.Debug|Any CPU.Build.0 = Debug|x64 {6E14631E-8223-427D-8A03-550EEE66B842}.Debug|x64.ActiveCfg = Debug|x64 {6E14631E-8223-427D-8A03-550EEE66B842}.Debug|x64.Build.0 = Debug|x64 + {6E14631E-8223-427D-8A03-550EEE66B842}.Release|Any CPU.ActiveCfg = Release|x64 + {6E14631E-8223-427D-8A03-550EEE66B842}.Release|Any CPU.Build.0 = Release|x64 {6E14631E-8223-427D-8A03-550EEE66B842}.Release|x64.ActiveCfg = Release|x64 {6E14631E-8223-427D-8A03-550EEE66B842}.Release|x64.Build.0 = Release|x64 + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|x64.Build.0 = Debug|Any CPU + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|Any CPU.Build.0 = Release|Any CPU + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|x64.ActiveCfg = Release|Any CPU + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Brio/Brio.cs b/Brio/Brio.cs index 2c2f3e08..ad23010b 100644 --- a/Brio/Brio.cs +++ b/Brio/Brio.cs @@ -9,6 +9,7 @@ using Brio.Game.Posing; using Brio.Game.World; using Brio.IPC; +using Brio.Remote; using Brio.Resources; using Brio.UI; using Brio.UI.Windows; @@ -137,6 +138,9 @@ private IServiceCollection SetupServices(DalamudServices dalamudServices) serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + // Remote + serviceCollection.AddSingleton(); + // UI serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Brio/Brio.csproj b/Brio/Brio.csproj index cccbe352..f3bfd9e0 100644 --- a/Brio/Brio.csproj +++ b/Brio/Brio.csproj @@ -26,6 +26,7 @@ + diff --git a/Brio/Remote/RemoteService.cs b/Brio/Remote/RemoteService.cs new file mode 100644 index 00000000..1aedc687 --- /dev/null +++ b/Brio/Remote/RemoteService.cs @@ -0,0 +1,49 @@ +using EasyTcp4; +using EasyTcp4.ServerUtils; +using System; + +namespace Brio.Remote; +public class RemoteService : IDisposable +{ + public const int Port = 1200; + + private EasyTcpServer? _server; + + public RemoteService() + { + StartServer(); + } + + public bool StartServer() + { + if(_server != null) + throw new Exception("Attempt to start IPC server while it is already running"); + + _server = new(); + _server.EnableServerKeepAlive(); + _server.OnDataReceive += OnDataReceived; + _server.OnError += (s, e) => Brio.Log.Error(e, "Remote error"); + _server.OnConnect += (s, e) => Brio.Log.Info("Remote client connected"); + _server.OnDisconnect += (s, e) => Brio.Log.Info("Remote client disconnected"); + + _server.Start(Port); + + return _server.IsRunning; + } + + public void Dispose() + { + _server?.Dispose(); + } + + public void Send(byte[] data) + { + _server.SendAll(data); + } + + private void OnDataReceived(object? sender, Message e) + { + byte[] data = e.Data; + Brio.Log.Info($"Received {data.Length} bytes"); + } +} diff --git a/WpfRemote/App.xaml b/WpfRemote/App.xaml new file mode 100644 index 00000000..0163c8d4 --- /dev/null +++ b/WpfRemote/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/WpfRemote/App.xaml.cs b/WpfRemote/App.xaml.cs new file mode 100644 index 00000000..e346ddbf --- /dev/null +++ b/WpfRemote/App.xaml.cs @@ -0,0 +1,17 @@ +namespace Remote +{ + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Data; + using System.Linq; + using System.Threading.Tasks; + using System.Windows; + + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/WpfRemote/AssemblyInfo.cs b/WpfRemote/AssemblyInfo.cs new file mode 100644 index 00000000..8b5504ec --- /dev/null +++ b/WpfRemote/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/WpfRemote/Log.cs b/WpfRemote/Log.cs new file mode 100644 index 00000000..64d28129 --- /dev/null +++ b/WpfRemote/Log.cs @@ -0,0 +1,14 @@ +using System; + +namespace WpfRemote; + +internal static class Log +{ + public static void Information(string message) + { + } + + public static void Error(Exception ex, string message) + { + } +} diff --git a/WpfRemote/MainWindow.xaml b/WpfRemote/MainWindow.xaml new file mode 100644 index 00000000..923cc87b --- /dev/null +++ b/WpfRemote/MainWindow.xaml @@ -0,0 +1,13 @@ + + + diff --git a/WpfRemote/MainWindow.xaml.cs b/WpfRemote/MainWindow.xaml.cs new file mode 100644 index 00000000..da6a0760 --- /dev/null +++ b/WpfRemote/MainWindow.xaml.cs @@ -0,0 +1,15 @@ +namespace WpfRemote; + +using System.Windows; + +public partial class MainWindow : Window +{ + private RemoteService _remoteService; + + public MainWindow() + { + InitializeComponent(); + + _remoteService = new(); + } +} diff --git a/WpfRemote/RemoteService.cs b/WpfRemote/RemoteService.cs new file mode 100644 index 00000000..0c350899 --- /dev/null +++ b/WpfRemote/RemoteService.cs @@ -0,0 +1,42 @@ +using EasyTcp4; +using EasyTcp4.ClientUtils; +using EasyTcp4.ClientUtils.Async; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace WpfRemote; + +internal class RemoteService +{ + public const int Port = Brio.Remote.RemoteService.Port; + + private EasyTcpClient? _client; + + public RemoteService() + { + Task.Run(StartClient); + } + + public async Task StartClient() + { + if (_client != null) + throw new Exception("Attempt to start remote client while it is already running"); + + _client = new(); + _client.OnError += (s, e) => Log.Error(e, "IPC error"); + _client.OnDataReceive += this.OnDataReceived; + + return await _client.ConnectAsync(IPAddress.Loopback, Port); + } + + public void Send(byte[] data) + { + _client.Send(data); + } + + private void OnDataReceived(object? sender, Message e) + { + } +} + diff --git a/WpfRemote/WpfRemote.csproj b/WpfRemote/WpfRemote.csproj new file mode 100644 index 00000000..0988e9ec --- /dev/null +++ b/WpfRemote/WpfRemote.csproj @@ -0,0 +1,14 @@ + + + + WinExe + net7.0-windows + enable + true + + + + + + + From eef3914cc4fe281dc3e1bdbd31ae2b3e994b98fb Mon Sep 17 00:00:00 2001 From: Yuki Date: Sat, 3 Feb 2024 20:21:23 +1100 Subject: [PATCH 2/6] message pack --- Brio/Brio.csproj | 1 + Brio/Remote/Heartbeat.cs | 10 ++++++++++ Brio/Remote/RemoteService.cs | 6 ++++-- WpfRemote/RemoteService.cs | 28 +++++++++++++++++++++++++--- 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 Brio/Remote/Heartbeat.cs diff --git a/Brio/Brio.csproj b/Brio/Brio.csproj index f3bfd9e0..145badf3 100644 --- a/Brio/Brio.csproj +++ b/Brio/Brio.csproj @@ -28,6 +28,7 @@ + diff --git a/Brio/Remote/Heartbeat.cs b/Brio/Remote/Heartbeat.cs new file mode 100644 index 00000000..07ba8b3f --- /dev/null +++ b/Brio/Remote/Heartbeat.cs @@ -0,0 +1,10 @@ +using MessagePack; + +namespace Brio.Remote; + +[MessagePackObject] +public class Heartbeat +{ + [Key(0)] + public int Count { get; set; } +} diff --git a/Brio/Remote/RemoteService.cs b/Brio/Remote/RemoteService.cs index 1aedc687..c831f4bb 100644 --- a/Brio/Remote/RemoteService.cs +++ b/Brio/Remote/RemoteService.cs @@ -1,5 +1,6 @@ using EasyTcp4; using EasyTcp4.ServerUtils; +using MessagePack; using System; namespace Brio.Remote; @@ -43,7 +44,8 @@ public void Send(byte[] data) private void OnDataReceived(object? sender, Message e) { - byte[] data = e.Data; - Brio.Log.Info($"Received {data.Length} bytes"); + object? obj = MessagePackSerializer.Typeless.Deserialize(e.Data); + + Brio.Log.Info($"Received {obj?.GetType()}"); } } diff --git a/WpfRemote/RemoteService.cs b/WpfRemote/RemoteService.cs index 0c350899..075c334b 100644 --- a/WpfRemote/RemoteService.cs +++ b/WpfRemote/RemoteService.cs @@ -1,9 +1,12 @@ -using EasyTcp4; +using Brio.Remote; +using EasyTcp4; using EasyTcp4.ClientUtils; using EasyTcp4.ClientUtils.Async; +using MessagePack; using System; using System.Net; using System.Threading.Tasks; +using System.Windows; namespace WpfRemote; @@ -12,6 +15,7 @@ internal class RemoteService public const int Port = Brio.Remote.RemoteService.Port; private EasyTcpClient? _client; + private int _heartbeatIndex = 0; public RemoteService() { @@ -27,16 +31,34 @@ public async Task StartClient() _client.OnError += (s, e) => Log.Error(e, "IPC error"); _client.OnDataReceive += this.OnDataReceived; - return await _client.ConnectAsync(IPAddress.Loopback, Port); + bool success = await _client.ConnectAsync(IPAddress.Loopback, Port); + + if (success) + _ = Task.Run(HeartbeatTask); + + return success; } - public void Send(byte[] data) + public void Send(object obj) { + byte[] data = MessagePackSerializer.Typeless.Serialize(obj); _client.Send(data); } private void OnDataReceived(object? sender, Message e) { } + + private async Task HeartbeatTask() + { + while(Application.Current != null && _client.IsConnected()) + { + Heartbeat hb = new(); + hb.Count = _heartbeatIndex; + Send(hb); + + await Task.Delay(1000); + } + } } From 5581cd8a9e4f80d8fa35c7e4d61bb3d224f838ab Mon Sep 17 00:00:00 2001 From: Yuki Date: Sat, 3 Feb 2024 21:12:00 +1100 Subject: [PATCH 3/6] proof of concept --- Brio/Remote/BoneMessage.cs | 35 +++++++++++++++++++ Brio/Remote/Configuration.cs | 6 ++++ Brio/Remote/Heartbeat.cs | 3 +- Brio/Remote/RemoteService.cs | 66 ++++++++++++++++++++++++++++++++---- WpfRemote/MainWindow.xaml | 46 ++++++++++++++++++++++++- WpfRemote/MainWindow.xaml.cs | 25 ++++++++++++++ WpfRemote/RemoteService.cs | 13 +++++-- 7 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 Brio/Remote/BoneMessage.cs create mode 100644 Brio/Remote/Configuration.cs diff --git a/Brio/Remote/BoneMessage.cs b/Brio/Remote/BoneMessage.cs new file mode 100644 index 00000000..fcd34258 --- /dev/null +++ b/Brio/Remote/BoneMessage.cs @@ -0,0 +1,35 @@ +using Brio.Game.Posing.Skeletons; +using MessagePack; + +namespace Brio.Remote; + +[MessagePackObject] +public class BoneMessage +{ + [Key(00)] public string? Name { get; set; } + [Key(01)] public string? DisplayName { get; set; } + [Key(02)] public float PositionX { get; set; } + [Key(03)] public float PositionY { get; set; } + [Key(04)] public float PositionZ { get; set; } + [Key(05)] public float ScaleX { get; set; } + [Key(06)] public float ScaleY { get; set; } + [Key(07)] public float ScaleZ { get; set; } + [Key(08)] public float RotationX { get; set; } + [Key(09)] public float RotationY { get; set; } + [Key(10)] public float RotationZ { get; set; } + [Key(11)] public float RotationW { get; set; } + + internal void FromBone(Bone bone) + { + this.PositionX = bone.LastTransform.Position.X; + this.PositionY = bone.LastTransform.Position.Y; + this.PositionZ = bone.LastTransform.Position.Z; + this.ScaleX = bone.LastTransform.Scale.X; + this.ScaleY = bone.LastTransform.Scale.Y; + this.ScaleZ = bone.LastTransform.Scale.Z; + this.RotationX = bone.LastTransform.Rotation.X; + this.RotationY = bone.LastTransform.Rotation.Y; + this.RotationZ = bone.LastTransform.Rotation.Z; + this.RotationW = bone.LastTransform.Rotation.W; + } +} diff --git a/Brio/Remote/Configuration.cs b/Brio/Remote/Configuration.cs new file mode 100644 index 00000000..f7c9bc64 --- /dev/null +++ b/Brio/Remote/Configuration.cs @@ -0,0 +1,6 @@ +namespace Brio.Remote; + +public static class Configuration +{ + public const int Port = 1200; +} diff --git a/Brio/Remote/Heartbeat.cs b/Brio/Remote/Heartbeat.cs index 07ba8b3f..682a5ef0 100644 --- a/Brio/Remote/Heartbeat.cs +++ b/Brio/Remote/Heartbeat.cs @@ -5,6 +5,5 @@ namespace Brio.Remote; [MessagePackObject] public class Heartbeat { - [Key(0)] - public int Count { get; set; } + [Key(00)] public int Count { get; set; } } diff --git a/Brio/Remote/RemoteService.cs b/Brio/Remote/RemoteService.cs index c831f4bb..a11cac4d 100644 --- a/Brio/Remote/RemoteService.cs +++ b/Brio/Remote/RemoteService.cs @@ -1,17 +1,26 @@ -using EasyTcp4; +using Brio.Capabilities.Posing; +using Brio.Entities; +using Brio.Entities.Actor; +using Brio.Game.Posing; +using Brio.Game.Posing.Skeletons; +using EasyTcp4; using EasyTcp4.ServerUtils; using MessagePack; using System; +using System.Threading.Tasks; namespace Brio.Remote; -public class RemoteService : IDisposable +internal class RemoteService : IDisposable { - public const int Port = 1200; + public const int SyncMs = 100; + private readonly EntityManager _entityManager; private EasyTcpServer? _server; - public RemoteService() + public RemoteService(EntityManager entityManager) { + _entityManager = entityManager; + StartServer(); } @@ -27,7 +36,9 @@ public bool StartServer() _server.OnConnect += (s, e) => Brio.Log.Info("Remote client connected"); _server.OnDisconnect += (s, e) => Brio.Log.Info("Remote client disconnected"); - _server.Start(Port); + _server.Start(Configuration.Port); + + Task.Run(Synchronizer); return _server.IsRunning; } @@ -35,17 +46,58 @@ public bool StartServer() public void Dispose() { _server?.Dispose(); + _server = null; } - public void Send(byte[] data) + public void Send(object obj) { + byte[] data = MessagePackSerializer.Typeless.Serialize(obj); _server.SendAll(data); } private void OnDataReceived(object? sender, Message e) { object? obj = MessagePackSerializer.Typeless.Deserialize(e.Data); + } + + private async Task Synchronizer() + { + while(_server != null) + { + await Task.Delay(SyncMs); + + if (_entityManager.SelectedEntity is ActorEntity actor) + { + PosingCapability? posing; + if (actor.TryGetCapability(out posing)) + { + SynchronizePosing(posing); + } + } + } + } - Brio.Log.Info($"Received {obj?.GetType()}"); + private void SynchronizePosing(PosingCapability posing) + { + posing.Selected.Switch( + bone => + { + Bone? realBone = posing.SkeletonPosing.GetBone(bone); + if(realBone != null && realBone.Skeleton.IsValid) + { + BoneMessage boneMessage = new(); + boneMessage.FromBone(realBone); + Send(boneMessage); + } + }, + _ => + { + // Model + }, + _ => + { + // Model + } + ); } } diff --git a/WpfRemote/MainWindow.xaml b/WpfRemote/MainWindow.xaml index 923cc87b..60b4dec4 100644 --- a/WpfRemote/MainWindow.xaml +++ b/WpfRemote/MainWindow.xaml @@ -9,5 +9,49 @@ Width="800" Height="450" mc:Ignorable="d"> - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WpfRemote/MainWindow.xaml.cs b/WpfRemote/MainWindow.xaml.cs index da6a0760..e75fdd38 100644 --- a/WpfRemote/MainWindow.xaml.cs +++ b/WpfRemote/MainWindow.xaml.cs @@ -1,5 +1,6 @@ namespace WpfRemote; +using Brio.Remote; using System.Windows; public partial class MainWindow : Window @@ -11,5 +12,29 @@ public MainWindow() InitializeComponent(); _remoteService = new(); + _remoteService.OnMessageCallback = OnMessage; + } + + private void OnMessage(object obj) + { + if (obj is BoneMessage bm) + { + Dispatcher?.Invoke(() => + { + DisplayBone(bm); + + }); + } + } + + public void DisplayBone(BoneMessage bm) + { + this.PosXText.Text = bm.PositionX.ToString(); + this.PosYText.Text = bm.PositionY.ToString(); + this.PosZText.Text = bm.PositionZ.ToString(); + + this.ScaleXText.Text = bm.ScaleX.ToString(); + this.ScaleYText.Text = bm.ScaleY.ToString(); + this.ScaleZText.Text = bm.ScaleZ.ToString(); } } diff --git a/WpfRemote/RemoteService.cs b/WpfRemote/RemoteService.cs index 075c334b..0730224f 100644 --- a/WpfRemote/RemoteService.cs +++ b/WpfRemote/RemoteService.cs @@ -12,11 +12,11 @@ namespace WpfRemote; internal class RemoteService { - public const int Port = Brio.Remote.RemoteService.Port; - private EasyTcpClient? _client; private int _heartbeatIndex = 0; + public Action? OnMessageCallback; + public RemoteService() { Task.Run(StartClient); @@ -31,7 +31,7 @@ public async Task StartClient() _client.OnError += (s, e) => Log.Error(e, "IPC error"); _client.OnDataReceive += this.OnDataReceived; - bool success = await _client.ConnectAsync(IPAddress.Loopback, Port); + bool success = await _client.ConnectAsync(IPAddress.Loopback, Configuration.Port); if (success) _ = Task.Run(HeartbeatTask); @@ -47,6 +47,13 @@ public void Send(object obj) private void OnDataReceived(object? sender, Message e) { + object? obj = MessagePackSerializer.Typeless.Deserialize(e.Data); + Log.Information($"Received {obj?.GetType()}"); + + if (obj == null) + return; + + OnMessageCallback?.Invoke(obj); } private async Task HeartbeatTask() From 3d8eaf274ae5ce5c076d151434663a338f78d251 Mon Sep 17 00:00:00 2001 From: Yuki Date: Sat, 3 Feb 2024 21:40:44 +1100 Subject: [PATCH 4/6] theme --- WpfRemote/3D/Cylinder.cs | 183 ++++++ WpfRemote/3D/Extensions/EulerExtensions.cs | 53 ++ .../3D/Extensions/QuaternionExtensions.cs | 61 ++ WpfRemote/3D/Lines/Circle.cs | 39 ++ WpfRemote/3D/Lines/Line.cs | 358 +++++++++++ WpfRemote/3D/MathUtils.cs | 445 ++++++++++++++ WpfRemote/3D/Matrix3DStack.cs | 103 ++++ WpfRemote/3D/PrsTransform.cs | 90 +++ WpfRemote/3D/Sphere.cs | 122 ++++ WpfRemote/App.xaml | 152 ++++- WpfRemote/Controls/MultiNumberBox.cs | 283 +++++++++ WpfRemote/Controls/MultiNumberBoxStyles.xaml | 156 +++++ WpfRemote/Controls/NumberBox.xaml | 130 ++++ WpfRemote/Controls/NumberBox.xaml.cs | 556 ++++++++++++++++++ WpfRemote/Controls/RelativeSlider.cs | 68 +++ WpfRemote/Controls/Slider.cs | 96 +++ .../Converters/AbsoluteNumberConverter.cs | 11 + .../AnyBoolIsFalseToBoolMultiConverter.cs | 33 ++ .../Converters/BoolInversionConverter.cs | 14 + WpfRemote/Converters/BoolToIntConverter.cs | 9 + WpfRemote/Converters/ColorToBrushConverter.cs | 13 + WpfRemote/Converters/ConverterBase.cs | 67 +++ WpfRemote/Converters/EnumToBoolConverter.cs | 85 +++ .../Converters/EnumToVisibilityConverter.cs | 20 + .../Converters/FloatToDoubleConverter.cs | 10 + .../GreaterThanToVisibilityConverter.cs | 11 + .../InvertedBoolToVisibilityConverter.cs | 19 + .../IsEmptyToVisibilityConverter.cs | 32 + WpfRemote/Converters/IsZeroToBoolConverter.cs | 51 ++ .../Converters/IsZeroToVisibilityConverter.cs | 19 + .../Converters/LessThanToBoolConverter.cs | 11 + .../LessThanToVisibilityConverter.cs | 11 + WpfRemote/Converters/ListToStringConverter.cs | 32 + WpfRemote/Converters/MultiBoolAndConverter.cs | 27 + .../MultiBoolAndToVisibilityConverter.cs | 30 + WpfRemote/Converters/MultiBoolOrConverter.cs | 27 + .../MultiBoolOrToVisibilityConverter.cs | 28 + .../NotEmptyToVisibilityConverter.cs | 32 + .../Converters/NotNullToBoolConverter.cs | 26 + .../NotNullToVisibilityConverter.cs | 27 + .../Converters/NotZeroToBoolConverter.cs | 19 + .../NotZeroToVisibilityConverter.cs | 19 + WpfRemote/Converters/NullToBoolConverter.cs | 26 + .../Converters/NullToVisibilityConverter.cs | 27 + WpfRemote/Converters/NumberConverter.cs | 37 ++ .../Converters/NumberToThicknessConverter.cs | 59 ++ .../Converters/RadiansToDegreesConverter.cs | 19 + .../StringHasContentToBoolConverter.cs | 19 + .../StringHasContentToVisibilityConverter.cs | 20 + WpfRemote/DependencyProperties/Binder.cs | 65 ++ .../DependencyProperty{TValue}.cs | 28 + .../DependencyProperties/IBind{TValue}.cs | 9 + .../Extensions/DependencyObjectExtensions.cs | 60 ++ WpfRemote/MainWindow.xaml | 47 +- WpfRemote/MainWindow.xaml.cs | 43 +- WpfRemote/Styles/BorderStyles.xaml | 70 +++ WpfRemote/Styles/ButtonStyles.xaml | 231 ++++++++ WpfRemote/Styles/CheckBoxStyles.xaml | 153 +++++ WpfRemote/Styles/ComboBoxStyles.xaml | 221 +++++++ WpfRemote/Styles/ExpanderStyles.xaml | 127 ++++ WpfRemote/Styles/GroupBoxStyles.xaml | 75 +++ WpfRemote/Styles/ListBoxStyles.xaml | 245 ++++++++ WpfRemote/Styles/ProgressBarStyles.xaml | 17 + WpfRemote/Styles/ScrollBarStyles.xaml | 119 ++++ WpfRemote/Styles/SliderStyles.xaml | 217 +++++++ WpfRemote/Styles/TabControlStyles.xaml | 314 ++++++++++ WpfRemote/Styles/TextBlockStyles.xaml | 83 +++ WpfRemote/Styles/TextBoxStyles.xaml | 142 +++++ WpfRemote/Styles/ToggleButtonStyles.xaml | 210 +++++++ WpfRemote/Styles/WindowStyles.xaml | 35 ++ WpfRemote/Themes/Dark.xaml | 33 ++ WpfRemote/Utility/Dispatch.cs | 70 +++ WpfRemote/WpfRemote.csproj | 8 + 73 files changed, 6358 insertions(+), 49 deletions(-) create mode 100644 WpfRemote/3D/Cylinder.cs create mode 100644 WpfRemote/3D/Extensions/EulerExtensions.cs create mode 100644 WpfRemote/3D/Extensions/QuaternionExtensions.cs create mode 100644 WpfRemote/3D/Lines/Circle.cs create mode 100644 WpfRemote/3D/Lines/Line.cs create mode 100644 WpfRemote/3D/MathUtils.cs create mode 100644 WpfRemote/3D/Matrix3DStack.cs create mode 100644 WpfRemote/3D/PrsTransform.cs create mode 100644 WpfRemote/3D/Sphere.cs create mode 100644 WpfRemote/Controls/MultiNumberBox.cs create mode 100644 WpfRemote/Controls/MultiNumberBoxStyles.xaml create mode 100644 WpfRemote/Controls/NumberBox.xaml create mode 100644 WpfRemote/Controls/NumberBox.xaml.cs create mode 100644 WpfRemote/Controls/RelativeSlider.cs create mode 100644 WpfRemote/Controls/Slider.cs create mode 100644 WpfRemote/Converters/AbsoluteNumberConverter.cs create mode 100644 WpfRemote/Converters/AnyBoolIsFalseToBoolMultiConverter.cs create mode 100644 WpfRemote/Converters/BoolInversionConverter.cs create mode 100644 WpfRemote/Converters/BoolToIntConverter.cs create mode 100644 WpfRemote/Converters/ColorToBrushConverter.cs create mode 100644 WpfRemote/Converters/ConverterBase.cs create mode 100644 WpfRemote/Converters/EnumToBoolConverter.cs create mode 100644 WpfRemote/Converters/EnumToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/FloatToDoubleConverter.cs create mode 100644 WpfRemote/Converters/GreaterThanToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/InvertedBoolToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/IsEmptyToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/IsZeroToBoolConverter.cs create mode 100644 WpfRemote/Converters/IsZeroToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/LessThanToBoolConverter.cs create mode 100644 WpfRemote/Converters/LessThanToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/ListToStringConverter.cs create mode 100644 WpfRemote/Converters/MultiBoolAndConverter.cs create mode 100644 WpfRemote/Converters/MultiBoolAndToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/MultiBoolOrConverter.cs create mode 100644 WpfRemote/Converters/MultiBoolOrToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/NotEmptyToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/NotNullToBoolConverter.cs create mode 100644 WpfRemote/Converters/NotNullToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/NotZeroToBoolConverter.cs create mode 100644 WpfRemote/Converters/NotZeroToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/NullToBoolConverter.cs create mode 100644 WpfRemote/Converters/NullToVisibilityConverter.cs create mode 100644 WpfRemote/Converters/NumberConverter.cs create mode 100644 WpfRemote/Converters/NumberToThicknessConverter.cs create mode 100644 WpfRemote/Converters/RadiansToDegreesConverter.cs create mode 100644 WpfRemote/Converters/StringHasContentToBoolConverter.cs create mode 100644 WpfRemote/Converters/StringHasContentToVisibilityConverter.cs create mode 100644 WpfRemote/DependencyProperties/Binder.cs create mode 100644 WpfRemote/DependencyProperties/DependencyProperty{TValue}.cs create mode 100644 WpfRemote/DependencyProperties/IBind{TValue}.cs create mode 100644 WpfRemote/Extensions/DependencyObjectExtensions.cs create mode 100644 WpfRemote/Styles/BorderStyles.xaml create mode 100644 WpfRemote/Styles/ButtonStyles.xaml create mode 100644 WpfRemote/Styles/CheckBoxStyles.xaml create mode 100644 WpfRemote/Styles/ComboBoxStyles.xaml create mode 100644 WpfRemote/Styles/ExpanderStyles.xaml create mode 100644 WpfRemote/Styles/GroupBoxStyles.xaml create mode 100644 WpfRemote/Styles/ListBoxStyles.xaml create mode 100644 WpfRemote/Styles/ProgressBarStyles.xaml create mode 100644 WpfRemote/Styles/ScrollBarStyles.xaml create mode 100644 WpfRemote/Styles/SliderStyles.xaml create mode 100644 WpfRemote/Styles/TabControlStyles.xaml create mode 100644 WpfRemote/Styles/TextBlockStyles.xaml create mode 100644 WpfRemote/Styles/TextBoxStyles.xaml create mode 100644 WpfRemote/Styles/ToggleButtonStyles.xaml create mode 100644 WpfRemote/Styles/WindowStyles.xaml create mode 100644 WpfRemote/Themes/Dark.xaml create mode 100644 WpfRemote/Utility/Dispatch.cs diff --git a/WpfRemote/3D/Cylinder.cs b/WpfRemote/3D/Cylinder.cs new file mode 100644 index 00000000..a1f69c34 --- /dev/null +++ b/WpfRemote/3D/Cylinder.cs @@ -0,0 +1,183 @@ +namespace WpfUtils.Meida3D; + +using System; +using System.Windows.Media.Media3D; + +public class Cylinder : ModelVisual3D +{ + private double radius; + private GeometryModel3D model; + private int slices = 32; + private double length = 1; + + public Cylinder() + { + this.model = new GeometryModel3D(); + this.model.Geometry = this.CalculateMesh(); + this.Content = this.model; + } + + public double Radius + { + get + { + return this.radius; + } + set + { + this.radius = value; + this.model.Geometry = this.CalculateMesh(); + } + } + + public int Slices + { + get + { + return this.slices; + } + set + { + this.slices = value; + this.model.Geometry = this.CalculateMesh(); + } + } + + public double Length + { + get + { + return this.length; + } + set + { + this.length = value; + this.model.Geometry = this.CalculateMesh(); + } + } + + public Material Material + { + get + { + return this.model.Material; + } + + set + { + this.model.Material = value; + } + } + + private MeshGeometry3D CalculateMesh() + { + MeshGeometry3D mesh = new MeshGeometry3D(); + + Vector3D axis = new Vector3D(0, this.length, 0); + Point3D endPoint = new Point3D(0, -(this.Length / 2), 0); + + // Get two vectors perpendicular to the axis. + Vector3D v1; + if ((axis.Z < -0.01) || (axis.Z > 0.01)) + { + v1 = new Vector3D(axis.Z, axis.Z, -axis.X - axis.Y); + } + else + { + v1 = new Vector3D(-axis.Y - axis.Z, axis.X, axis.X); + } + + Vector3D v2 = Vector3D.CrossProduct(v1, axis); + + // Make the vectors have length radius. + v1 *= this.Radius / v1.Length; + v2 *= this.Radius / v2.Length; + + // Make the top end cap. + // Make the end point. + int pt0 = mesh.Positions.Count; // Index of end_point. + mesh.Positions.Add(endPoint); + + // Make the top points. + double theta = 0; + double dtheta = 2 * Math.PI / this.Slices; + for (int i = 0; i < this.Slices; i++) + { + mesh.Positions.Add(endPoint + (Math.Cos(theta) * v1) + (Math.Sin(theta) * v2)); + theta += dtheta; + } + + // Make the top triangles. + int pt1 = mesh.Positions.Count - 1; // Index of last point. + int pt2 = pt0 + 1; // Index of first point. + for (int i = 0; i < this.Slices; i++) + { + mesh.TriangleIndices.Add(pt0); + mesh.TriangleIndices.Add(pt1); + mesh.TriangleIndices.Add(pt2); + pt1 = pt2++; + } + + // Make the bottom end cap. + // Make the end point. + pt0 = mesh.Positions.Count; // Index of end_point2. + Point3D end_point2 = endPoint + axis; + mesh.Positions.Add(end_point2); + + // Make the bottom points. + theta = 0; + for (int i = 0; i < this.Slices; i++) + { + mesh.Positions.Add(end_point2 + (Math.Cos(theta) * v1) + (Math.Sin(theta) * v2)); + theta += dtheta; + } + + // Make the bottom triangles. + theta = 0; + pt1 = mesh.Positions.Count - 1; // Index of last point. + pt2 = pt0 + 1; // Index of first point. + for (int i = 0; i < this.Slices; i++) + { + mesh.TriangleIndices.Add(this.Slices + 1); // end_point2 + mesh.TriangleIndices.Add(pt2); + mesh.TriangleIndices.Add(pt1); + pt1 = pt2++; + } + + // Make the sides. + // Add the points to the mesh. + int first_side_point = mesh.Positions.Count; + theta = 0; + for (int i = 0; i < this.Slices; i++) + { + Point3D p1 = endPoint + (Math.Cos(theta) * v1) + (Math.Sin(theta) * v2); + mesh.Positions.Add(p1); + Point3D p2 = p1 + axis; + mesh.Positions.Add(p2); + theta += dtheta; + } + + // Make the side triangles. + pt1 = mesh.Positions.Count - 2; + pt2 = pt1 + 1; + int pt3 = first_side_point; + int pt4 = pt3 + 1; + for (int i = 0; i < this.Slices; i++) + { + mesh.TriangleIndices.Add(pt1); + mesh.TriangleIndices.Add(pt2); + mesh.TriangleIndices.Add(pt4); + + mesh.TriangleIndices.Add(pt1); + mesh.TriangleIndices.Add(pt4); + mesh.TriangleIndices.Add(pt3); + + pt1 = pt3; + pt3 += 2; + pt2 = pt4; + pt4 += 2; + } + + return mesh; + } +} diff --git a/WpfRemote/3D/Extensions/EulerExtensions.cs b/WpfRemote/3D/Extensions/EulerExtensions.cs new file mode 100644 index 00000000..589033e3 --- /dev/null +++ b/WpfRemote/3D/Extensions/EulerExtensions.cs @@ -0,0 +1,53 @@ +namespace System.Windows.Media.Media3D; + +public static class EulerExtensions +{ + private static double deg2Rad = (Math.PI * 2) / 360; + + /// + /// Convert into a Quaternion assuming the Vector3D represents euler angles. + /// + /// Quaternion from Euler angles. + public static Quaternion ToQuaternion(this Vector3D self) + { + double yaw = self.Y * deg2Rad; + double pitch = self.X * deg2Rad; + double roll = self.Z * deg2Rad; + + double c1 = Math.Cos(yaw / 2); + double s1 = Math.Sin(yaw / 2); + double c2 = Math.Cos(pitch / 2); + double s2 = Math.Sin(pitch / 2); + double c3 = Math.Cos(roll / 2); + double s3 = Math.Sin(roll / 2); + + double c1c2 = c1 * c2; + double s1s2 = s1 * s2; + + double x = (c1c2 * s3) + (s1s2 * c3); + double y = (s1 * c2 * c3) + (c1 * s2 * s3); + double z = (c1 * s2 * c3) - (s1 * c2 * s3); + double w = (c1c2 * c3) - (s1s2 * s3); + + return new Quaternion(x, y, z, w); + } + + public static Vector3D NormalizeAngles(this Vector3D self) + { + self.X = NormalizeAngle(self.X); + self.Y = NormalizeAngle(self.Y); + self.Z = NormalizeAngle(self.Z); + return self; + } + + private static double NormalizeAngle(double angle) + { + while (angle > 360) + angle -= 360; + + while (angle < 0) + angle += 360; + + return angle; + } +} diff --git a/WpfRemote/3D/Extensions/QuaternionExtensions.cs b/WpfRemote/3D/Extensions/QuaternionExtensions.cs new file mode 100644 index 00000000..f7ca956b --- /dev/null +++ b/WpfRemote/3D/Extensions/QuaternionExtensions.cs @@ -0,0 +1,61 @@ +namespace System.Windows.Media.Media3D; + +public static class QuaternionExtensions +{ + private static readonly double Rad2Deg = 360 / (Math.PI * 2); + + /// + /// Converts quaternion to euler angles. + /// + /// Quaternion to convert. + /// Vector3D as euler angles. + public static Vector3D ToEulerAngles(this Quaternion q1) + { + Vector3D v = default; + + double test = (q1.X * q1.Y) + (q1.Z * q1.W); + + if (test > 0.4995f) + { + v.Y = 2f * Math.Atan2(q1.X, q1.Y); + v.X = Math.PI / 2; + v.Z = 0; + return NormalizeAngles(v * Rad2Deg); + } + + if (test < -0.4995f) + { + v.Y = -2f * Math.Atan2(q1.X, q1.W); + v.X = -Math.PI / 2; + v.Z = 0; + return NormalizeAngles(v * Rad2Deg); + } + + double sqx = q1.X * q1.X; + double sqy = q1.Y * q1.Y; + double sqz = q1.Z * q1.Z; + + v.Y = Math.Atan2((2 * q1.Y * q1.W) - (2 * q1.X * q1.Z), 1 - (2 * sqy) - (2 * sqz)); + v.X = Math.Asin(2 * test); + v.Z = Math.Atan2((2 * q1.X * q1.W) - (2 * q1.Y * q1.Z), 1 - (2 * sqx) - (2 * sqz)); + + return NormalizeAngles(v * Rad2Deg); + } + + private static Vector3D NormalizeAngles(Vector3D angles) + { + angles.X = NormalizeAngle(angles.X); + angles.Y = NormalizeAngle(angles.Y); + angles.Z = NormalizeAngle(angles.Z); + return angles; + } + + private static double NormalizeAngle(double angle) + { + while (angle > 360) + angle -= 360; + while (angle < 0) + angle += 360; + return angle; + } +} diff --git a/WpfRemote/3D/Lines/Circle.cs b/WpfRemote/3D/Lines/Circle.cs new file mode 100644 index 00000000..8cbb2fb4 --- /dev/null +++ b/WpfRemote/3D/Lines/Circle.cs @@ -0,0 +1,39 @@ +namespace WpfUtils.Meida3D.Lines; + +using System; +using System.Windows.Media.Media3D; + +public class Circle : Line +{ + private double radius; + + public Circle() + { + this.Generate(); + } + + public double Radius + { + get + { + return this.radius; + } + set + { + this.radius = value; + this.Generate(); + } + } + + public void Generate() + { + this.Points.Clear(); + + double angleStep = MathUtils.DegreesToRadians(1); + for (int i = 0; i < 360; i++) + { + this.Points.Add(new Point3D(Math.Cos(angleStep * i) * this.Radius, 0.0, Math.Sin(angleStep * i) * this.Radius)); + this.Points.Add(new Point3D(Math.Cos(angleStep * (i + 1)) * this.Radius, 0.0, Math.Sin(angleStep * (i + 1)) * this.Radius)); + } + } +} diff --git a/WpfRemote/3D/Lines/Line.cs b/WpfRemote/3D/Lines/Line.cs new file mode 100644 index 00000000..b503a6b9 --- /dev/null +++ b/WpfRemote/3D/Lines/Line.cs @@ -0,0 +1,358 @@ +namespace WpfUtils.Meida3D; + +using System; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Media3D; + +public class Line : ModelVisual3D, IDisposable +{ + public static readonly DependencyProperty ColorProperty = DependencyProperty.Register(nameof(Color), typeof(Color), typeof(Line), new PropertyMetadata(Colors.White, OnColorChanged)); + public static readonly DependencyProperty ThicknessProperty = DependencyProperty.Register(nameof(Thickness), typeof(double), typeof(Line), new PropertyMetadata(1.0, OnThicknessChanged)); + public static readonly DependencyProperty PointsProperty = DependencyProperty.Register(nameof(Points), typeof(Point3DCollection), typeof(Line), new PropertyMetadata(null, OnPointsChanged)); + + private readonly GeometryModel3D model; + private readonly MeshGeometry3D mesh; + + private Matrix3D visualToScreen; + private Matrix3D screenToVisual; + + public Line() + { + this.mesh = new MeshGeometry3D(); + this.model = new GeometryModel3D(); + this.model.Geometry = this.mesh; + this.SetColor(this.Color); + + this.Content = this.model; + this.Points = new Point3DCollection(); + + CompositionTarget.Rendering += this.OnRender; + } + + public Color Color + { + get { return (Color)this.GetValue(ColorProperty); } + set { this.SetValue(ColorProperty, value); } + } + + public double Thickness + { + get { return (double)this.GetValue(ThicknessProperty); } + set { this.SetValue(ThicknessProperty, value); } + } + + public Point3DCollection Points + { + get { return (Point3DCollection)this.GetValue(PointsProperty); } + set { this.SetValue(PointsProperty, value); } + } + + public void Dispose() + { + CompositionTarget.Rendering -= this.OnRender; + + this.Points.Clear(); + this.Children.Clear(); + this.Content = null; + } + + public void MakeWireframe(Model3D model) + { + this.Points.Clear(); + + if (model == null) + { + return; + } + + Matrix3DStack transform = new Matrix3DStack(); + transform.Push(Matrix3D.Identity); + + this.WireframeHelper(model, transform); + } + + public Point3D? NearestPoint2D(Point3D cameraPoint) + { + double closest = double.MaxValue; + Point3D? closestPoint = null; + + Matrix3D matrix; + if (!MathUtils.ToViewportTransform(this, out matrix)) + return null; + + MatrixTransform3D transform = new MatrixTransform3D(matrix); + + foreach (Point3D point in this.Points) + { + Point3D cameraSpacePoint = transform.Transform(point); + cameraSpacePoint.Z = cameraSpacePoint.Z * 100; + + Vector3D dir = cameraPoint - cameraSpacePoint; + if (dir.Length < closest) + { + closest = dir.Length; + closestPoint = point; + } + } + + return closestPoint; + } + + private static void OnColorChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + { + ((Line)sender).SetColor((Color)args.NewValue); + } + + private static void OnThicknessChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + { + ((Line)sender).GeometryDirty(); + } + + private static void OnPointsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + { + ((Line)sender).GeometryDirty(); + } + + private void SetColor(Color color) + { + MaterialGroup unlitMaterial = new MaterialGroup(); + unlitMaterial.Children.Add(new DiffuseMaterial(new SolidColorBrush(Colors.Black))); + unlitMaterial.Children.Add(new EmissiveMaterial(new SolidColorBrush(color))); + unlitMaterial.Freeze(); + + this.model.Material = unlitMaterial; + this.model.BackMaterial = unlitMaterial; + } + + private void OnRender(object? sender, EventArgs e) + { + if (this.Points.Count == 0 && this.mesh.Positions.Count == 0) + return; + + if (this.UpdateTransforms()) + { + this.RebuildGeometry(); + } + } + + private void GeometryDirty() + { + // Force next call to UpdateTransforms() to return true. + this.visualToScreen = MathUtils.ZeroMatrix; + } + + private void RebuildGeometry() + { + double halfThickness = this.Thickness / 2.0; + int numLines = this.Points.Count / 2; + + Point3DCollection positions = new Point3DCollection(numLines * 4); + + for (int i = 0; i < numLines; i++) + { + int startIndex = i * 2; + + Point3D startPoint = this.Points[startIndex]; + Point3D endPoint = this.Points[startIndex + 1]; + + this.AddSegment(positions, startPoint, endPoint, halfThickness); + } + + positions.Freeze(); + this.mesh.Positions = positions; + + Int32Collection indices = new Int32Collection(this.Points.Count * 3); + + for (int i = 0; i < this.Points.Count / 2; i++) + { + indices.Add((i * 4) + 2); + indices.Add((i * 4) + 1); + indices.Add((i * 4) + 0); + + indices.Add((i * 4) + 2); + indices.Add((i * 4) + 3); + indices.Add((i * 4) + 1); + } + + indices.Freeze(); + this.mesh.TriangleIndices = indices; + } + + private void AddSegment(Point3DCollection positions, Point3D startPoint, Point3D endPoint, double halfThickness) + { + // NOTE: We want the vector below to be perpendicular post projection so + // we need to compute the line direction in post-projective space. + Vector3D lineDirection = (endPoint * this.visualToScreen) - (startPoint * this.visualToScreen); + lineDirection.Z = 0; + lineDirection.Normalize(); + + // NOTE: Implicit Rot(90) during construction to get a perpendicular vector. + Vector delta = new Vector(-lineDirection.Y, lineDirection.X); + delta *= halfThickness; + + Point3D pOut1, pOut2; + + this.Widen(startPoint, delta, out pOut1, out pOut2); + + positions.Add(pOut1); + positions.Add(pOut2); + + this.Widen(endPoint, delta, out pOut1, out pOut2); + + positions.Add(pOut1); + positions.Add(pOut2); + } + + private void Widen(Point3D pIn, Vector delta, out Point3D pOut1, out Point3D pOut2) + { + Point4D pIn4 = (Point4D)pIn; + Point4D pOut41 = pIn4 * this.visualToScreen; + Point4D pOut42 = pOut41; + + pOut41.X += delta.X * pOut41.W; + pOut41.Y += delta.Y * pOut41.W; + + pOut42.X -= delta.X * pOut42.W; + pOut42.Y -= delta.Y * pOut42.W; + + pOut41 *= this.screenToVisual; + pOut42 *= this.screenToVisual; + + // NOTE: Z is not modified above, so we use the original Z below. + pOut1 = new Point3D( + pOut41.X / pOut41.W, + pOut41.Y / pOut41.W, + pOut41.Z / pOut41.W); + + pOut2 = new Point3D( + pOut42.X / pOut42.W, + pOut42.Y / pOut42.W, + pOut42.Z / pOut42.W); + } + + private bool UpdateTransforms() + { + Viewport3DVisual? viewport; + bool success; + + Matrix3D visualToScreen = MathUtils.TryTransformTo2DAncestor(this, out viewport, out success); + + if (!success || !visualToScreen.HasInverse) + { + this.mesh.Positions = null; + return false; + } + + if (visualToScreen == this.visualToScreen) + { + return false; + } + + this.visualToScreen = this.screenToVisual = visualToScreen; + this.screenToVisual.Invert(); + + return true; + } + + private void WireframeHelper(Model3D model, Matrix3DStack matrixStack) + { + Transform3D transform = model.Transform; + + if (transform != null && transform != Transform3D.Identity) + { + matrixStack.Prepend(model.Transform.Value); + } + + try + { + Model3DGroup? group = model as Model3DGroup; + + if (group != null) + { + this.WireframeHelper(group, matrixStack); + return; + } + + GeometryModel3D? geometry = model as GeometryModel3D; + + if (geometry != null) + { + this.WireframeHelper(geometry, matrixStack); + return; + } + } + finally + { + if (transform != null && transform != Transform3D.Identity) + { + matrixStack.Pop(); + } + } + } + + private void WireframeHelper(Model3DGroup group, Matrix3DStack matrixStack) + { + foreach (Model3D child in group.Children) + { + this.WireframeHelper(child, matrixStack); + } + } + + private void WireframeHelper(GeometryModel3D model, Matrix3DStack matrixStack) + { + Geometry3D geometry = model.Geometry; + MeshGeometry3D? mesh = geometry as MeshGeometry3D; + + if (mesh != null) + { + Point3D[] positions = new Point3D[mesh.Positions.Count]; + mesh.Positions.CopyTo(positions, 0); + matrixStack.Peek().Transform(positions); + + Int32Collection indices = mesh.TriangleIndices; + + if (indices.Count > 0) + { + int limit = positions.Length - 1; + + for (int i = 2, count = indices.Count; i < count; i += 3) + { + int i0 = indices[i - 2]; + int i1 = indices[i - 1]; + int i2 = indices[i]; + + // WPF halts rendering on the first deformed triangle. We should + // do the same. + if ((i0 < 0 || i0 > limit) || (i1 < 0 || i1 > limit) || (i2 < 0 || i2 > limit)) + { + break; + } + + this.AddTriangle(positions, i0, i1, i2); + } + } + else + { + for (int i = 2, count = positions.Length; i < count; i += 3) + { + int i0 = i - 2; + int i1 = i - 1; + int i2 = i; + + this.AddTriangle(positions, i0, i1, i2); + } + } + } + } + + private void AddTriangle(Point3D[] positions, int i0, int i1, int i2) + { + this.Points.Add(positions[i0]); + this.Points.Add(positions[i1]); + this.Points.Add(positions[i1]); + this.Points.Add(positions[i2]); + this.Points.Add(positions[i2]); + this.Points.Add(positions[i0]); + } +} diff --git a/WpfRemote/3D/MathUtils.cs b/WpfRemote/3D/MathUtils.cs new file mode 100644 index 00000000..89164a10 --- /dev/null +++ b/WpfRemote/3D/MathUtils.cs @@ -0,0 +1,445 @@ +namespace WpfUtils.Meida3D; + +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Media3D; + +public static class MathUtils +{ + public static readonly Matrix3D ZeroMatrix = new Matrix3D(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + public static readonly Vector3D XAxis = new Vector3D(1, 0, 0); + public static readonly Vector3D YAxis = new Vector3D(0, 1, 0); + public static readonly Vector3D ZAxis = new Vector3D(0, 0, 1); + + public static double GetAspectRatio(Size size) + { + return size.Width / size.Height; + } + + public static double DegreesToRadians(double degrees) + { + return degrees * (Math.PI / 180.0); + } + + public static double RadiansToDegrees(double radians) + { + return radians * (180 / Math.PI); + } + + /// + /// Computes the effective view matrix for the given + /// camera. + /// + public static Matrix3D GetViewMatrix(Camera camera) + { + if (camera == null) + { + throw new ArgumentNullException("camera"); + } + + ProjectionCamera? projectionCamera = camera as ProjectionCamera; + + if (projectionCamera != null) + { + return GetViewMatrix(projectionCamera); + } + + MatrixCamera? matrixCamera = camera as MatrixCamera; + + if (matrixCamera != null) + { + return matrixCamera.ViewMatrix; + } + + throw new ArgumentException(string.Format("Unsupported camera type '{0}'.", camera.GetType().FullName), "camera"); + } + + /// + /// Computes the effective projection matrix for the given + /// camera. + /// + public static Matrix3D GetProjectionMatrix(Camera camera, double aspectRatio) + { + if (camera == null) + { + throw new ArgumentNullException("camera"); + } + + PerspectiveCamera? perspectiveCamera = camera as PerspectiveCamera; + + if (perspectiveCamera != null) + { + return GetProjectionMatrix(perspectiveCamera, aspectRatio); + } + + OrthographicCamera? orthographicCamera = camera as OrthographicCamera; + + if (orthographicCamera != null) + { + return GetProjectionMatrix(orthographicCamera, aspectRatio); + } + + MatrixCamera? matrixCamera = camera as MatrixCamera; + + if (matrixCamera != null) + { + return matrixCamera.ProjectionMatrix; + } + + throw new ArgumentException(string.Format("Unsupported camera type '{0}'.", camera.GetType().FullName), "camera"); + } + + public static bool ToViewportTransform(DependencyObject visual, out Matrix3D matrix) + { + matrix = Matrix3D.Identity; + + Viewport3DVisual? viewportVisual; + Matrix3D toWorld = GetWorldTransformationMatrix(visual, out viewportVisual); + + bool success; + Matrix3D toViewport = TryWorldToViewportTransform(viewportVisual, out success); + + if (!success) + return false; + + toWorld.Append(toViewport); + matrix = toWorld; + return true; + } + + /// + /// Computes the transform from world space to the Viewport3DVisual's + /// inner 2D space. + /// This method can fail if Camera.Transform is non-invertable + /// in which case the camera clip planes will be coincident and + /// nothing will render. In this case success will be false. + /// + public static Matrix3D TryWorldToViewportTransform(Viewport3DVisual? visual, out bool success) + { + success = false; + Matrix3D result = TryWorldToCameraTransform(visual, out success); + + if (visual != null && success) + { + result.Append(GetProjectionMatrix(visual.Camera, MathUtils.GetAspectRatio(visual.Viewport.Size))); + result.Append(GetHomogeneousToViewportTransform(visual.Viewport)); + success = true; + } + + return result; + } + + /// + /// Computes the transform from world space to camera space + /// This method can fail if Camera.Transform is non-invertable + /// in which case the camera clip planes will be coincident and + /// nothing will render. In this case success will be false. + /// + public static Matrix3D TryWorldToCameraTransform(Viewport3DVisual? visual, out bool success) + { + success = false; + + if (visual == null) + return ZeroMatrix; + + Matrix3D result = Matrix3D.Identity; + + Camera camera = visual.Camera; + + if (camera == null) + { + return ZeroMatrix; + } + + Rect viewport = visual.Viewport; + + if (viewport == Rect.Empty) + { + return ZeroMatrix; + } + + Transform3D cameraTransform = camera.Transform; + + if (cameraTransform != null) + { + Matrix3D m = cameraTransform.Value; + + if (!m.HasInverse) + { + return ZeroMatrix; + } + + m.Invert(); + result.Append(m); + } + + result.Append(GetViewMatrix(camera)); + + success = true; + return result; + } + + /// + /// Computes the transform from the inner space of the given + /// Visual3D to the 2D space of the Viewport3DVisual which + /// contains it. + /// The result will contain the transform of the given visual. + /// This method can fail if Camera.Transform is non-invertable + /// in which case the camera clip planes will be coincident and + /// nothing will render. In this case success will be false. + /// + public static Matrix3D TryTransformTo2DAncestor(DependencyObject visual, out Viewport3DVisual? viewport, out bool success) + { + Matrix3D to2D = GetWorldTransformationMatrix(visual, out viewport); + + if (viewport == null) + { + success = false; + return ZeroMatrix; + } + + to2D.Append(MathUtils.TryWorldToViewportTransform(viewport, out success)); + + if (!success) + { + return ZeroMatrix; + } + + return to2D; + } + + /// + /// Computes the transform from the inner space of the given + /// Visual3D to the camera coordinate space + /// The result will contain the transform of the given visual. + /// This method can fail if Camera.Transform is non-invertable + /// in which case the camera clip planes will be coincident and + /// nothing will render. In this case success will be false. + /// + public static Matrix3D TryTransformToCameraSpace(DependencyObject visual, out Viewport3DVisual? viewport, out bool success) + { + Matrix3D toViewSpace = GetWorldTransformationMatrix(visual, out viewport); + toViewSpace.Append(MathUtils.TryWorldToCameraTransform(viewport, out success)); + + if (!success) + { + return ZeroMatrix; + } + + return toViewSpace; + } + + /// + /// Transforms the axis-aligned bounding box 'bounds' by + /// 'transform'. + /// + /// The AABB to transform. + /// the transform. + /// Transformed AABB. + public static Rect3D TransformBounds(Rect3D bounds, Matrix3D transform) + { + double x1 = bounds.X; + double y1 = bounds.Y; + double z1 = bounds.Z; + double x2 = bounds.X + bounds.SizeX; + double y2 = bounds.Y + bounds.SizeY; + double z2 = bounds.Z + bounds.SizeZ; + + Point3D[] points = new Point3D[] + { + new Point3D(x1, y1, z1), + new Point3D(x1, y1, z2), + new Point3D(x1, y2, z1), + new Point3D(x1, y2, z2), + new Point3D(x2, y1, z1), + new Point3D(x2, y1, z2), + new Point3D(x2, y2, z1), + new Point3D(x2, y2, z2), + }; + + transform.Transform(points); + + // reuse the 1 and 2 variables to stand for smallest and largest + Point3D p = points[0]; + x1 = x2 = p.X; + y1 = y2 = p.Y; + z1 = z2 = p.Z; + + for (int i = 1; i < points.Length; i++) + { + p = points[i]; + + x1 = Math.Min(x1, p.X); + y1 = Math.Min(y1, p.Y); + z1 = Math.Min(z1, p.Z); + x2 = Math.Max(x2, p.X); + y2 = Math.Max(y2, p.Y); + z2 = Math.Max(z2, p.Z); + } + + return new Rect3D(x1, y1, z1, x2 - x1, y2 - y1, z2 - z1); + } + + /// + /// Normalizes v if |v| > 0. + /// This normalization is slightly different from Vector3D.Normalize. Here + /// we just divide by the length but Vector3D.Normalize tries to avoid + /// overflow when finding the length. + /// + /// The vector to normalize. + /// 'true' if v was normalized. + public static bool TryNormalize(ref Vector3D v) + { + double length = v.Length; + + if (length != 0) + { + v /= length; + return true; + } + + return false; + } + + /// + /// Computes the center of 'box'. + /// + /// The Rect3D we want the center of. + /// The center point. + public static Point3D GetCenter(Rect3D box) + { + return new Point3D(box.X + (box.SizeX / 2), box.Y + (box.SizeY / 2), box.Z + (box.SizeZ / 2)); + } + + public static Point3D NearestPointOnRay(Point3D rayOrigin, Vector3D rayDirection, Point3D pnt) + { + rayDirection.Normalize(); + Vector3D v = pnt - rayOrigin; + double d = Vector3D.DotProduct(v, rayDirection); + return rayOrigin + (rayDirection * d); + } + + private static Matrix3D GetViewMatrix(ProjectionCamera camera) + { + Debug.Assert(camera != null, "Caller needs to ensure camera is non-null."); + + // This math is identical to what you find documented for + // D3DXMatrixLookAtRH with the exception that WPF uses a + // LookDirection vector rather than a LookAt point. + Vector3D zAxis = -camera.LookDirection; + zAxis.Normalize(); + + Vector3D xAxis = Vector3D.CrossProduct(camera.UpDirection, zAxis); + xAxis.Normalize(); + + Vector3D yAxis = Vector3D.CrossProduct(zAxis, xAxis); + + Vector3D position = (Vector3D)camera.Position; + double offsetX = -Vector3D.DotProduct(xAxis, position); + double offsetY = -Vector3D.DotProduct(yAxis, position); + double offsetZ = -Vector3D.DotProduct(zAxis, position); + + return new Matrix3D(xAxis.X, yAxis.X, zAxis.X, 0, xAxis.Y, yAxis.Y, zAxis.Y, 0, xAxis.Z, yAxis.Z, zAxis.Z, 0, offsetX, offsetY, offsetZ, 1); + } + + private static Matrix3D GetProjectionMatrix(OrthographicCamera camera, double aspectRatio) + { + Debug.Assert(camera != null, "Caller needs to ensure camera is non-null."); + + // This math is identical to what you find documented for + // D3DXMatrixOrthoRH with the exception that in WPF only + // the camera's width is specified. Height is calculated + // from width and the aspect ratio. + double w = camera.Width; + double h = w / aspectRatio; + double zn = camera.NearPlaneDistance; + double zf = camera.FarPlaneDistance; + + double m33 = 1 / (zn - zf); + double m43 = zn * m33; + + return new Matrix3D(2 / w, 0, 0, 0, 0, 2 / h, 0, 0, 0, 0, m33, 0, 0, 0, m43, 1); + } + + private static Matrix3D GetProjectionMatrix(PerspectiveCamera camera, double aspectRatio) + { + Debug.Assert(camera != null, "Caller needs to ensure camera is non-null."); + + // This math is identical to what you find documented for + // D3DXMatrixPerspectiveFovRH with the exception that in + // WPF the camera's horizontal rather the vertical + // field-of-view is specified. + double hFoV = MathUtils.DegreesToRadians(camera.FieldOfView); + double zn = camera.NearPlaneDistance; + double zf = camera.FarPlaneDistance; + + double xScale = 1 / Math.Tan(hFoV / 2); + double yScale = aspectRatio * xScale; + double m33 = (zf == double.PositiveInfinity) ? -1 : (zf / (zn - zf)); + double m43 = zn * m33; + + return new Matrix3D(xScale, 0, 0, 0, 0, yScale, 0, 0, 0, 0, m33, -1, 0, 0, m43, 0); + } + + private static Matrix3D GetHomogeneousToViewportTransform(Rect viewport) + { + double scaleX = viewport.Width / 2; + double scaleY = viewport.Height / 2; + double offsetX = viewport.X + scaleX; + double offsetY = viewport.Y + scaleY; + + return new Matrix3D(scaleX, 0, 0, 0, 0, -scaleY, 0, 0, 0, 0, 1, 0, offsetX, offsetY, 0, 1); + } + + /// + /// Gets the object space to world space transformation for the given DependencyObject. + /// + /// The visual whose world space transform should be found. + /// The Viewport3DVisual the Visual is contained within. + /// The world space transformation. + private static Matrix3D GetWorldTransformationMatrix(DependencyObject visual, out Viewport3DVisual? viewport) + { + Matrix3D worldTransform = Matrix3D.Identity; + viewport = null; + + if (!(visual is Visual3D)) + { + throw new ArgumentException("Must be of type Visual3D.", "visual"); + } + + while (visual != null) + { + if (!(visual is ModelVisual3D)) + { + break; + } + + Transform3D transform = (Transform3D)visual.GetValue(ModelVisual3D.TransformProperty); + + if (transform != null) + { + worldTransform.Append(transform.Value); + } + + visual = VisualTreeHelper.GetParent(visual); + } + + viewport = visual as Viewport3DVisual; + + if (viewport == null) + { + if (visual != null) + { + // In WPF 3D v1 the only possible configuration is a chain of + // ModelVisual3Ds leading up to a Viewport3DVisual. + throw new ApplicationException(string.Format("Unsupported type: '{0}'. Expected tree of ModelVisual3Ds leading up to a Viewport3DVisual.", visual.GetType().FullName)); + } + + return ZeroMatrix; + } + + return worldTransform; + } +} diff --git a/WpfRemote/3D/Matrix3DStack.cs b/WpfRemote/3D/Matrix3DStack.cs new file mode 100644 index 00000000..d593929a --- /dev/null +++ b/WpfRemote/3D/Matrix3DStack.cs @@ -0,0 +1,103 @@ +namespace WpfUtils.Meida3D; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Windows.Media.Media3D; + +/// +/// Matrix3DStack is a stack of Matrix3Ds. +/// +public class Matrix3DStack : IEnumerable, ICollection +{ + private readonly List storage = new List(); + + public int Count + { + get { return this.storage.Count; } + } + + bool ICollection.IsSynchronized + { + get { return ((ICollection)this.storage).IsSynchronized; } + } + + object ICollection.SyncRoot + { + get { return ((ICollection)this.storage).SyncRoot; } + } + + public Matrix3D Peek() + { + return this.storage[this.storage.Count - 1]; + } + + public void Push(Matrix3D item) + { + this.storage.Add(item); + } + + public void Append(Matrix3D item) + { + if (this.Count > 0) + { + Matrix3D top = this.Peek(); + top.Append(item); + this.Push(top); + } + else + { + this.Push(item); + } + } + + public void Prepend(Matrix3D item) + { + if (this.Count > 0) + { + Matrix3D top = this.Peek(); + top.Prepend(item); + this.Push(top); + } + else + { + this.Push(item); + } + } + + public Matrix3D Pop() + { + Matrix3D result = this.Peek(); + this.storage.RemoveAt(this.storage.Count - 1); + + return result; + } + + public void Clear() + { + this.storage.Clear(); + } + + public bool Contains(Matrix3D item) + { + return this.storage.Contains(item); + } + + void ICollection.CopyTo(Array array, int index) + { + ((ICollection)this.storage).CopyTo(array, index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + for (int i = this.storage.Count - 1; i >= 0; i--) + { + yield return this.storage[i]; + } + } +} diff --git a/WpfRemote/3D/PrsTransform.cs b/WpfRemote/3D/PrsTransform.cs new file mode 100644 index 00000000..436c4782 --- /dev/null +++ b/WpfRemote/3D/PrsTransform.cs @@ -0,0 +1,90 @@ +namespace WpfUtils.Meida3D; + +using System.Windows.Media.Media3D; + +public class PrsTransform +{ + private readonly Transform3DGroup transform = new Transform3DGroup(); + private readonly TranslateTransform3D position = new TranslateTransform3D(); + private readonly QuaternionRotation3D rotation = new QuaternionRotation3D(); + private readonly ScaleTransform3D scale = new ScaleTransform3D(); + + public PrsTransform() + { + this.transform.Children.Add(this.position); + + RotateTransform3D rotation = new RotateTransform3D(); + rotation.Rotation = this.rotation; + this.transform.Children.Add(rotation); + + this.transform.Children.Add(this.scale); + } + + public Transform3DGroup Transform => this.transform; + public bool IsAffine => this.transform.IsAffine; + public Matrix3D Value => this.transform.Value; + + public Vector3D Scale3D + { + get + { + return new Vector3D(this.scale.ScaleX, this.scale.ScaleY, this.scale.ScaleZ); + } + + set + { + this.scale.ScaleX = value.X; + this.scale.ScaleY = value.Y; + this.scale.ScaleZ = value.Z; + } + } + + public double UniformScale + { + get + { + double scale = this.scale.ScaleX; + this.UniformScale = scale; + return scale; + } + + set + { + this.scale.ScaleX = value; + this.scale.ScaleY = value; + this.scale.ScaleZ = value; + } + } + + public Quaternion Rotation + { + get => this.rotation.Quaternion; + set => this.rotation.Quaternion = value; + } + + public Vector3D Position + { + get + { + return new Vector3D(this.position.OffsetX, this.position.OffsetY, this.position.OffsetZ); + } + + set + { + this.position.OffsetX = value.X; + this.position.OffsetY = value.Y; + this.position.OffsetZ = value.Z; + + /*this.scale.CenterX = value.X; + this.scale.CenterY = value.Y; + this.scale.CenterZ = value.Z;*/ + } + } + + public void Reset() + { + this.Position = new Vector3D(0, 0, 0); + this.Rotation = Quaternion.Identity; + this.UniformScale = 1; + } +} diff --git a/WpfRemote/3D/Sphere.cs b/WpfRemote/3D/Sphere.cs new file mode 100644 index 00000000..bd92a0c6 --- /dev/null +++ b/WpfRemote/3D/Sphere.cs @@ -0,0 +1,122 @@ +namespace WpfUtils.Meida3D; + +using System; +using System.Windows; +using System.Windows.Media.Media3D; + +public class Sphere : ModelVisual3D +{ + private readonly GeometryModel3D model; + private int slices = 32; + private int stacks = 16; + private double radius = 1; + private Point3D center = default; + + public Sphere() + { + this.model = new GeometryModel3D(); + this.model.Geometry = this.CalculateMesh(); + this.Content = this.model; + } + + public int Slices + { + get + { + return this.slices; + } + set + { + this.slices = value; + this.model.Geometry = this.CalculateMesh(); + } + } + + public int Stacks + { + get + { + return this.stacks; + } + set + { + this.stacks = value; + this.model.Geometry = this.CalculateMesh(); + } + } + + public double Radius + { + get + { + return this.radius; + } + set + { + this.radius = value; + this.model.Geometry = this.CalculateMesh(); + } + } + + public Material Material + { + get + { + return this.model.Material; + } + + set + { + this.model.Material = value; + } + } + + private MeshGeometry3D CalculateMesh() + { + MeshGeometry3D mesh = new MeshGeometry3D(); + + for (int stack = 0; stack <= this.Stacks; stack++) + { + double phi = (Math.PI / 2) - (stack * Math.PI / this.Stacks); + double y = this.Radius * Math.Sin(phi); + double scale = -this.Radius * Math.Cos(phi); + + for (int slice = 0; slice <= this.Slices; slice++) + { + double theta = slice * 2 * Math.PI / this.Slices; + double x = scale * Math.Sin(theta); + double z = scale * Math.Cos(theta); + + Vector3D normal = new Vector3D(x, y, z); + mesh.Normals.Add(normal); + mesh.Positions.Add(this.center + normal); + mesh.TextureCoordinates.Add(new Point((double)slice / this.Slices, (double)stack / this.Stacks)); + } + } + + for (int stack = 0; stack <= this.Stacks; stack++) + { + int top = (stack + 0) * (this.Slices + 1); + int bot = (stack + 1) * (this.Slices + 1); + + for (int slice = 0; slice < this.Slices; slice++) + { + if (stack != 0) + { + mesh.TriangleIndices.Add(top + slice); + mesh.TriangleIndices.Add(bot + slice); + mesh.TriangleIndices.Add(top + slice + 1); + } + + if (stack != this.Stacks - 1) + { + mesh.TriangleIndices.Add(top + slice + 1); + mesh.TriangleIndices.Add(bot + slice); + mesh.TriangleIndices.Add(bot + slice + 1); + } + } + } + + return mesh; + } +} diff --git a/WpfRemote/App.xaml b/WpfRemote/App.xaml index 0163c8d4..db98d8ff 100644 --- a/WpfRemote/App.xaml +++ b/WpfRemote/App.xaml @@ -1,9 +1,145 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Controls/NumberBox.xaml b/WpfRemote/Controls/NumberBox.xaml new file mode 100644 index 00000000..c4da117f --- /dev/null +++ b/WpfRemote/Controls/NumberBox.xaml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WpfRemote/Controls/NumberBox.xaml.cs b/WpfRemote/Controls/NumberBox.xaml.cs new file mode 100644 index 00000000..4fbe3c3a --- /dev/null +++ b/WpfRemote/Controls/NumberBox.xaml.cs @@ -0,0 +1,556 @@ +namespace WpfUtils.Controls; + +using System; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using PropertyChanged.SourceGenerator; +using WpfUtils.DependencyProperties; +using DrawPoint = System.Drawing.Point; +using WinCur = System.Windows.Forms.Cursor; +using WinPoint = System.Windows.Point; + +/// +/// Interaction logic for NumberBox.xaml. +/// +public partial class NumberBox : UserControl, INotifyPropertyChanged +{ + public static readonly IBind ValueDp = Binder.Register(nameof(Value), OnValueChanged); + public static readonly IBind TickDp = Binder.Register(nameof(TickFrequency), OnTickChanged, BindMode.OneWay); + public static readonly IBind SliderDp = Binder.Register(nameof(Slider), OnSliderChanged, BindMode.OneWay); + public static readonly IBind ButtonsDp = Binder.Register(nameof(Buttons), OnButtonsChanged, BindMode.OneWay); + public static readonly IBind MinDp = Binder.Register(nameof(Minimum), OnMinimumChanged, BindMode.OneWay); + public static readonly IBind MaxDp = Binder.Register(nameof(Maximum), OnMaximumChanged, BindMode.OneWay); + public static readonly IBind WrapDp = Binder.Register(nameof(Wrap), BindMode.OneWay); + public static readonly IBind OffsetDp = Binder.Register(nameof(ValueOffset), BindMode.OneWay); + public static readonly IBind UncapTextInputDp = Binder.Register(nameof(UncapTextInput), BindMode.OneWay); + public static readonly IBind PrefixDp = Binder.Register(nameof(Prefix), BindMode.OneWay); + public static readonly IBind SuffixDp = Binder.Register(nameof(Suffix), BindMode.OneWay); + public static readonly IBind CornerRadiusDp = Binder.Register(nameof(CornerRadius), OnCornerRadiusChanged, BindMode.OneWay); + + private string? inputString; + private Key keyHeld = Key.None; + private double relativeSliderStart; + private double relativeSliderCurrent; + private bool bypassFocusLock = false; + + public NumberBox() + { + this.InitializeComponent(); + this.TickFrequency = 1; + this.Minimum = double.MinValue; + this.Maximum = double.MaxValue; + this.Wrap = false; + this.Text = this.DisplayValue.ToString(); + this.Slider = SliderModes.None; + this.Buttons = false; + this.CornerRadius = new(6, 6, 6, 6); + + this.ContentArea.DataContext = this; + } + + public event PropertyChangedEventHandler? PropertyChanged; + + public enum SliderModes + { + None, + Absolute, + Relative, + } + + public double TickFrequency + { + get => TickDp.Get(this); + set => TickDp.Set(this, value); + } + + public SliderModes Slider + { + get => SliderDp.Get(this); + set => SliderDp.Set(this, value); + } + + public bool Buttons + { + get => ButtonsDp.Get(this); + set => ButtonsDp.Set(this, value); + } + + public double Minimum + { + get => MinDp.Get(this); + set => MinDp.Set(this, value); + } + + public double Maximum + { + get => MaxDp.Get(this); + set => MaxDp.Set(this, value); + } + + public bool Wrap + { + get => WrapDp.Get(this); + set => WrapDp.Set(this, value); + } + + public double ValueOffset + { + get => OffsetDp.Get(this); + set => OffsetDp.Set(this, value); + } + + public double Value + { + get => ValueDp.Get(this); + set => ValueDp.Set(this, value); + } + + public bool UncapTextInput + { + get => UncapTextInputDp.Get(this); + set => UncapTextInputDp.Set(this, value); + } + + public object Prefix + { + get => PrefixDp.Get(this); + set => PrefixDp.Set(this, value); + } + + public object Suffix + { + get => SuffixDp.Get(this); + set => SuffixDp.Set(this, value); + } + + public CornerRadius CornerRadius + { + get => CornerRadiusDp.Get(this); + set => CornerRadiusDp.Set(this, value); + } + + public double DisplayValue + { + get + { + return this.Value + this.ValueOffset; + } + + set + { + this.Value = value - this.ValueOffset; + + if (!this.UncapTextInput) + { + if (this.Wrap) + { + double range = this.Maximum - this.Minimum; + + if (this.Value > this.Maximum) + this.Value = this.Minimum + ((this.Value - this.Maximum) % range); + + if (this.Value < this.Minimum) + this.Value = this.Maximum - ((this.Maximum - this.Value) % range); + } + + this.Value = Math.Max(this.Minimum, this.Value); + this.Value = Math.Min(this.Maximum, this.Value); + } + + this.PropertyChanged?.Invoke(this, new(nameof(NumberBox.DisplayValue))); + } + } + + public string? Text + { + get + { + return this.inputString; + } + + set + { + this.inputString = value; + + double val; + if (double.TryParse(value, out val)) + { + this.DisplayValue = val; + this.ErrorDisplay.Visibility = Visibility.Collapsed; + } + else + { + try + { + val = Convert.ToDouble(new DataTable().Compute(value, null)); + this.DisplayValue = val; + this.ErrorDisplay.Visibility = Visibility.Collapsed; + } + catch (Exception) + { + this.ErrorDisplay.Visibility = Visibility.Visible; + } + } + + this.PropertyChanged?.Invoke(this, new(nameof(NumberBox.Text))); + } + } + + public double SliderValue + { + get + { + if (this.Slider == SliderModes.Absolute) + { + return this.DisplayValue; + } + else + { + return this.relativeSliderCurrent; + } + } + set + { + this.bypassFocusLock = true; + + if (this.Slider == SliderModes.Absolute) + { + this.DisplayValue = value; + } + else + { + this.relativeSliderCurrent = value; + + if (Keyboard.IsKeyDown(Key.LeftShift)) + value *= 10; + + if (Keyboard.IsKeyDown(Key.LeftCtrl)) + value /= 10; + + this.DisplayValue = this.relativeSliderStart + value; + } + + this.bypassFocusLock = false; + + this.PropertyChanged?.Invoke(this, new(nameof(NumberBox.SliderValue))); + } + } + + public double SliderMinimum + { + get + { + if (this.Slider == SliderModes.Absolute) + { + return this.Minimum; + } + else + { + return -(this.TickFrequency * 30); + } + } + } + + public double SliderMaximum + { + get + { + if (this.Slider == SliderModes.Absolute) + { + return this.Maximum; + } + else + { + return this.TickFrequency * 30; + } + } + } + + protected override void OnPreviewKeyDown(KeyEventArgs e) + { + bool focused = this.InputBox.IsKeyboardFocused || this.InputSlider.IsKeyboardFocused; + if (!focused) + return; + + if (e.Key == Key.Return) + { + this.Commit(true); + e.Handled = true; + } + + if (e.Key == Key.Up || e.Key == Key.Down) + { + e.Handled = true; + + if (e.IsRepeat) + { + if (this.keyHeld == e.Key) + return; + + this.keyHeld = e.Key; + Task.Run(this.TickHeldKey); + } + else + { + this.TickKey(e.Key); + } + } + } + + protected override void OnPreviewKeyUp(KeyEventArgs e) + { + if (this.keyHeld == e.Key) + { + e.Handled = true; + this.keyHeld = Key.None; + } + } + + protected override void OnPreviewMouseWheel(MouseWheelEventArgs e) + { + if (!this.InputBox.IsFocused) + return; + + e.Handled = true; + this.TickValue(e.Delta > 0); + } + + private static void OnValueChanged(NumberBox sender, double v) + { + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderValue))); + + if (!sender.bypassFocusLock && sender.InputBox.IsFocused) + return; + + sender.Text = sender.DisplayValue.ToString("0.###"); + } + + private static void OnSliderChanged(NumberBox sender, SliderModes mode) + { + sender.SliderArea.Visibility = mode != SliderModes.None ? Visibility.Visible : Visibility.Collapsed; + sender.BoxBorder.CornerRadius = mode != SliderModes.None ? new(0, sender.CornerRadius.TopRight, sender.CornerRadius.BottomRight, 0) : sender.CornerRadius; + sender.SliderArea.CornerRadius = new(sender.CornerRadius.TopLeft, 0, 0, sender.CornerRadius.BottomLeft); + + int inputColumnWidth = 64; + if (sender.Buttons) + inputColumnWidth += 48; + + sender.SliderColumn.Width = mode != SliderModes.None ? new GridLength(1, GridUnitType.Star) : new GridLength(0); + sender.InputBoxColumn.Width = mode != SliderModes.None ? new GridLength(inputColumnWidth) : new GridLength(1, GridUnitType.Star); + + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderMaximum))); + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderMinimum))); + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderValue))); + } + + private static void OnCornerRadiusChanged(NumberBox sender, CornerRadius value) + { + sender.BoxBorder.CornerRadius = sender.Slider != SliderModes.None ? new(0, sender.CornerRadius.TopRight, sender.CornerRadius.BottomRight, 0) : sender.CornerRadius; + sender.SliderArea.CornerRadius = new(sender.CornerRadius.TopLeft, 0, 0, sender.CornerRadius.BottomLeft); + } + + private static void OnButtonsChanged(NumberBox sender, bool v) + { + sender.DownButton.Visibility = v ? Visibility.Visible : Visibility.Collapsed; + sender.UpButton.Visibility = v ? Visibility.Visible : Visibility.Collapsed; + } + + private static void OnMinimumChanged(NumberBox sender, double value) + { + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderMinimum))); + } + + private static void OnMaximumChanged(NumberBox sender, double value) + { + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderMaximum))); + } + + private static void OnTickChanged(NumberBox sender, double tick) + { + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderMaximum))); + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderMinimum))); + sender.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(nameof(NumberBox.SliderValue))); + } + + private void UserControl_Loaded(object sender, RoutedEventArgs e) + { + Window window = Window.GetWindow(this); + if (window != null) + { + window.MouseDown += this.OnWindowMouseDown; + window.Deactivated += this.OnWindowDeactivated; + } + + OnSliderChanged(this, this.Slider); + OnButtonsChanged(this, this.Buttons); + OnTickChanged(this, this.TickFrequency); + + this.Text = this.DisplayValue.ToString("0.###"); + } + + private double Validate(double v) + { + if (this.Wrap) + { + if (v > this.Maximum) + { + v = this.Minimum; + } + + if (v < this.Minimum) + { + v = this.Maximum; + } + } + else + { + v = Math.Min(v, this.Maximum); + v = Math.Max(v, this.Minimum); + } + + ////if (this.TickFrequency != 0) + //// v = Math.Round(v / this.TickFrequency) * this.TickFrequency; + + return v; + } + + private void OnLostFocus(object sender, RoutedEventArgs e) + { + this.Text = this.DisplayValue.ToString("0.###"); + ////this.Commit(false); + } + + private void Commit(bool refocus) + { + try + { + this.DisplayValue = Convert.ToDouble(new DataTable().Compute(this.inputString, null)); + this.ErrorDisplay.Visibility = Visibility.Collapsed; + } + catch (Exception) + { + this.ErrorDisplay.Visibility = Visibility.Visible; + } + + this.Text = this.DisplayValue.ToString("0.###"); + + if (refocus) + { + this.InputBox.Focus(); + this.InputBox.CaretIndex = int.MaxValue; + } + } + + private async Task TickHeldKey() + { + while (this.keyHeld != Key.None) + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + this.TickKey(this.keyHeld); + }); + + await Task.Delay(10); + } + } + + private void TickKey(Key key) + { + if (key == Key.Up) + { + this.TickValue(true); + this.Commit(true); + } + else if (key == Key.Down) + { + this.TickValue(false); + this.Commit(true); + } + } + + private void TickValue(bool increase) + { + double delta = increase ? this.TickFrequency : -this.TickFrequency; + + if (Keyboard.IsKeyDown(Key.LeftShift)) + delta *= 10; + + if (Keyboard.IsKeyDown(Key.LeftCtrl)) + delta /= 10; + + double value = this.DisplayValue; + double newValue = value + delta; + newValue = this.Validate(newValue); + + if (newValue == value) + return; + + this.bypassFocusLock = true; + this.DisplayValue = newValue; + this.bypassFocusLock = false; + } + + private void OnDownClick(object sender, RoutedEventArgs e) + { + this.TickValue(false); + } + + private void OnUpClick(object sender, RoutedEventArgs e) + { + this.TickValue(true); + } + + private void OnSliderMouseMove(object sender, MouseEventArgs e) + { + if (this.Slider != SliderModes.Absolute) + return; + + if (e.LeftButton == MouseButtonState.Pressed && this.Wrap) + { + WinPoint rightEdge = this.InputSlider.PointToScreen(new WinPoint(this.InputSlider.ActualWidth - 5, this.InputSlider.ActualHeight / 2)); + WinPoint leftEdge = this.InputSlider.PointToScreen(new WinPoint(6, this.InputSlider.ActualHeight / 2)); + + if (WinCur.Position.X > rightEdge.X) + { + WinCur.Position = new DrawPoint((int)leftEdge.X, (int)leftEdge.Y); + } + + if (WinCur.Position.X < leftEdge.X) + { + WinCur.Position = new DrawPoint((int)rightEdge.X, (int)rightEdge.Y); + } + } + } + + private void OnWindowMouseDown(object? sender, MouseButtonEventArgs e) + { + FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); + Keyboard.ClearFocus(); + } + + private void OnWindowDeactivated(object? sender, EventArgs e) + { + FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); + Keyboard.ClearFocus(); + } + + private void OnSliderPreviewMouseDown(object? sender, MouseButtonEventArgs e) + { + this.relativeSliderStart = this.DisplayValue; + } + + private void OnSliderPreviewMouseUp(object? sender, MouseButtonEventArgs e) + { + if (this.Slider == SliderModes.Relative) + { + this.relativeSliderStart = this.DisplayValue; + this.SliderValue = 0; + } + } +} diff --git a/WpfRemote/Controls/RelativeSlider.cs b/WpfRemote/Controls/RelativeSlider.cs new file mode 100644 index 00000000..912dd225 --- /dev/null +++ b/WpfRemote/Controls/RelativeSlider.cs @@ -0,0 +1,68 @@ +namespace WpfUtils.Controls; + +using System.Windows; +using System.Windows.Input; +using WpfUtils.DependencyProperties; + +public class RelativeSlider : Slider +{ + public static readonly IBind RelativeValueDp = Binder.Register(nameof(RelativeValue)); + public static readonly IBind RelativeRangeDp = Binder.Register(nameof(RelativeRange), OnRelativeRangeChanged, BindMode.OneWay); + + private double relativeSliderStart; + + public RelativeSlider() + { + this.PreviewMouseDown += this.OnPreviewMouseDown; + this.PreviewMouseUp += this.OnPreviewMouseUp; + this.ValueChanged += this.OnValueChanged; + + this.Value = 0; + } + + public double RelativeValue + { + get => RelativeValueDp.Get(this); + set => RelativeValueDp.Set(this, value); + } + + public double RelativeRange + { + get => RelativeRangeDp.Get(this); + set => RelativeRangeDp.Set(this, value); + } + + protected override void OnPreviewKeyDown(object sender, KeyEventArgs e) + { + this.relativeSliderStart = this.RelativeValue; + base.OnPreviewKeyDown(sender, e); + } + + protected override void OnPreviewKeyUp(object sender, KeyEventArgs e) + { + this.relativeSliderStart = this.RelativeValue; + base.OnPreviewKeyUp(sender, e); + } + + private static void OnRelativeRangeChanged(RelativeSlider sender, double value) + { + sender.Minimum = -value; + sender.Maximum = value; + } + + private void OnValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + this.RelativeValue = this.relativeSliderStart + this.Value; + } + + private void OnPreviewMouseDown(object? sender, MouseButtonEventArgs e) + { + this.relativeSliderStart = this.RelativeValue; + } + + private void OnPreviewMouseUp(object? sender, MouseButtonEventArgs e) + { + this.relativeSliderStart = this.RelativeValue; + this.Value = 0; + } +} diff --git a/WpfRemote/Controls/Slider.cs b/WpfRemote/Controls/Slider.cs new file mode 100644 index 00000000..1c2fa838 --- /dev/null +++ b/WpfRemote/Controls/Slider.cs @@ -0,0 +1,96 @@ +namespace WpfUtils.Controls; + +using System.Reflection; +using System.Windows.Input; + +public class Slider : System.Windows.Controls.Slider +{ + private MethodInfo? moveToNextTickMethod; + + public Slider() + { + this.PreviewKeyDown += this.OnPreviewKeyDown; + this.PreviewKeyUp += this.OnPreviewKeyUp; + } + + protected double GetChangeMultiplier() + { + if (Keyboard.IsKeyDown(Key.LeftShift)) + return 10; + + if (Keyboard.IsKeyDown(Key.RightShift)) + return 10; + + if (Keyboard.IsKeyDown(Key.LeftCtrl)) + return 0.1f; + + if (Keyboard.IsKeyDown(Key.RightCtrl)) + return 0.1f; + + return 1.0; + } + + protected override void OnDecreaseSmall() + { + this.MoveToNextTick(-this.SmallChange * this.GetChangeMultiplier()); + } + + protected override void OnIncreaseSmall() + { + this.MoveToNextTick(this.SmallChange * this.GetChangeMultiplier()); + } + + protected override void OnDecreaseLarge() + { + this.MoveToNextTick(-this.LargeChange * this.GetChangeMultiplier()); + } + + protected override void OnIncreaseLarge() + { + this.MoveToNextTick(this.LargeChange * this.GetChangeMultiplier()); + } + + protected void MoveToNextTick(double direction) + { + if (this.moveToNextTickMethod == null) + this.moveToNextTickMethod = typeof(System.Windows.Controls.Slider).GetMethod("MoveToNextTick", BindingFlags.NonPublic | BindingFlags.Instance); + + if (this.moveToNextTickMethod == null) + return; + + this.moveToNextTickMethod.Invoke(this, new object[] { direction }); + } + + protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Left) + { + this.OnDecreaseSmall(); + e.Handled = true; + } + else if (e.Key == Key.Right) + { + this.OnIncreaseSmall(); + e.Handled = true; + } + else if (e.Key == Key.Down) + { + this.MoveFocus(new(FocusNavigationDirection.Next)); + e.Handled = true; + } + else if (e.Key == Key.Up) + { + this.MoveFocus(new(FocusNavigationDirection.Previous)); + e.Handled = true; + } + } + + protected virtual void OnPreviewKeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Down || e.Key == Key.Up) + { + e.Handled = true; + this.Value = 0; + } + } +} diff --git a/WpfRemote/Converters/AbsoluteNumberConverter.cs b/WpfRemote/Converters/AbsoluteNumberConverter.cs new file mode 100644 index 00000000..a43a08e5 --- /dev/null +++ b/WpfRemote/Converters/AbsoluteNumberConverter.cs @@ -0,0 +1,11 @@ +namespace WpfUtils.Converters; + +using System; + +public class AbsoluteNumberConverter : ConverterBase +{ + protected override double Convert(double value) + { + return Math.Abs(value); + } +} diff --git a/WpfRemote/Converters/AnyBoolIsFalseToBoolMultiConverter.cs b/WpfRemote/Converters/AnyBoolIsFalseToBoolMultiConverter.cs new file mode 100644 index 00000000..662a9306 --- /dev/null +++ b/WpfRemote/Converters/AnyBoolIsFalseToBoolMultiConverter.cs @@ -0,0 +1,33 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows.Data; + +/// +/// If all of the bools are true, returns false. +/// If any of the bools are false, returns true. +/// +public class AnyBoolIsFalseToBoolMultiConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + foreach (object value in values) + { + if (value is bool boolValue) + { + if (!boolValue) + { + return true; + } + } + } + + return false; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException("BooleanAndConverter is a OneWay converter."); + } +} diff --git a/WpfRemote/Converters/BoolInversionConverter.cs b/WpfRemote/Converters/BoolInversionConverter.cs new file mode 100644 index 00000000..c880345e --- /dev/null +++ b/WpfRemote/Converters/BoolInversionConverter.cs @@ -0,0 +1,14 @@ +namespace WpfUtils.Converters; + +public class BoolInversionConverter : ConverterBase +{ + protected override bool Convert(bool value) + { + return !value; + } + + protected override bool ConvertBack(bool value) + { + return !value; + } +} diff --git a/WpfRemote/Converters/BoolToIntConverter.cs b/WpfRemote/Converters/BoolToIntConverter.cs new file mode 100644 index 00000000..11fa7afd --- /dev/null +++ b/WpfRemote/Converters/BoolToIntConverter.cs @@ -0,0 +1,9 @@ +namespace WpfUtils.Converters; + +public class BoolToIntConverter : ConverterBase +{ + protected override int Convert(bool value) + { + return value ? 1 : 0; + } +} diff --git a/WpfRemote/Converters/ColorToBrushConverter.cs b/WpfRemote/Converters/ColorToBrushConverter.cs new file mode 100644 index 00000000..7a2780d5 --- /dev/null +++ b/WpfRemote/Converters/ColorToBrushConverter.cs @@ -0,0 +1,13 @@ +namespace WpfUtils.Converters; + +using System.Windows.Data; +using System.Windows.Media; + +[ValueConversion(typeof(Color), typeof(Brush))] +public class ColorToBrushConverter : ConverterBase +{ + protected override Brush Convert(Color value) + { + return new SolidColorBrush(value); + } +} diff --git a/WpfRemote/Converters/ConverterBase.cs b/WpfRemote/Converters/ConverterBase.cs new file mode 100644 index 00000000..efa2343a --- /dev/null +++ b/WpfRemote/Converters/ConverterBase.cs @@ -0,0 +1,67 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows.Data; + +public abstract class ConverterBase : IValueConverter +{ + public object? Parameter { get; private set; } + + public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + this.Parameter = parameter; + + if (value is TFrom tValue) + return this.Convert(tValue); + + throw new InvalidCastException(); + } + + public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + this.Parameter = parameter; + + if (value is TTo tValue) + return this.ConvertBack(tValue); + + throw new InvalidCastException(); + } + + protected abstract TTo Convert(TFrom? value); + + protected virtual TFrom ConvertBack(TTo? value) + { + throw new NotSupportedException(); + } +} + +public abstract class ConverterBase : ConverterBase +{ + public new TParameter Parameter + { + get + { + if (base.Parameter is TParameter tParameter) + return tParameter; + + if (typeof(TParameter) == typeof(double)) + { + double val = System.Convert.ToDouble(base.Parameter, CultureInfo.InvariantCulture); + + if (val is TParameter tParameterVal) + return tParameterVal; + } + + if (typeof(TParameter) == typeof(int)) + { + int val = System.Convert.ToInt32(base.Parameter, CultureInfo.InvariantCulture); + + if (val is TParameter tParameterVal) + return tParameterVal; + } + + throw new InvalidCastException(); + } + } +} diff --git a/WpfRemote/Converters/EnumToBoolConverter.cs b/WpfRemote/Converters/EnumToBoolConverter.cs new file mode 100644 index 00000000..a29219fb --- /dev/null +++ b/WpfRemote/Converters/EnumToBoolConverter.cs @@ -0,0 +1,85 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows.Data; + +[ValueConversion(typeof(Enum), typeof(bool))] +public class EnumToBoolConverter : IValueConverter +{ + private enum AdditionMode + { + Or, + And, + } + + public static bool Convert(object? value, Type targetType, object parameter) + { + if (value == null) + return false; + + Type enumType = value.GetType(); + + if (!enumType.IsEnum) + throw new Exception("Enum converter can only be used on an enum type"); + + Enum currentValue = (Enum)value; + + string enumValueString = (string)parameter; + + AdditionMode mode = AdditionMode.Or; + + if (enumValueString.Contains("|") && enumValueString.Contains("&")) + { + throw new NotSupportedException("Cannot mix or (|) with and (&) in enum converter parameter"); + } + else if (enumValueString.Contains("|")) + { + mode = AdditionMode.Or; + } + else if (enumValueString.Contains("&")) + { + mode = AdditionMode.And; + } + + string[] values = enumValueString.Split('|', '?', StringSplitOptions.RemoveEmptyEntries); + bool returnvalue = false; + + foreach (string enumValueStringPart in values) + { + Enum parameterValue = (Enum)Enum.Parse(enumType, enumValueStringPart.Trim(' ', '!')); + + bool isEnumValue = Enum.Equals(currentValue, parameterValue); + + if (enumValueString.StartsWith('!')) + isEnumValue = !isEnumValue; + + if (mode == AdditionMode.Or) + { + returnvalue |= isEnumValue; + } + else + { + returnvalue &= isEnumValue; + } + } + + return returnvalue; + } + + public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) + { + return Convert(value, targetType, parameter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not bool bVal || bVal == false) + return Binding.DoNothing; + + if (!targetType.IsEnum) + throw new Exception("Enum converter can only be used on an enum type"); + + return Enum.Parse(targetType, (string)parameter); + } +} diff --git a/WpfRemote/Converters/EnumToVisibilityConverter.cs b/WpfRemote/Converters/EnumToVisibilityConverter.cs new file mode 100644 index 00000000..dfd6d330 --- /dev/null +++ b/WpfRemote/Converters/EnumToVisibilityConverter.cs @@ -0,0 +1,20 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(Enum), typeof(Visibility))] +public class EnumToVisibilityConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) + { + return EnumToBoolConverter.Convert(value, targetType, parameter) ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/WpfRemote/Converters/FloatToDoubleConverter.cs b/WpfRemote/Converters/FloatToDoubleConverter.cs new file mode 100644 index 00000000..c6c17a9b --- /dev/null +++ b/WpfRemote/Converters/FloatToDoubleConverter.cs @@ -0,0 +1,10 @@ +namespace WpfUtils.Converters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +internal class FloatToDoubleConverter +{ +} diff --git a/WpfRemote/Converters/GreaterThanToVisibilityConverter.cs b/WpfRemote/Converters/GreaterThanToVisibilityConverter.cs new file mode 100644 index 00000000..03c81405 --- /dev/null +++ b/WpfRemote/Converters/GreaterThanToVisibilityConverter.cs @@ -0,0 +1,11 @@ +namespace WpfUtils.Converters; + +using System.Windows; + +public class GreaterThanToVisibilityConverter : ConverterBase +{ + protected override Visibility Convert(double value) + { + return value > this.Parameter ? Visibility.Visible : Visibility.Collapsed; + } +} diff --git a/WpfRemote/Converters/InvertedBoolToVisibilityConverter.cs b/WpfRemote/Converters/InvertedBoolToVisibilityConverter.cs new file mode 100644 index 00000000..af570ba1 --- /dev/null +++ b/WpfRemote/Converters/InvertedBoolToVisibilityConverter.cs @@ -0,0 +1,19 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +public class InvertedBoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool)value ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/IsEmptyToVisibilityConverter.cs b/WpfRemote/Converters/IsEmptyToVisibilityConverter.cs new file mode 100644 index 00000000..c6642039 --- /dev/null +++ b/WpfRemote/Converters/IsEmptyToVisibilityConverter.cs @@ -0,0 +1,32 @@ +namespace WpfUtils.Converters; + +using System; +using System.Collections; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(IEnumerable), typeof(Visibility))] +public class IsEmptyToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + return Visibility.Visible; + + if (value is IEnumerable enumerable) + { + foreach (object obj in enumerable) + { + return Visibility.Collapsed; + } + } + + return Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/IsZeroToBoolConverter.cs b/WpfRemote/Converters/IsZeroToBoolConverter.cs new file mode 100644 index 00000000..1a889e1e --- /dev/null +++ b/WpfRemote/Converters/IsZeroToBoolConverter.cs @@ -0,0 +1,51 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(bool))] +public class IsZeroToBoolConverter : IValueConverter +{ + public static bool IsZero(object value) + { + if (value is int intV) + { + return intV == 0; + } + else if (value is float floatV) + { + return floatV == 0; + } + else if (value is double doubleV) + { + return doubleV == 0; + } + else if (value is uint uintV) + { + return uintV == 0; + } + else if (value is ushort ushortV) + { + return ushortV == 0; + } + else if (value is byte byteV) + { + return byteV == 0; + } + else + { + throw new NotImplementedException($"value type {value.GetType()} not supported for not zero converter"); + } + } + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return IsZeroToBoolConverter.IsZero(value); + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/IsZeroToVisibilityConverter.cs b/WpfRemote/Converters/IsZeroToVisibilityConverter.cs new file mode 100644 index 00000000..1c0a0764 --- /dev/null +++ b/WpfRemote/Converters/IsZeroToVisibilityConverter.cs @@ -0,0 +1,19 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(Visibility))] +public class IsZeroToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return IsZeroToBoolConverter.IsZero(value) ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/LessThanToBoolConverter.cs b/WpfRemote/Converters/LessThanToBoolConverter.cs new file mode 100644 index 00000000..bc32d587 --- /dev/null +++ b/WpfRemote/Converters/LessThanToBoolConverter.cs @@ -0,0 +1,11 @@ +namespace WpfUtils.Converters; + +using System.Windows; + +public class LessThanToBoolConverter : ConverterBase +{ + protected override bool Convert(double value) + { + return value < this.Parameter; + } +} diff --git a/WpfRemote/Converters/LessThanToVisibilityConverter.cs b/WpfRemote/Converters/LessThanToVisibilityConverter.cs new file mode 100644 index 00000000..f929f943 --- /dev/null +++ b/WpfRemote/Converters/LessThanToVisibilityConverter.cs @@ -0,0 +1,11 @@ +namespace WpfUtils.Converters; + +using System.Windows; + +public class LessThanToVisibilityConverter : ConverterBase +{ + protected override Visibility Convert(double value) + { + return value < this.Parameter ? Visibility.Visible : Visibility.Collapsed; + } +} diff --git a/WpfRemote/Converters/ListToStringConverter.cs b/WpfRemote/Converters/ListToStringConverter.cs new file mode 100644 index 00000000..500029f6 --- /dev/null +++ b/WpfRemote/Converters/ListToStringConverter.cs @@ -0,0 +1,32 @@ +namespace WpfUtils.Converters; + +using System; +using System.Collections; +using System.Windows.Data; + +[ValueConversion(typeof(IEnumerable), typeof(string))] +public class ListToStringConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is IEnumerable enumerable) + { + string str = string.Empty; + int count = 0; + foreach (object v in enumerable) + { + str += v.ToString() + ", "; + count++; + } + + return count + ": " + str.TrimEnd(' ', ','); + } + + throw new Exception("List to string converter can only be used with enumerable sources"); + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/MultiBoolAndConverter.cs b/WpfRemote/Converters/MultiBoolAndConverter.cs new file mode 100644 index 00000000..a06dc997 --- /dev/null +++ b/WpfRemote/Converters/MultiBoolAndConverter.cs @@ -0,0 +1,27 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows.Data; + +public class MultiBoolAndConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + foreach (object value in values) + { + if (value is bool boolValue) + { + if (!boolValue) + return false; + } + } + + return true; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException("BooleanAndConverter is a OneWay converter."); + } +} diff --git a/WpfRemote/Converters/MultiBoolAndToVisibilityConverter.cs b/WpfRemote/Converters/MultiBoolAndToVisibilityConverter.cs new file mode 100644 index 00000000..30d1096c --- /dev/null +++ b/WpfRemote/Converters/MultiBoolAndToVisibilityConverter.cs @@ -0,0 +1,30 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +public class MultiBoolAndToVisibilityConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + foreach (object value in values) + { + if (value is bool boolValue) + { + if (!boolValue) + { + return Visibility.Collapsed; + } + } + } + + return Visibility.Visible; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException("BooleanAndConverter is a OneWay converter."); + } +} diff --git a/WpfRemote/Converters/MultiBoolOrConverter.cs b/WpfRemote/Converters/MultiBoolOrConverter.cs new file mode 100644 index 00000000..7b57cd7c --- /dev/null +++ b/WpfRemote/Converters/MultiBoolOrConverter.cs @@ -0,0 +1,27 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows.Data; + +public class MultiBoolOrConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + foreach (object value in values) + { + if (value is bool boolValue) + { + if (boolValue) + return true; + } + } + + return false; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException("BooleanAndConverter is a OneWay converter."); + } +} diff --git a/WpfRemote/Converters/MultiBoolOrToVisibilityConverter.cs b/WpfRemote/Converters/MultiBoolOrToVisibilityConverter.cs new file mode 100644 index 00000000..1c38821f --- /dev/null +++ b/WpfRemote/Converters/MultiBoolOrToVisibilityConverter.cs @@ -0,0 +1,28 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +public class MultiBoolOrToVisibilityConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + foreach (object value in values) + { + if (value is bool boolValue) + { + if (boolValue) + return Visibility.Visible; + } + } + + return Visibility.Collapsed; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException("BooleanAndConverter is a OneWay converter."); + } +} diff --git a/WpfRemote/Converters/NotEmptyToVisibilityConverter.cs b/WpfRemote/Converters/NotEmptyToVisibilityConverter.cs new file mode 100644 index 00000000..8ee99194 --- /dev/null +++ b/WpfRemote/Converters/NotEmptyToVisibilityConverter.cs @@ -0,0 +1,32 @@ +namespace WpfUtils.Converters; + +using System; +using System.Collections; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(IEnumerable), typeof(Visibility))] +public class NotEmptyToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + return Visibility.Collapsed; + + if (value is IEnumerable enumerable) + { + foreach (object obj in enumerable) + { + return Visibility.Visible; + } + } + + return Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/NotNullToBoolConverter.cs b/WpfRemote/Converters/NotNullToBoolConverter.cs new file mode 100644 index 00000000..f37f9f7b --- /dev/null +++ b/WpfRemote/Converters/NotNullToBoolConverter.cs @@ -0,0 +1,26 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(bool))] +public class NotNullToBoolConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is string str) + { + if (string.IsNullOrEmpty(str)) + { + value = null; + } + } + + return value != null; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/NotNullToVisibilityConverter.cs b/WpfRemote/Converters/NotNullToVisibilityConverter.cs new file mode 100644 index 00000000..af113424 --- /dev/null +++ b/WpfRemote/Converters/NotNullToVisibilityConverter.cs @@ -0,0 +1,27 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(Visibility))] +public class NotNullToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is string str) + { + if (string.IsNullOrEmpty(str)) + { + value = null; + } + } + + return value == null ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/NotZeroToBoolConverter.cs b/WpfRemote/Converters/NotZeroToBoolConverter.cs new file mode 100644 index 00000000..36d85a0d --- /dev/null +++ b/WpfRemote/Converters/NotZeroToBoolConverter.cs @@ -0,0 +1,19 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(bool))] +public class NotZeroToBoolConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return !IsZeroToBoolConverter.IsZero(value); + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/NotZeroToVisibilityConverter.cs b/WpfRemote/Converters/NotZeroToVisibilityConverter.cs new file mode 100644 index 00000000..75715485 --- /dev/null +++ b/WpfRemote/Converters/NotZeroToVisibilityConverter.cs @@ -0,0 +1,19 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(Visibility))] +public class NotZeroToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return IsZeroToBoolConverter.IsZero(value) ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/NullToBoolConverter.cs b/WpfRemote/Converters/NullToBoolConverter.cs new file mode 100644 index 00000000..5264e5a3 --- /dev/null +++ b/WpfRemote/Converters/NullToBoolConverter.cs @@ -0,0 +1,26 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(bool))] +public class NullToBoolConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is string str) + { + if (string.IsNullOrEmpty(str)) + { + value = null; + } + } + + return value == null; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/NullToVisibilityConverter.cs b/WpfRemote/Converters/NullToVisibilityConverter.cs new file mode 100644 index 00000000..a4441d91 --- /dev/null +++ b/WpfRemote/Converters/NullToVisibilityConverter.cs @@ -0,0 +1,27 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(Visibility))] +public class NullToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is string str) + { + if (string.IsNullOrEmpty(str)) + { + value = null; + } + } + + return value == null ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/NumberConverter.cs b/WpfRemote/Converters/NumberConverter.cs new file mode 100644 index 00000000..84595dc0 --- /dev/null +++ b/WpfRemote/Converters/NumberConverter.cs @@ -0,0 +1,37 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows.Data; + +[ValueConversion(typeof(object), typeof(double))] +public class NumberConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double doubleVal && parameter is Type target) + { + if (target == typeof(byte)) + return System.Convert.ToByte(value); + + if (target == typeof(short)) + return System.Convert.ToInt16(value); + + if (target == typeof(int)) + return System.Convert.ToInt32(value); + + if (target == typeof(long)) + return System.Convert.ToInt64(value); + + if (target == typeof(float)) + return System.Convert.ToSingle(value); + } + + return value; + } +} diff --git a/WpfRemote/Converters/NumberToThicknessConverter.cs b/WpfRemote/Converters/NumberToThicknessConverter.cs new file mode 100644 index 00000000..41befc93 --- /dev/null +++ b/WpfRemote/Converters/NumberToThicknessConverter.cs @@ -0,0 +1,59 @@ +namespace WpfUtils.Converters; + +using System.Windows; + +public class NumberToThicknessLeftConverter : NumberToThicknessConverter +{ + protected override void Add(double value, ref Thickness baseThickness) + { + baseThickness.Left += value; + } +} + +public class NumberToThicknessTopConverter : NumberToThicknessConverter +{ + protected override void Add(double value, ref Thickness baseThickness) + { + baseThickness.Top += value; + } +} + +public class NumberToThicknessRightConverter : NumberToThicknessConverter +{ + protected override void Add(double value, ref Thickness baseThickness) + { + baseThickness.Right += value; + } +} + +public class NumberToThicknessBottomConverter : NumberToThicknessConverter +{ + protected override void Add(double value, ref Thickness baseThickness) + { + baseThickness.Bottom += value; + } +} + +public abstract class NumberToThicknessConverter : ConverterBase +{ + private static readonly ThicknessConverter ThicknessConverter = new(); + + protected sealed override Thickness Convert(double value) + { + Thickness thickness = default; + + if (this.Parameter is string paramStr) + { + object? obj = ThicknessConverter.ConvertFrom(this.Parameter); + if (obj is Thickness thicknessParam) + { + thickness = thicknessParam; + } + } + + this.Add(value, ref thickness); + return thickness; + } + + protected abstract void Add(double value, ref Thickness thickness); +} \ No newline at end of file diff --git a/WpfRemote/Converters/RadiansToDegreesConverter.cs b/WpfRemote/Converters/RadiansToDegreesConverter.cs new file mode 100644 index 00000000..adabbf14 --- /dev/null +++ b/WpfRemote/Converters/RadiansToDegreesConverter.cs @@ -0,0 +1,19 @@ +namespace WpfUtils.Converters; + +using System; +using System.Globalization; +using System.Windows.Data; + +[ValueConversion(typeof(float), typeof(float))] +public class RadiansToDegreesConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return System.Convert.ToSingle(value) * (180 / Math.PI); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return System.Convert.ToSingle(value) * (Math.PI / 180); + } +} diff --git a/WpfRemote/Converters/StringHasContentToBoolConverter.cs b/WpfRemote/Converters/StringHasContentToBoolConverter.cs new file mode 100644 index 00000000..6f6ae173 --- /dev/null +++ b/WpfRemote/Converters/StringHasContentToBoolConverter.cs @@ -0,0 +1,19 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows.Data; + +[ValueConversion(typeof(string), typeof(bool))] +public class StringHasContentToBoolConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + string? val = value as string; + return !string.IsNullOrEmpty(val); + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/Converters/StringHasContentToVisibilityConverter.cs b/WpfRemote/Converters/StringHasContentToVisibilityConverter.cs new file mode 100644 index 00000000..d035f471 --- /dev/null +++ b/WpfRemote/Converters/StringHasContentToVisibilityConverter.cs @@ -0,0 +1,20 @@ +namespace WpfUtils.Converters; + +using System; +using System.Windows; +using System.Windows.Data; + +[ValueConversion(typeof(string), typeof(Visibility))] +public class StringHasContentToVisibilityConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + string? val = value as string; + return string.IsNullOrEmpty(val) ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/WpfRemote/DependencyProperties/Binder.cs b/WpfRemote/DependencyProperties/Binder.cs new file mode 100644 index 00000000..3ca54785 --- /dev/null +++ b/WpfRemote/DependencyProperties/Binder.cs @@ -0,0 +1,65 @@ +namespace WpfUtils.DependencyProperties; + +using System; +using System.Reflection; +using System.Windows; +using System.Windows.Data; + +public class Binder +{ + public static DependencyProperty Register(string propertyName, BindMode mode) + { + Action callback = (d, e) => { }; + return Register(propertyName, new PropertyChangedCallback(callback), mode); + } + + public static DependencyProperty Register(string propertyName, Action? changed = null, BindMode mode = BindMode.TwoWay) + { + Action callback = (d, e) => + { + if (d is TOwner owner && e.NewValue is TValue value) + { + changed?.Invoke(owner, value); + } + }; + + return Register(propertyName, new PropertyChangedCallback(callback), mode); + } + + public static DependencyProperty Register(string propertyName, Action changed, BindMode mode = BindMode.TwoWay) + { + Action callback = (d, e) => + { + if (d is TOwner owner) + { + TValue oldValue = (TValue)e.OldValue; + TValue newValue = (TValue)e.NewValue; + changed?.Invoke(owner, oldValue, newValue); + } + }; + + return Register(propertyName, new PropertyChangedCallback(callback), mode); + } + + private static DependencyProperty Register(string propertyName, PropertyChangedCallback callback, BindMode mode) + { + PropertyInfo? property = typeof(TOwner).GetProperty(propertyName); + if (property == null) + throw new Exception("Failed to locate property: \"" + propertyName + "\" on type: \"" + typeof(TOwner) + "\" for binding."); + + FrameworkPropertyMetadata meta = new FrameworkPropertyMetadata(new PropertyChangedCallback(callback)); + meta.BindsTwoWayByDefault = mode == BindMode.TwoWay; + meta.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged; + meta.Inherits = true; + DependencyProperty dp = DependencyProperty.Register(propertyName, typeof(TValue), typeof(TOwner), meta); + DependencyProperty dpv = new DependencyProperty(dp); + return dpv; + } +} + +#pragma warning disable SA1201 +public enum BindMode +{ + OneWay, + TwoWay, +} diff --git a/WpfRemote/DependencyProperties/DependencyProperty{TValue}.cs b/WpfRemote/DependencyProperties/DependencyProperty{TValue}.cs new file mode 100644 index 00000000..4f4ddcba --- /dev/null +++ b/WpfRemote/DependencyProperties/DependencyProperty{TValue}.cs @@ -0,0 +1,28 @@ +namespace WpfUtils.DependencyProperties; + +using System.Windows; + +public class DependencyProperty : IBind +{ + private DependencyProperty dp; + + public DependencyProperty(DependencyProperty dp) + { + this.dp = dp; + } + + public TValue Get(DependencyObject control) + { + return (TValue)control.GetValue(this.dp); + } + + public void Set(DependencyObject control, TValue value) + { + TValue old = this.Get(control); + + if (old != null && old.Equals(value)) + return; + + control.SetValue(this.dp, value); + } +} diff --git a/WpfRemote/DependencyProperties/IBind{TValue}.cs b/WpfRemote/DependencyProperties/IBind{TValue}.cs new file mode 100644 index 00000000..51f7130c --- /dev/null +++ b/WpfRemote/DependencyProperties/IBind{TValue}.cs @@ -0,0 +1,9 @@ +namespace WpfUtils.DependencyProperties; + +using System.Windows; + +public interface IBind +{ + TValue Get(DependencyObject control); + void Set(DependencyObject control, TValue value); +} diff --git a/WpfRemote/Extensions/DependencyObjectExtensions.cs b/WpfRemote/Extensions/DependencyObjectExtensions.cs new file mode 100644 index 00000000..c85fbe7a --- /dev/null +++ b/WpfRemote/Extensions/DependencyObjectExtensions.cs @@ -0,0 +1,60 @@ +namespace System.Windows; + +using System.Collections.Generic; +using System.Windows.Media; + +public static class DependencyObjectExtensions +{ + public static T? FindParent(this DependencyObject child) + where T : DependencyObject + { + DependencyObject parentObject = VisualTreeHelper.GetParent(child); + + if (parentObject == null) + return null; + + T? parent = parentObject as T; + + if (parent != null) + { + return parent; + } + else + { + return parentObject.FindParent(); + } + } + + public static T? FindChild(this DependencyObject self) + where T : notnull + { + List results = new List(); + self.FindChildren(ref results); + + if (results.Count == 0) + return default; + + return results[0]; + } + + public static List FindChildren(this DependencyObject self) + { + List results = new List(); + self.FindChildren(ref results); + return results; + } + + public static void FindChildren(this DependencyObject self, ref List results) + { + int children = VisualTreeHelper.GetChildrenCount(self); + for (int i = 0; i < children; i++) + { + DependencyObject? child = VisualTreeHelper.GetChild(self, i); + + if (child is T tChild) + results.Add(tChild); + + child.FindChildren(ref results); + } + } +} diff --git a/WpfRemote/MainWindow.xaml b/WpfRemote/MainWindow.xaml index 60b4dec4..cbcd886d 100644 --- a/WpfRemote/MainWindow.xaml +++ b/WpfRemote/MainWindow.xaml @@ -2,24 +2,27 @@ x:Class="WpfRemote.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:WpfUtils.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:local="clr-namespace:Remote" + xmlns:local="clr-namespace:WpfRemote" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="MainWindow" Width="800" Height="450" + Background="{DynamicResource BackgroundBrush}" + SnapsToDevicePixels="True" + UseLayoutRounding="True" mc:Ignorable="d"> + + + + - - - - - @@ -27,31 +30,19 @@ - - - + X="{Binding PositionX}" + Y="{Binding PositionY}" + Z="{Binding PositionZ}" /> - - - + X="{Binding ScaleX}" + Y="{Binding ScaleY}" + Z="{Binding ScaleZ}" /> + + diff --git a/WpfRemote/MainWindow.xaml.cs b/WpfRemote/MainWindow.xaml.cs index e75fdd38..4f3750d9 100644 --- a/WpfRemote/MainWindow.xaml.cs +++ b/WpfRemote/MainWindow.xaml.cs @@ -1,7 +1,9 @@ namespace WpfRemote; using Brio.Remote; +using System.ComponentModel; using System.Windows; +using PropertyChanged.SourceGenerator; public partial class MainWindow : Window { @@ -15,26 +17,41 @@ public MainWindow() _remoteService.OnMessageCallback = OnMessage; } + protected MainWindowViewModel ViewModel => (MainWindowViewModel)this.DataContext; + private void OnMessage(object obj) { if (obj is BoneMessage bm) { + if (Dispatcher.HasShutdownStarted) + return; + Dispatcher?.Invoke(() => { - DisplayBone(bm); - + ViewModel.PositionX = bm.PositionX; + ViewModel.PositionY = bm.PositionY; + ViewModel.PositionZ = bm.PositionZ; + ViewModel.ScaleX = bm.ScaleX; + ViewModel.ScaleY = bm.ScaleY; + ViewModel.ScaleZ = bm.ScaleZ; + + //RotationX = bm.RotationX; + //RotationY = bm.RotationY; + //RotationZ = bm.RotationZ; }); } } - - public void DisplayBone(BoneMessage bm) - { - this.PosXText.Text = bm.PositionX.ToString(); - this.PosYText.Text = bm.PositionY.ToString(); - this.PosZText.Text = bm.PositionZ.ToString(); - - this.ScaleXText.Text = bm.ScaleX.ToString(); - this.ScaleYText.Text = bm.ScaleY.ToString(); - this.ScaleZText.Text = bm.ScaleZ.ToString(); - } } + +public partial class MainWindowViewModel +{ + [Notify] public float positionX = 0; + [Notify] public float positionY = 0; + [Notify] public float positionZ = 0; + [Notify] public float scaleX = 0; + [Notify] public float scaleY = 0; + [Notify] public float scaleZ = 0; + [Notify] public float rotationX = 0; + [Notify] public float rotationY = 0; + [Notify] public float rotationZ = 0; +} \ No newline at end of file diff --git a/WpfRemote/Styles/BorderStyles.xaml b/WpfRemote/Styles/BorderStyles.xaml new file mode 100644 index 00000000..b1178e0d --- /dev/null +++ b/WpfRemote/Styles/BorderStyles.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/ButtonStyles.xaml b/WpfRemote/Styles/ButtonStyles.xaml new file mode 100644 index 00000000..676eef3a --- /dev/null +++ b/WpfRemote/Styles/ButtonStyles.xaml @@ -0,0 +1,231 @@ + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/CheckBoxStyles.xaml b/WpfRemote/Styles/CheckBoxStyles.xaml new file mode 100644 index 00000000..874763ac --- /dev/null +++ b/WpfRemote/Styles/CheckBoxStyles.xaml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/ComboBoxStyles.xaml b/WpfRemote/Styles/ComboBoxStyles.xaml new file mode 100644 index 00000000..7b88710e --- /dev/null +++ b/WpfRemote/Styles/ComboBoxStyles.xaml @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/ExpanderStyles.xaml b/WpfRemote/Styles/ExpanderStyles.xaml new file mode 100644 index 00000000..8b8c000e --- /dev/null +++ b/WpfRemote/Styles/ExpanderStyles.xaml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/GroupBoxStyles.xaml b/WpfRemote/Styles/GroupBoxStyles.xaml new file mode 100644 index 00000000..2b545a60 --- /dev/null +++ b/WpfRemote/Styles/GroupBoxStyles.xaml @@ -0,0 +1,75 @@ + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/ListBoxStyles.xaml b/WpfRemote/Styles/ListBoxStyles.xaml new file mode 100644 index 00000000..3cc27a70 --- /dev/null +++ b/WpfRemote/Styles/ListBoxStyles.xaml @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/ProgressBarStyles.xaml b/WpfRemote/Styles/ProgressBarStyles.xaml new file mode 100644 index 00000000..4cce2809 --- /dev/null +++ b/WpfRemote/Styles/ProgressBarStyles.xaml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/ScrollBarStyles.xaml b/WpfRemote/Styles/ScrollBarStyles.xaml new file mode 100644 index 00000000..45dd0c07 --- /dev/null +++ b/WpfRemote/Styles/ScrollBarStyles.xaml @@ -0,0 +1,119 @@ + + + 12 + 12 + 48 + 48 + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/SliderStyles.xaml b/WpfRemote/Styles/SliderStyles.xaml new file mode 100644 index 00000000..23aad36d --- /dev/null +++ b/WpfRemote/Styles/SliderStyles.xaml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/TabControlStyles.xaml b/WpfRemote/Styles/TabControlStyles.xaml new file mode 100644 index 00000000..eddbab8d --- /dev/null +++ b/WpfRemote/Styles/TabControlStyles.xaml @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/TextBlockStyles.xaml b/WpfRemote/Styles/TextBlockStyles.xaml new file mode 100644 index 00000000..17ff3215 --- /dev/null +++ b/WpfRemote/Styles/TextBlockStyles.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/TextBoxStyles.xaml b/WpfRemote/Styles/TextBoxStyles.xaml new file mode 100644 index 00000000..d87779e8 --- /dev/null +++ b/WpfRemote/Styles/TextBoxStyles.xaml @@ -0,0 +1,142 @@ + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/ToggleButtonStyles.xaml b/WpfRemote/Styles/ToggleButtonStyles.xaml new file mode 100644 index 00000000..e5a9480d --- /dev/null +++ b/WpfRemote/Styles/ToggleButtonStyles.xaml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Styles/WindowStyles.xaml b/WpfRemote/Styles/WindowStyles.xaml new file mode 100644 index 00000000..cf2a6888 --- /dev/null +++ b/WpfRemote/Styles/WindowStyles.xaml @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/WpfRemote/Themes/Dark.xaml b/WpfRemote/Themes/Dark.xaml new file mode 100644 index 00000000..f3d186f3 --- /dev/null +++ b/WpfRemote/Themes/Dark.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WpfRemote/Utility/Dispatch.cs b/WpfRemote/Utility/Dispatch.cs new file mode 100644 index 00000000..db0df367 --- /dev/null +++ b/WpfRemote/Utility/Dispatch.cs @@ -0,0 +1,70 @@ +namespace WpfUtils; + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; + +public static class Dispatch +{ + ////public static SwitchToUiAwaitable MainThread() => new(); + public static SwitchFromUiAwaitable NonUiThread() => new(); + + public static SwitchToMainThreadAwaitable MainThread(this DispatcherObject self) => new(self.Dispatcher); + public static SwitchToMainThreadAwaitable MainThread(this Dispatcher self) => new(self); + + public struct SwitchToMainThreadAwaitable : INotifyCompletion + { + private readonly Dispatcher dispatch; + + public SwitchToMainThreadAwaitable(Dispatcher dispatcher) + { + this.dispatch = dispatcher; + } + + public bool IsCompleted => this.dispatch?.CheckAccess() == true; + + public SwitchToMainThreadAwaitable GetAwaiter() => this; + public void GetResult() + { + } + + public void OnCompleted(Action continuation) + { + this.dispatch.BeginInvoke(continuation); + } + } + + public struct SwitchFromUiAwaitable : INotifyCompletion + { + private readonly Dispatcher? dispatch; + + public SwitchFromUiAwaitable() + { + this.dispatch = Dispatcher.FromThread(Thread.CurrentThread); + + if (this.dispatch == null) + { + this.dispatch = Application.Current?.Dispatcher; + } + } + + public bool IsCompleted => this.dispatch?.CheckAccess() == false; + + public SwitchFromUiAwaitable GetAwaiter() + { + return this; + } + + public void GetResult() + { + } + + public void OnCompleted(Action continuation) + { + Task.Run(continuation); + } + } +} diff --git a/WpfRemote/WpfRemote.csproj b/WpfRemote/WpfRemote.csproj index 0988e9ec..e03d4e33 100644 --- a/WpfRemote/WpfRemote.csproj +++ b/WpfRemote/WpfRemote.csproj @@ -5,8 +5,16 @@ net7.0-windows enable true + True + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + From 4805c5f5e0bd808c29f359536905f2a1e4104c91 Mon Sep 17 00:00:00 2001 From: Yuki Date: Sat, 3 Feb 2024 21:44:52 +1100 Subject: [PATCH 5/6] show bone name --- Brio/Remote/BoneMessage.cs | 3 +++ WpfRemote/MainWindow.xaml | 22 +++++++++++++++++----- WpfRemote/MainWindow.xaml.cs | 4 ++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Brio/Remote/BoneMessage.cs b/Brio/Remote/BoneMessage.cs index fcd34258..8f1b2de7 100644 --- a/Brio/Remote/BoneMessage.cs +++ b/Brio/Remote/BoneMessage.cs @@ -21,6 +21,9 @@ public class BoneMessage internal void FromBone(Bone bone) { + this.Name = bone.Name; + this.DisplayName = bone.FriendlyName; + this.PositionX = bone.LastTransform.Position.X; this.PositionY = bone.LastTransform.Position.Y; this.PositionZ = bone.LastTransform.Position.Z; diff --git a/WpfRemote/MainWindow.xaml b/WpfRemote/MainWindow.xaml index cbcd886d..1c51443d 100644 --- a/WpfRemote/MainWindow.xaml +++ b/WpfRemote/MainWindow.xaml @@ -25,24 +25,36 @@ + - - + + + - + + + + - + diff --git a/WpfRemote/MainWindow.xaml.cs b/WpfRemote/MainWindow.xaml.cs index 4f3750d9..6ce97775 100644 --- a/WpfRemote/MainWindow.xaml.cs +++ b/WpfRemote/MainWindow.xaml.cs @@ -28,6 +28,8 @@ private void OnMessage(object obj) Dispatcher?.Invoke(() => { + ViewModel.BoneName = bm.Name ?? string.Empty; + ViewModel.BoneDisplayName = bm.DisplayName ?? string.Empty; ViewModel.PositionX = bm.PositionX; ViewModel.PositionY = bm.PositionY; ViewModel.PositionZ = bm.PositionZ; @@ -45,6 +47,8 @@ private void OnMessage(object obj) public partial class MainWindowViewModel { + [Notify] public string boneName = string.Empty; + [Notify] public string boneDisplayName = string.Empty; [Notify] public float positionX = 0; [Notify] public float positionY = 0; [Notify] public float positionZ = 0; From e381ecf21692c216fc04b9adbf02787e92b9fc93 Mon Sep 17 00:00:00 2001 From: Yuki Date: Sat, 3 Feb 2024 22:22:21 +1100 Subject: [PATCH 6/6] namespaces --- Brio.sln | 13 +- Brio/Remote/RemoteService.cs | 2 +- WpfRemote/3D/Cylinder.cs | 2 +- .../3D/Extensions/HkQuaternionExtensions.cs | 109 ++++ WpfRemote/3D/Extensions/HkVectorExtensions.cs | 19 + WpfRemote/3D/Lines/Circle.cs | 2 +- WpfRemote/3D/Lines/Line.cs | 2 +- WpfRemote/3D/MathUtils.cs | 2 +- WpfRemote/3D/Matrix3DStack.cs | 2 +- WpfRemote/3D/PrsTransform.cs | 2 +- WpfRemote/3D/Sphere.cs | 2 +- WpfRemote/App.xaml | 8 +- WpfRemote/Controls/Gizmo.cs | 557 ++++++++++++++++++ WpfRemote/Controls/MultiNumberBox.cs | 6 +- WpfRemote/Controls/MultiNumberBoxStyles.xaml | 2 +- WpfRemote/Controls/NumberBox.xaml | 6 +- WpfRemote/Controls/NumberBox.xaml.cs | 4 +- WpfRemote/Controls/RelativeSlider.cs | 4 +- WpfRemote/Controls/Slider.cs | 2 +- .../Converters/AbsoluteNumberConverter.cs | 2 +- .../AnyBoolIsFalseToBoolMultiConverter.cs | 2 +- .../Converters/BoolInversionConverter.cs | 2 +- WpfRemote/Converters/BoolToIntConverter.cs | 2 +- WpfRemote/Converters/ColorToBrushConverter.cs | 2 +- WpfRemote/Converters/ConverterBase.cs | 2 +- WpfRemote/Converters/EnumToBoolConverter.cs | 2 +- .../Converters/EnumToVisibilityConverter.cs | 2 +- .../Converters/FloatToDoubleConverter.cs | 2 +- .../GreaterThanToVisibilityConverter.cs | 2 +- .../InvertedBoolToVisibilityConverter.cs | 2 +- .../IsEmptyToVisibilityConverter.cs | 2 +- WpfRemote/Converters/IsZeroToBoolConverter.cs | 2 +- .../Converters/IsZeroToVisibilityConverter.cs | 2 +- .../Converters/LessThanToBoolConverter.cs | 2 +- .../LessThanToVisibilityConverter.cs | 2 +- WpfRemote/Converters/ListToStringConverter.cs | 2 +- WpfRemote/Converters/MultiBoolAndConverter.cs | 2 +- .../MultiBoolAndToVisibilityConverter.cs | 2 +- WpfRemote/Converters/MultiBoolOrConverter.cs | 2 +- .../MultiBoolOrToVisibilityConverter.cs | 2 +- .../NotEmptyToVisibilityConverter.cs | 2 +- .../Converters/NotNullToBoolConverter.cs | 2 +- .../NotNullToVisibilityConverter.cs | 2 +- .../Converters/NotZeroToBoolConverter.cs | 2 +- .../NotZeroToVisibilityConverter.cs | 2 +- WpfRemote/Converters/NullToBoolConverter.cs | 2 +- .../Converters/NullToVisibilityConverter.cs | 2 +- WpfRemote/Converters/NumberConverter.cs | 2 +- .../Converters/NumberToThicknessConverter.cs | 2 +- .../Converters/RadiansToDegreesConverter.cs | 2 +- .../StringHasContentToBoolConverter.cs | 2 +- .../StringHasContentToVisibilityConverter.cs | 2 +- WpfRemote/DependencyProperties/Binder.cs | 2 +- .../DependencyProperty{TValue}.cs | 2 +- .../DependencyProperties/IBind{TValue}.cs | 2 +- WpfRemote/MainWindow.xaml | 26 +- WpfRemote/MainWindow.xaml.cs | 23 +- WpfRemote/Styles/ButtonStyles.xaml | 2 +- WpfRemote/Styles/CheckBoxStyles.xaml | 2 +- WpfRemote/Styles/ComboBoxStyles.xaml | 4 +- WpfRemote/Styles/GroupBoxStyles.xaml | 2 +- WpfRemote/Styles/ListBoxStyles.xaml | 4 +- WpfRemote/Styles/ProgressBarStyles.xaml | 2 +- WpfRemote/Styles/TabControlStyles.xaml | 4 +- WpfRemote/Utility/Dispatch.cs | 2 +- WpfRemote/WpfRemote.csproj | 57 +- 66 files changed, 836 insertions(+), 110 deletions(-) create mode 100644 WpfRemote/3D/Extensions/HkQuaternionExtensions.cs create mode 100644 WpfRemote/3D/Extensions/HkVectorExtensions.cs create mode 100644 WpfRemote/Controls/Gizmo.cs diff --git a/Brio.sln b/Brio.sln index 76956c1b..ee494f70 100644 --- a/Brio.sln +++ b/Brio.sln @@ -23,14 +23,11 @@ Global {6E14631E-8223-427D-8A03-550EEE66B842}.Release|Any CPU.Build.0 = Release|x64 {6E14631E-8223-427D-8A03-550EEE66B842}.Release|x64.ActiveCfg = Release|x64 {6E14631E-8223-427D-8A03-550EEE66B842}.Release|x64.Build.0 = Release|x64 - {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|x64.ActiveCfg = Debug|Any CPU - {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|x64.Build.0 = Debug|Any CPU - {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|Any CPU.Build.0 = Release|Any CPU - {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|x64.ActiveCfg = Release|Any CPU - {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|x64.Build.0 = Release|Any CPU + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|Any CPU.ActiveCfg = Debug|x64 + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|x64.ActiveCfg = Debug|x64 + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Debug|x64.Build.0 = Debug|x64 + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|Any CPU.ActiveCfg = Release|x64 + {7A54ABF2-1053-4446-AA79-4ADB666184D5}.Release|x64.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Brio/Remote/RemoteService.cs b/Brio/Remote/RemoteService.cs index a11cac4d..f0e7e778 100644 --- a/Brio/Remote/RemoteService.cs +++ b/Brio/Remote/RemoteService.cs @@ -12,7 +12,7 @@ namespace Brio.Remote; internal class RemoteService : IDisposable { - public const int SyncMs = 100; + public const int SyncMs = 33; private readonly EntityManager _entityManager; private EasyTcpServer? _server; diff --git a/WpfRemote/3D/Cylinder.cs b/WpfRemote/3D/Cylinder.cs index a1f69c34..95c83d58 100644 --- a/WpfRemote/3D/Cylinder.cs +++ b/WpfRemote/3D/Cylinder.cs @@ -1,4 +1,4 @@ -namespace WpfUtils.Meida3D; +namespace WpfRemote.Meida3D; using System; using System.Windows.Media.Media3D; diff --git a/WpfRemote/3D/Extensions/HkQuaternionExtensions.cs b/WpfRemote/3D/Extensions/HkQuaternionExtensions.cs new file mode 100644 index 00000000..2606def4 --- /dev/null +++ b/WpfRemote/3D/Extensions/HkQuaternionExtensions.cs @@ -0,0 +1,109 @@ +namespace FFXIVClientStructs.Havok; + +using System; +using System.Numerics; + +public static class HkQuaternionExtensions +{ + private static readonly float Deg2Rad = ((float)Math.PI * 2) / 360; + private static readonly float Rad2Deg = 360 / ((float)Math.PI * 2); + + public static hkQuaternionf New(float x, float y, float z, float w) + { + hkQuaternionf v = default; + v.X = x; + v.Y = y; + v.Z = z; + v.W = w; + return v; + } + + public static Quaternion ToQuaternion(this hkQuaternionf q) => new Quaternion(q.X, q.Y, q.Z, q.W); + public static hkQuaternionf ToHavok(this Quaternion q) => new hkQuaternionf + { + X = q.X, + Y = q.Y, + Z = q.Z, + W = q.W, + }; + + public static hkQuaternionf FromQuaternion(this hkQuaternionf tar, Quaternion q) + { + tar.X = q.X; + tar.Y = q.Y; + tar.Z = q.Z; + tar.W = q.W; + return tar; + } + + public static hkQuaternionf FromEuler(hkVector4f euler) + { + float yaw = euler.Y * Deg2Rad; + float pitch = euler.X * Deg2Rad; + float roll = euler.Z * Deg2Rad; + + float c1 = MathF.Cos(yaw / 2); + float s1 = MathF.Sin(yaw / 2); + float c2 = MathF.Cos(pitch / 2); + float s2 = MathF.Sin(pitch / 2); + float c3 = MathF.Cos(roll / 2); + float s3 = MathF.Sin(roll / 2); + + float c1c2 = c1 * c2; + float s1s2 = s1 * s2; + + hkQuaternionf v = default; + v.X = (c1c2 * s3) + (s1s2 * c3); + v.Y = (s1 * c2 * c3) + (c1 * s2 * s3); + v.Z = (c1 * s2 * c3) - (s1 * c2 * s3); + v.W = (c1c2 * c3) - (s1s2 * s3); + return v; + } + + public static hkVector4f ToEuler(this hkQuaternionf self) + { + hkVector4f v = default; + + double test = (self.X * self.Y) + (self.Z * self.W); + + if (test > 0.4995f) + { + v.Y = 2f * (float)Math.Atan2(self.X, self.Y); + v.X = (float)Math.PI / 2; + v.Z = 0; + } + else if (test < -0.4995f) + { + v.Y = -2f * (float)Math.Atan2(self.X, self.W); + v.X = -(float)Math.PI / 2; + v.Z = 0; + } + else + { + double sqx = self.X * self.X; + double sqy = self.Y * self.Y; + double sqz = self.Z * self.Z; + + v.Y = (float)Math.Atan2((2 * self.Y * self.W) - (2 * self.X * self.Z), 1 - (2 * sqy) - (2 * sqz)); + v.X = (float)Math.Asin(2 * test); + v.Z = (float)Math.Atan2((2 * self.X * self.W) - (2 * self.Y * self.Z), 1 - (2 * sqx) - (2 * sqz)); + } + + v.X = NormalizeAngle(v.X * Rad2Deg); + v.Y = NormalizeAngle(v.Y * Rad2Deg); + v.Z = NormalizeAngle(v.Z * Rad2Deg); + + return v; + } + + private static float NormalizeAngle(float angle) + { + while (angle > 360) + angle -= 360; + + while (angle < 0) + angle += 360; + + return angle; + } +} diff --git a/WpfRemote/3D/Extensions/HkVectorExtensions.cs b/WpfRemote/3D/Extensions/HkVectorExtensions.cs new file mode 100644 index 00000000..2c5b3a6b --- /dev/null +++ b/WpfRemote/3D/Extensions/HkVectorExtensions.cs @@ -0,0 +1,19 @@ +namespace FFXIVClientStructs.Havok; + +using System.Numerics; + +public static class HkVectorExtensions +{ + public static Vector3 ToVector3(this hkVector4f vec) => new Vector3(vec.X, vec.Y, vec.Z); + public static Vector4 ToVector4(this hkVector4f vec) => new Vector4(vec.X, vec.Y, vec.Z, vec.W); + + public static hkVector4f ToHavok(this Vector3 v) => new hkVector4f { X = v.X, Y = v.Y, Z = v.Z, W = 1 }; + + public static hkVector4f SetFromVector3(this hkVector4f tar, Vector3 vec) + { + tar.X = vec.X; + tar.Y = vec.Y; + tar.Z = vec.Z; + return tar; + } +} diff --git a/WpfRemote/3D/Lines/Circle.cs b/WpfRemote/3D/Lines/Circle.cs index 8cbb2fb4..0f10aa2c 100644 --- a/WpfRemote/3D/Lines/Circle.cs +++ b/WpfRemote/3D/Lines/Circle.cs @@ -1,4 +1,4 @@ -namespace WpfUtils.Meida3D.Lines; +namespace WpfRemote.Meida3D.Lines; using System; using System.Windows.Media.Media3D; diff --git a/WpfRemote/3D/Lines/Line.cs b/WpfRemote/3D/Lines/Line.cs index b503a6b9..0cb9d1fe 100644 --- a/WpfRemote/3D/Lines/Line.cs +++ b/WpfRemote/3D/Lines/Line.cs @@ -1,4 +1,4 @@ -namespace WpfUtils.Meida3D; +namespace WpfRemote.Meida3D; using System; using System.Windows; diff --git a/WpfRemote/3D/MathUtils.cs b/WpfRemote/3D/MathUtils.cs index 89164a10..61d0d198 100644 --- a/WpfRemote/3D/MathUtils.cs +++ b/WpfRemote/3D/MathUtils.cs @@ -1,4 +1,4 @@ -namespace WpfUtils.Meida3D; +namespace WpfRemote.Meida3D; using System; using System.Diagnostics; diff --git a/WpfRemote/3D/Matrix3DStack.cs b/WpfRemote/3D/Matrix3DStack.cs index d593929a..a0d400e7 100644 --- a/WpfRemote/3D/Matrix3DStack.cs +++ b/WpfRemote/3D/Matrix3DStack.cs @@ -1,4 +1,4 @@ -namespace WpfUtils.Meida3D; +namespace WpfRemote.Meida3D; using System; using System.Collections; diff --git a/WpfRemote/3D/PrsTransform.cs b/WpfRemote/3D/PrsTransform.cs index 436c4782..5eb28ce6 100644 --- a/WpfRemote/3D/PrsTransform.cs +++ b/WpfRemote/3D/PrsTransform.cs @@ -1,4 +1,4 @@ -namespace WpfUtils.Meida3D; +namespace WpfRemote.Meida3D; using System.Windows.Media.Media3D; diff --git a/WpfRemote/3D/Sphere.cs b/WpfRemote/3D/Sphere.cs index bd92a0c6..cdd32177 100644 --- a/WpfRemote/3D/Sphere.cs +++ b/WpfRemote/3D/Sphere.cs @@ -1,4 +1,4 @@ -namespace WpfUtils.Meida3D; +namespace WpfRemote.Meida3D; using System; using System.Windows; diff --git a/WpfRemote/App.xaml b/WpfRemote/App.xaml index db98d8ff..65cd28ed 100644 --- a/WpfRemote/App.xaml +++ b/WpfRemote/App.xaml @@ -2,8 +2,8 @@ x:Class="Remote.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:controls="clr-namespace:WpfUtils.Controls" - xmlns:converters="clr-namespace:WpfUtils.Converters" + xmlns:controls="clr-namespace:WpfRemote.Controls" + xmlns:converters="clr-namespace:WpfRemote.Converters" xmlns:local="clr-namespace:Remote" StartupUri="MainWindow.xaml"> @@ -79,10 +79,6 @@ BasedOn="{StaticResource WpfUtilsCheckBox}" TargetType="{x:Type CheckBox}" /> -