Browse Source

Merge remote-tracking branch 'origin/kenric' into frank

frankvandenbos 2 days ago
parent
commit
d86413d3d8

+ 124 - 79
InABox.Avalonia/Components/ImageEditor/ImageEditor.axaml

@@ -4,19 +4,86 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 			 xmlns:components="using:InABox.Avalonia.Components"
 			 xmlns:converters="using:InABox.Avalonia.Converters"
+			 xmlns:image="using:InABox.Avalonia.Components.ImageEditing"
 			 xmlns:system="using:System"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="InABox.Avalonia.Components.ImageEditor">
+	<UserControl.Styles>
+		<Style Selector="Button">
+			<Setter Property="Background"
+					Value="Transparent"/>
+			<Setter Property="ClipToBounds"
+					Value="False"/>
+		</Style>
+		<Style Selector="Button:flyout-open /template/ ContentPresenter">
+			<Setter Property="BoxShadow"
+					Value="0 0 10 DarkGray"/>
+		</Style>
+		<Style Selector="Button.active /template/ ContentPresenter">
+			<Setter Property="BoxShadow"
+					Value="0 0 10 DarkGray"/>
+		</Style>
+		<Style Selector="Button:disabled">
+			<Setter Property="Foreground"
+					Value="#909090"/>
+		</Style>
+		<Style Selector="Button:disabled /template/ ContentPresenter">
+			<Setter Property="Background"
+					Value="Transparent"/>
+		</Style>
+		<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
+			<Setter Property="BorderBrush" Value="{Binding $parent[Button].BorderBrush}" />
+			<Setter Property="BorderThickness" Value="{Binding $parent[Button].BorderThickness}" />
+			<Setter Property="Background" Value="{Binding $parent[Button].Background}" />
+			<Setter Property="Foreground" Value="{Binding $parent[Button].Foreground}" />
+		</Style>
+		
+		<Style Selector="FlyoutPresenter">
+			<Setter Property="Padding" Value="5"/>
+			<Setter Property="Background" Value="Transparent"/>
+			<Setter Property="BorderThickness" Value="0"/>
+			<Setter Property="CornerRadius" Value="0"/>
+			<Setter Property="ClipToBounds" Value="False"/>
+		</Style>
+		<Style Selector="FlyoutPresenter > Border">
+			<Setter Property="Padding" Value="{StaticResource PrsControlSpacing}"/>
+			<Setter Property="BorderBrush" Value="Black"/>
+			<Setter Property="BorderThickness" Value="0"/>
+			<Setter Property="Background" Value="White"/>
+			<Setter Property="BoxShadow" Value="0 0 10 Gray"/>
+		</Style>
+		<Style Selector="Rectangle.Separator">
+			<Setter Property="Fill" Value="Gray"/>
+			<Setter Property="Width" Value="1"/>
+			<Setter Property="VerticalAlignment" Value="Stretch"/>
+			<Setter Property="Margin" Value="0,10"/>
+		</Style>
+	</UserControl.Styles>
 	<UserControl.Resources>
 		<converters:DoubleCalculator x:Key="Add1Calculator" Constants="1.0" Type="Sum"/>
 	</UserControl.Resources>
 	<Border CornerRadius="{Binding $parent[components:ImageEditor].CornerRadius}" ClipToBounds="True">
 		<Grid>
 			<Grid.RowDefinitions>
+				<RowDefinition Height="Auto"/>
 				<RowDefinition Height="*"/>
 				<RowDefinition Height="Auto"/>
 			</Grid.RowDefinitions>
-			<Canvas Name="OuterCanvas" Background="White">
+			<Canvas Name="OuterCanvas" Grid.Row="1" Background="White" PointerWheelChanged="OuterCanvas_PointerWheelChanged">
+				<Canvas.GestureRecognizers>
+					<image:PanAndZoomGestureRecognizer/>
+				</Canvas.GestureRecognizers>
+				<Canvas.Styles>
+					<Style Selector="Thumb">
+						<Setter Property="Template">
+							<ControlTemplate>
+								<Border Background="LightGray"
+										BorderBrush="Black"
+										BorderThickness="1"/>
+							</ControlTemplate>
+						</Setter>
+					</Style>
+				</Canvas.Styles>
 				<Border Name="ImageBorder" Background="White"
 						BoxShadow="0 0 10 Gray">
 					<Grid>
@@ -30,37 +97,41 @@
 					</Grid>
 				</Border>
 			</Canvas>
-			<Border Grid.Row="1"
+			<Border Grid.Row="0"
+					BoxShadow="0 0 10 Gray"
+					IsVisible="{Binding $parent[components:ImageEditor].ShowButtons}">
+				<Border ClipToBounds="True"
+						Background="White">
+					<Grid>
+						<Grid.ColumnDefinitions>
+							<ColumnDefinition Width="*"/>
+							<ColumnDefinition Width="Auto"/>
+							<ColumnDefinition Width="*"/>
+						</Grid.ColumnDefinitions>
+						<ItemsControl Grid.Column="1" ItemsSource="{Binding $parent[components:ImageEditor].ModeButtons}">
+							<ItemsControl.ItemTemplate>
+								<DataTemplate DataType="components:ImageEditorModeButton">
+									<Button Classes.active="{Binding Active}"
+											Width="40" Height="40" Margin="10"
+											CommandParameter="{Binding Mode}"
+											Command="{Binding $parent[components:ImageEditor].SetModeCommand}"
+											Content="{Binding Content}"/>
+								</DataTemplate>
+							</ItemsControl.ItemTemplate>
+							<ItemsControl.ItemsPanel>
+								<ItemsPanelTemplate>
+									<UniformGrid Margin="-10" Rows="1"/>
+								</ItemsPanelTemplate>
+							</ItemsControl.ItemsPanel>
+						</ItemsControl>
+					</Grid>
+				</Border>
+			</Border>
+			<Border Grid.Row="2"
 					BoxShadow="0 0 10 Gray"
 					IsVisible="{Binding $parent[components:ImageEditor].ShowButtons}">
 				<Border ClipToBounds="True"
 						Background="White">
-					<Border.Styles>
-						<Style Selector="Button">
-							<Setter Property="Background"
-									Value="Transparent"/>
-							<Setter Property="ClipToBounds"
-									Value="False"/>
-						</Style>
-						<Style Selector="Button:flyout-open /template/ ContentPresenter">
-							<Setter Property="BoxShadow"
-									Value="0 0 10 DarkGray"/>
-						</Style>
-						<Style Selector="Button:disabled">
-							<Setter Property="Foreground"
-									Value="#909090"/>
-						</Style>
-						<Style Selector="Button:disabled /template/ ContentPresenter">
-							<Setter Property="Background"
-									Value="Transparent"/>
-						</Style>
-						<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
-							<Setter Property="BorderBrush" Value="{Binding $parent[Button].BorderBrush}" />
-							<Setter Property="BorderThickness" Value="{Binding $parent[Button].BorderThickness}" />
-							<Setter Property="Background" Value="{Binding $parent[Button].Background}" />
-							<Setter Property="Foreground" Value="{Binding $parent[Button].Foreground}" />
-						</Style>
-					</Border.Styles>
 					<Grid>
 						<Grid.ColumnDefinitions>
 							<ColumnDefinition Width="*"/>
@@ -69,65 +140,39 @@
 						</Grid.ColumnDefinitions>
 						<StackPanel Grid.Column="1" Margin="-10"
 									Orientation="Horizontal">
-							<StackPanel.Styles>
-								<!--
-								<Style Selector="FlyoutPresenter">
-									<Setter Property="Padding" Value="{StaticResource PrsControlSpacing}"/>
-									<Setter Property="CornerRadius" Value="{StaticResource PrsCornerRadius}"/>
-									<Setter Property="BorderBrush" Value="Black"/>
-									<Setter Property="MinWidth" Value="0"/>
-									<Setter Property="Background" Value="White"/>
-								</Style>
-								-->
-								<Style Selector="FlyoutPresenter">
-									<Setter Property="Padding" Value="5"/>
-									<Setter Property="Background" Value="Transparent"/>
-									<Setter Property="BorderThickness" Value="0"/>
-									<Setter Property="CornerRadius" Value="0"/>
-									<Setter Property="ClipToBounds" Value="False"/>
-								</Style>
-								<Style Selector="FlyoutPresenter > Border">
-									<Setter Property="Padding" Value="{StaticResource PrsControlSpacing}"/>
-									<Setter Property="BorderBrush" Value="Black"/>
-									<Setter Property="BorderThickness" Value="0"/>
-									<Setter Property="Background" Value="White"/>
-									<Setter Property="BoxShadow" Value="0 0 10 Gray"/>
-								</Style>
-								<Style Selector="Rectangle.Separator">
-									<Setter Property="Fill" Value="Gray"/>
-									<Setter Property="Width" Value="1"/>
-									<Setter Property="VerticalAlignment" Value="Stretch"/>
-									<Setter Property="Margin" Value="0,10"/>
-								</Style>
-							</StackPanel.Styles>
-							<Button Name="ShapeButton"
-									Width="40" Height="40"
+							<Button Name="FontSizeButton" Width="40" Height="40"
 									Margin="10">
+								<Canvas Width="25" Height="25"
+										HorizontalAlignment="Center" VerticalAlignment="Center">
+									<TextBlock Text="A" FontSize="20"
+											   Canvas.Left="2"
+											   Canvas.Bottom="1"/>
+									<TextBlock Text="A" FontSize="10"
+											   Canvas.Left="15"
+											   Canvas.Bottom="3"/>
+								</Canvas>
 								<Button.Flyout>
 									<Flyout Placement="Top" VerticalOffset="0">
 										<Border>
-											<ItemsControl ItemsSource="{Binding $parent[components:ImageEditor].ModeButtons}">
-												<ItemsControl.ItemTemplate>
-													<DataTemplate DataType="components:ImageEditorModeButton">
-														<Button Width="40" Height="40" Margin="10"
-																CommandParameter="{Binding Mode}"
-																Command="{Binding $parent[components:ImageEditor].SetModeCommand}"
-																Content="{Binding Canvas}"/>
-													</DataTemplate>
-												</ItemsControl.ItemTemplate>
-												<ItemsControl.ItemsPanel>
-													<ItemsPanelTemplate>
-														<UniformGrid Margin="-10" Rows="1"/>
-													</ItemsPanelTemplate>
-												</ItemsControl.ItemsPanel>
-											</ItemsControl>
+											<Grid>
+												<Grid.RowDefinitions>
+													<RowDefinition Height="40"/>
+													<RowDefinition Height="Auto"/>
+												</Grid.RowDefinitions>
+												<TextBlock FontSize="{Binding $parent[components:ImageEditor].FontSize}"
+														   Text="Aa"
+														   VerticalAlignment="Center"
+														   TextAlignment="Center"/>
+												<Slider Value="{Binding $parent[components:ImageEditor].FontSize}"
+														Grid.Row="1"
+														Minimum="5.0"
+														Maximum="40.0"
+														Width="150"/>
+											</Grid>
 										</Border>
 									</Flyout>
 								</Button.Flyout>
 							</Button>
-							
-							<Rectangle Classes="Separator"/>
-							
 							<Button Name="LineThicknessButton" Width="40" Height="40"
 									Margin="10">
 								<Canvas Width="25" Height="21"

+ 380 - 40
InABox.Avalonia/Components/ImageEditor/ImageEditor.axaml.cs

@@ -7,6 +7,7 @@ using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
+using Avalonia.Layout;
 using Avalonia.Skia.Helpers;
 using CommunityToolkit.Mvvm.Input;
 using FluentResults;
@@ -15,6 +16,10 @@ using InABox.Avalonia.Converters;
 using InABox.Core;
 using SkiaSharp;
 using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using Avalonia.LogicalTree;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.Maui.Devices;
 
 namespace InABox.Avalonia.Components;
 
@@ -22,14 +27,19 @@ public enum ImageEditingMode
 {
     Polyline,
     Rectangle,
-    Ellipse
+    Ellipse,
+    Text,
+    Dimension
 }
 
-public class ImageEditorModeButton(ImageEditingMode mode, Canvas? canvas)
+public partial class ImageEditorModeButton(ImageEditingMode mode, Control? content, bool active) : ObservableObject
 {
     public ImageEditingMode Mode { get; set; } = mode;
 
-    public Canvas? Canvas { get; set; } = canvas;
+    public Control? Content { get; set; } = content;
+
+    [ObservableProperty]
+    private bool _active = active;
 }
 
 public class ImageEditorTransparentImageBrushConverter : AbstractConverter<IBrush?, IBrush?>
@@ -113,6 +123,9 @@ public partial class ImageEditor : UserControl
     public static readonly StyledProperty<bool> ShowButtonsProperty =
         AvaloniaProperty.Register<ImageEditor, bool>(nameof(ShowButtons), true);
 
+    public static readonly StyledProperty<double> FontSizeValueProperty =
+        AvaloniaProperty.Register<ImageEditor, double>(nameof(FontSizeValue), 12);
+
     public IImage? Source
     {
         get => GetValue(SourceProperty);
@@ -163,6 +176,12 @@ public partial class ImageEditor : UserControl
         set => SetValue(LineThicknessProperty, value);
     }
 
+    public double FontSizeValue
+    {
+        get => GetValue(FontSizeValueProperty);
+        set => SetValue(FontSizeValueProperty, value);
+    }
+
     #endregion
 
     #region Events
@@ -177,11 +196,24 @@ public partial class ImageEditor : UserControl
 
     private ObservableCollection<IImageEditorObject> Objects = new();
 
-    private IImageEditorObject? CurrentObject;
+    private IImageEditorObject? _currentObject;
+    private IImageEditorObject? CurrentObject
+    {
+        get => _currentObject;
+        set
+        {
+            _currentObject?.SetActive(false);
+            _currentObject = value;
+        }
+    }
 
     private Stack<IImageEditorObject> RedoStack = new();
 
     private double ScaleFactor = 1.0;
+    private double _originalScaleFactor = 1.0;
+
+    // Center of the image.
+    private Point ImageCenter = new();
 
     #endregion
 
@@ -211,13 +243,70 @@ public partial class ImageEditor : UserControl
         SetMode(Mode);
 
         OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
+        OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent, OuterCanvas_PinchEnded);
+        OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEvent, OuterCanvas_Pinch);
+    }
+
+    private void OuterCanvas_PinchEnded(object? sender, PanAndZoomEndedEventArgs e)
+    {
+        _originalScaleFactor = ScaleFactor;
+    }
+
+    private void OuterCanvas_Pinch(object? sender, PanAndZoomEventArgs e)
+    {
+        Zoom(e.ScaleOrigin - e.Pan, e.ScaleOrigin, _originalScaleFactor * e.Scale);
+    }
+
+    private void Zoom(Point originalOrigin, Point newOrigin, double newScaleFactor)
+    {
+        // Convert Scale Origin to image coordinates (relative to center).
+        // Work out where this position will move to under the new scaling.
+        // Adjust so that these are the same.
+
+        var pos = originalOrigin - ImageCenter;
+        var contentMPos = pos / ScaleFactor;
+
+        ScaleFactor = newScaleFactor;
+
+        var scaledPos = ImageCenter + contentMPos * ScaleFactor;
+        var offset = scaledPos - newOrigin;
+
+        ImageCenter -= offset;
+        UpdateCanvasPosition();
+    }
+
+    private const double _wheelSpeed = 0.1;
+    private const double _panSpeed = 30;
+
+    private void Pan(double x, double y)
+    {
+        ImageCenter += new Vector(x, y);
+        UpdateCanvasPosition();
+    }
+
+    private void OuterCanvas_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
+    {
+        if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
+        {
+            var pos = e.GetPosition(OuterCanvas);
+            var wheelSpeed = _wheelSpeed;
+            Zoom(pos, pos, e.Delta.Y > 0 ? ScaleFactor * (1 + e.Delta.Y * wheelSpeed) : ScaleFactor / (1 + (-e.Delta.Y) * wheelSpeed));
+        }
+        else if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
+        {
+            Pan(e.Delta.Y * _panSpeed, e.Delta.X * _panSpeed);
+        }
+        else
+        {
+            Pan(e.Delta.X * _panSpeed, e.Delta.Y * _panSpeed);
+        }
     }
 
     #region Layout
 
     private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
     {
-        PositionImage();
+        // PositionImage();
     }
 
     protected override void OnLoaded(RoutedEventArgs e)
@@ -233,15 +322,25 @@ public partial class ImageEditor : UserControl
 
         var scaleFactor = Math.Min(canvasWidth / ImageWidth, canvasHeight / ImageHeight);
         ScaleFactor = scaleFactor;
+        _originalScaleFactor = ScaleFactor;
 
-        var imageWidth = ImageWidth * scaleFactor;
-        var imageHeight = ImageHeight * scaleFactor;
+        ImageCenter = new Point(
+            OuterCanvas.Bounds.Width / 2,
+            OuterCanvas.Bounds.Height / 2);
+
+        UpdateCanvasPosition();
+    }
+
+    private void UpdateCanvasPosition()
+    {
+        var imageWidth = ImageWidth * ScaleFactor;
+        var imageHeight = ImageHeight * ScaleFactor;
 
         ImageBorder.Width = imageWidth;
         ImageBorder.Height = imageHeight;
 
-        Canvas.SetLeft(ImageBorder, OuterCanvas.Bounds.Width / 2 - imageWidth / 2);
-        Canvas.SetTop(ImageBorder, OuterCanvas.Bounds.Height / 2 - imageHeight / 2);
+        Canvas.SetLeft(ImageBorder, ImageCenter.X - imageWidth / 2);
+        Canvas.SetTop(ImageBorder, ImageCenter.Y - imageHeight / 2);
 
         Canvas.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
         Canvas.Width = ImageWidth;
@@ -290,9 +389,15 @@ public partial class ImageEditor : UserControl
     [RelayCommand]
     private void SetMode(ImageEditingMode mode)
     {
+        foreach(var button in ModeButtons)
+        {
+            button.Active = button.Mode == mode;
+        }
         Mode = mode;
-        ShapeButton.Content = CreateModeButtonContent(mode);
+        // ShapeButton.Content = CreateModeButtonContent(mode);
         SecondaryColour.IsVisible = HasSecondaryColour();
+        LineThicknessButton.IsVisible = HasLineThickness();
+        FontSizeButton.IsVisible = Mode == ImageEditingMode.Text;
     }
 
     private bool HasSecondaryColour()
@@ -300,6 +405,14 @@ public partial class ImageEditor : UserControl
         return Mode == ImageEditingMode.Rectangle || Mode == ImageEditingMode.Ellipse;
     }
 
+    private bool HasLineThickness()
+    {
+        return Mode == ImageEditingMode.Rectangle
+            || Mode == ImageEditingMode.Ellipse
+            || Mode == ImageEditingMode.Polyline
+            || Mode == ImageEditingMode.Dimension;
+    }
+
     #endregion
 
     #region Mode Buttons
@@ -309,40 +422,44 @@ public partial class ImageEditor : UserControl
         AddModeButton(ImageEditingMode.Polyline);
         AddModeButton(ImageEditingMode.Rectangle);
         AddModeButton(ImageEditingMode.Ellipse);
+        AddModeButton(ImageEditingMode.Text);
+        AddModeButton(ImageEditingMode.Dimension);
     }
 
     private void AddModeButton(ImageEditingMode mode)
     {
-        ModeButtons.Add(new(mode, CreateModeButtonContent(mode)));
+        ModeButtons.Add(new(mode, CreateModeButtonContent(mode), mode == Mode));
     }
 
-    private Canvas? CreateModeButtonContent(ImageEditingMode mode, bool bindColour = false)
+    private Control? CreateModeButtonContent(ImageEditingMode mode, bool bindColour = false)
     {
         switch (mode)
         {
             case ImageEditingMode.Polyline:
                 var canvas = new Canvas();
-                var points = new Point[] { new(0, 0), new(20, 8), new(5, 16), new(25, 25) };
-                var line1 = new Polyline { Points = points, Width = 25, Height = 25 };
-                var line2 = new Polyline { Points = points, Width = 25, Height = 25 };
-                line1.StrokeThickness = 4;
-                line1.StrokeLineCap = PenLineCap.Round;
-                line1.StrokeJoin = PenLineJoin.Round;
-                line1.Stroke = new SolidColorBrush(Colors.Black);
-                canvas.Children.Add(line1);
-
-                if (bindColour)
                 {
-                    line1.StrokeThickness = 5;
-                    line2.StrokeThickness = 4;
-                    line2.StrokeLineCap = PenLineCap.Round;
-                    line2.StrokeJoin = PenLineJoin.Round;
-                    line2.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
+                    var points = new Point[] { new(0, 0), new(20, 8), new(5, 16), new(25, 25) };
+                    var line1 = new Polyline { Points = points, Width = 25, Height = 25 };
+                    var line2 = new Polyline { Points = points, Width = 25, Height = 25 };
+                    line1.StrokeThickness = 4;
+                    line1.StrokeLineCap = PenLineCap.Round;
+                    line1.StrokeJoin = PenLineJoin.Round;
+                    line1.Stroke = new SolidColorBrush(Colors.Black);
+                    canvas.Children.Add(line1);
+
+                    if (bindColour)
                     {
-                        Source = this,
-                        Converter = ImageEditorRemoveOpacityConverter.Instance
-                    });
-                    canvas.Children.Add(line2);
+                        line1.StrokeThickness = 5;
+                        line2.StrokeThickness = 4;
+                        line2.StrokeLineCap = PenLineCap.Round;
+                        line2.StrokeJoin = PenLineJoin.Round;
+                        line2.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
+                        {
+                            Source = this,
+                            Converter = ImageEditorRemoveOpacityConverter.Instance
+                        });
+                        canvas.Children.Add(line2);
+                    }
                 }
                 return canvas;
             case ImageEditingMode.Rectangle:
@@ -404,6 +521,129 @@ public partial class ImageEditor : UserControl
                 ellipse.Height = 25;
                 canvas.Children.Add(ellipse);
 
+                return canvas;
+            case ImageEditingMode.Text:
+                var textBox = new TextBlock();
+                textBox.Text = "T";
+                textBox.FontSize = 25;
+                textBox.TextAlignment = TextAlignment.Center;
+                textBox.HorizontalAlignment = HorizontalAlignment.Center;
+                textBox.VerticalAlignment = VerticalAlignment.Center;
+                if (bindColour)
+                {
+                    textBox.Bind(TextBlock.ForegroundProperty, new Binding(nameof(PrimaryBrush))
+                    {
+                        Source = this,
+                        Converter = ImageEditorRemoveOpacityConverter.Instance
+                    });
+                }
+                return textBox;
+            case ImageEditingMode.Dimension:
+                canvas = new Canvas();
+                canvas.Width = 25;
+                canvas.Height = 25;
+
+                {
+                    var dimLines = new List<Line>();
+
+                    dimLines.Add(new Line
+                    {
+                        StartPoint = new(2, 10),
+                        EndPoint = new(23, 10),
+                        StrokeLineCap = PenLineCap.Round
+                    });
+                    dimLines.Add(new Line
+                    {
+                        StartPoint = new(2, 10),
+                        EndPoint = new(5, 7),
+                        StrokeLineCap = PenLineCap.Square
+                    });
+                    dimLines.Add(new Line
+                    {
+                        StartPoint = new(2, 10),
+                        EndPoint = new(5, 13),
+                        StrokeLineCap = PenLineCap.Square
+                    });
+                    dimLines.Add(new Line
+                    {
+                        StartPoint = new(23, 10),
+                        EndPoint = new(20, 7),
+                        StrokeLineCap = PenLineCap.Square
+                    });
+                    dimLines.Add(new Line
+                    {
+                        StartPoint = new(23, 10),
+                        EndPoint = new(20, 13),
+                        StrokeLineCap = PenLineCap.Square
+                    });
+
+                    var dotLines = new List<Line>();
+                    dotLines.Add(new Line
+                    {
+                        StartPoint = new(2, 10),
+                        EndPoint = new(2, 24),
+                        StrokeDashArray = [2, 2]
+                    });
+                    dotLines.Add(new Line
+                    {
+                        StartPoint = new(23, 10),
+                        EndPoint = new(23, 24),
+                        StrokeDashArray = [2, 2]
+                    });
+
+                    var number = new TextBlock
+                    {
+                        Text = "10",
+                        FontSize = 9,
+                        TextAlignment = TextAlignment.Center,
+                        Width = 25
+                    };
+                    Canvas.SetLeft(number, 0);
+                    Canvas.SetTop(number, -1);
+
+                    foreach (var line in dimLines)
+                    {
+                        line.StrokeThickness = 2;
+                        line.Stroke = new SolidColorBrush(Colors.Black);
+                    }
+
+                    foreach (var line in dotLines)
+                    {
+                        line.StrokeThickness = 1;
+                        line.Stroke = new SolidColorBrush(Colors.Black);
+                    }
+
+                    if (bindColour)
+                    {
+                        foreach (var line in dimLines)
+                        {
+                            line.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
+                            {
+                                Source = this,
+                                Converter = ImageEditorRemoveOpacityConverter.Instance
+                            });
+                        }
+                        foreach (var line in dotLines)
+                        {
+                            line.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
+                            {
+                                Source = this,
+                                Converter = ImageEditorRemoveOpacityConverter.Instance
+                            });
+                        }
+                    }
+
+                    foreach (var line in dimLines)
+                    {
+                        canvas.Children.Add(line);
+                    }
+                    foreach (var line in dotLines)
+                    {
+                        canvas.Children.Add(line);
+                    }
+                    canvas.Children.Add(number);
+                }
+
                 return canvas;
             default:
                 return null;
@@ -432,19 +672,53 @@ public partial class ImageEditor : UserControl
             context.DrawImage(Source, new(0, 0, ImageWidth, ImageHeight));
         }
 
+        CurrentObject = null;
+
         foreach (var obj in Objects)
         {
             var control = obj.GetControl();
-            var left = Canvas.GetLeft(control);
-            var top = Canvas.GetTop(control);
-            if (double.IsNaN(left)) left = 0;
-            if (double.IsNaN(top)) top = 0;
-            using (context.PushTransform(Matrix.CreateTranslation(new(left, top))))
+            Render(context, control);
+        }
+        return renderBitmap;
+    }
+    private void Render(DrawingContext context, Control control)
+    {
+        var left = Canvas.GetLeft(control);
+        var top = Canvas.GetTop(control);
+        if (double.IsNaN(left)) left = 0;
+        if (double.IsNaN(top)) top = 0;
+
+        var matrix = Matrix.CreateTranslation(new(left, top));
+
+        if(control.RenderTransform is not null)
+        {
+            Vector offset;
+            if(control.RenderTransformOrigin.Unit == RelativeUnit.Relative)
+            {
+                offset = new Vector(
+                    control.Bounds.Width * control.RenderTransformOrigin.Point.X,
+                    control.Bounds.Height * control.RenderTransformOrigin.Point.Y);
+            }
+            else
             {
-                control.Render(context);
+                offset = new Vector(control.RenderTransformOrigin.Point.X, control.RenderTransformOrigin.Point.Y);
             }
+
+            matrix = (Matrix.CreateTranslation(-offset) * control.RenderTransform.Value * Matrix.CreateTranslation(offset)) * matrix;
         }
-        return renderBitmap;
+
+        using (context.PushTransform(matrix))
+        {
+            control.Render(context);
+            if(control is Panel panel)
+            {
+                foreach(var child in panel.Children)
+                {
+                    Render(context, child);
+                }
+            }
+        }
+
     }
 
     public byte[] SaveImage()
@@ -460,7 +734,7 @@ public partial class ImageEditor : UserControl
 
     #region Editing
 
-    private void Objects_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+    private void RefreshObjects()
     {
         Canvas.Children.Clear();
         foreach(var item in Objects)
@@ -470,6 +744,11 @@ public partial class ImageEditor : UserControl
         }
     }
 
+    private void Objects_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+    {
+        RefreshObjects();
+    }
+
     Point ConvertToImageCoordinates(Point canvasCoordinates)
     {
         return canvasCoordinates;// new(canvasCoordinates.X / ScaleFactor, canvasCoordinates.Y / ScaleFactor);
@@ -477,6 +756,7 @@ public partial class ImageEditor : UserControl
 
     private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
     {
+        CurrentObject = null;
         var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
         switch (Mode)
         {
@@ -511,6 +791,18 @@ public partial class ImageEditor : UserControl
                 };
                 AddObject(CurrentObject);
                 break;
+            case ImageEditingMode.Dimension:
+                CurrentObject = new DimensionObject
+                {
+                    Point1 = position,
+                    Point2 = position,
+                    PrimaryBrush = PrimaryBrush,
+                    Text = "",
+                    Offset = 30,
+                    LineThickness = LineThickness
+                };
+                AddObject(CurrentObject);
+                break;
         }
     }
 
@@ -534,6 +826,19 @@ public partial class ImageEditor : UserControl
                 ellipse.Update();
                 Changed?.Invoke(this, new EventArgs());
                 break;
+            case SelectionObject textSelection:
+                textSelection.Point2 = position;
+                textSelection.Update();
+                Changed?.Invoke(this, new EventArgs());
+                break;
+            case DimensionObject dimension:
+                if (!dimension.Complete)
+                {
+                    dimension.Point2 = position;
+                    dimension.Update();
+                    Changed?.Invoke(this, new EventArgs());
+                }
+                break;
         }
     }
 
@@ -560,8 +865,43 @@ public partial class ImageEditor : UserControl
                 CurrentObject = null;
                 Changed?.Invoke(this, new EventArgs());
                 break;
-        }
+            case DimensionObject dimension:
+                dimension.Point2 = position;
+                if(dimension.Point1 == dimension.Point2)
+                {
+                    Objects.Remove(dimension);
+                    CurrentObject = null;
+                    return;
+                }
+                dimension.Complete = true;
 
+                Navigation.Popup<TextEditViewModel, string?>(x => { }).ContinueWith(task =>
+                {
+                    dimension.Text = task.Result ?? "";
+                    dimension.Update();
+                }, TaskScheduler.FromCurrentSynchronizationContext());
+                Changed?.Invoke(this, new EventArgs());
+                break;
+            default:
+                switch (Mode)
+                {
+                    case ImageEditingMode.Text:
+                        Navigation.Popup<TextEditViewModel, string?>(x => { }).ContinueWith(task =>
+                        {
+                            var text = new TextObject
+                            {
+                                Text = task.Result ?? "",
+                                FontSize = FontSize,
+                                PrimaryBrush = PrimaryBrush,
+                                Point = position
+                            };
+                            Objects.Add(text);
+                        }, TaskScheduler.FromCurrentSynchronizationContext());
+
+                        break;
+                }
+                break;
+        }
     }
 
     #endregion

+ 217 - 0
InABox.Avalonia/Components/ImageEditor/Objects/DimensionObject.cs

@@ -0,0 +1,217 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+using Avalonia.Media;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace InABox.Avalonia.Components.ImageEditing;
+
+internal class DimensionObject : IImageEditorObject
+{
+    public IBrush? PrimaryBrush { get; set; }
+
+    public Point Point1 { get; set; }
+
+    public Point Point2 { get; set; }
+
+    public double LineThickness { get; set; }
+
+    public string Text { get; set; } = "";
+
+    public double Offset { get; set; } = 10;
+
+    public bool Complete { get; set; } = false;
+
+    private Canvas Control = new();
+
+    private Thumb? Thumb;
+
+    private bool _active = true;
+
+    public Control GetControl() => Control;
+
+    private Vector GetMainVec()
+    {
+        var p1 = new Vector(Point1.X, Point1.Y);
+        var p2 = new Vector(Point2.X, Point2.Y);
+
+        var mainVec = p2 - p1;
+        var length = mainVec.Length;
+        if(length == 0)
+        {
+            mainVec = new Vector(0, 0);
+        }
+        else
+        {
+            mainVec /= length;
+        }
+
+        return mainVec;
+    }
+
+    private Vector GetPerpVec()
+    {
+        var mainVec = GetMainVec();
+        return new Vector(mainVec.Y, -mainVec.X);
+    }
+
+    public void Update()
+    {
+        Control.Children.RemoveAll(Control.Children.Where(x => !(x is Thumb)));
+
+        Point Point(Vector v) => new(v.X, v.Y);
+
+        var p1 = new Vector(Point1.X, Point1.Y);
+        var p2 = new Vector(Point2.X, Point2.Y);
+
+        var mainVec = p2 - p1;
+        var length = mainVec.Length;
+        if(length == 0)
+        {
+            mainVec = new Vector(0, 0);
+        }
+        else
+        {
+            mainVec /= length;
+        }
+        var perpVec = new Vector(mainVec.Y, -mainVec.X);
+
+        var arrowLength = LineThickness * 2;
+        var offset = perpVec * Offset;
+
+        var mainLines = new List<Line>()
+        {
+            new()
+            {
+                StartPoint = Point(p1 + offset),
+                EndPoint = Point(p2 + offset),
+                StrokeLineCap = PenLineCap.Round
+            },
+            new()
+            {
+                StartPoint = Point(p1 + offset),
+                EndPoint = Point(p1 + mainVec * arrowLength + perpVec * arrowLength + offset),
+                StrokeLineCap = PenLineCap.Square
+            },
+            new()
+            {
+                StartPoint = Point(p1 + offset),
+                EndPoint = Point(p1 + mainVec * arrowLength - perpVec * arrowLength + offset),
+                StrokeLineCap = PenLineCap.Square
+            },
+            new()
+            {
+                StartPoint = Point(p2 + offset),
+                EndPoint = Point(p2 - mainVec * arrowLength + perpVec * arrowLength + offset),
+                StrokeLineCap = PenLineCap.Square
+            },
+            new()
+            {
+                StartPoint = Point(p2 + offset),
+                EndPoint = Point(p2 - mainVec * arrowLength - perpVec * arrowLength + offset),
+                StrokeLineCap = PenLineCap.Square
+            }
+        };
+
+        var dotLines = new List<Line>()
+        {
+            new()
+            {
+                StartPoint = Point(p1),
+                EndPoint = Point(p1 + offset)
+            },
+            new()
+            {
+                StartPoint = Point(p2),
+                EndPoint = Point(p2 + offset)
+            },
+        };
+
+        foreach (var line in dotLines)
+        {
+            line.StrokeThickness = LineThickness;
+            line.Stroke = PrimaryBrush;
+            line.StrokeDashArray = [2, 2];
+            Control.Children.Add(line);
+        }
+
+        foreach (var line in mainLines)
+        {
+            line.StrokeThickness = LineThickness;
+            line.Stroke = PrimaryBrush;
+            Control.Children.Add(line);
+        }
+
+        var textHeight = 20;
+
+        var text = new TextBlock
+        {
+            Text = Text,
+            FontSize = textHeight,
+            TextAlignment = TextAlignment.Center,
+            Width = length,
+            Foreground = PrimaryBrush
+        };
+        text.RenderTransform = new RotateTransform(Math.Atan2(mainVec.Y, mainVec.X) * 180 / Math.PI);
+        var textPos = p1 + offset + mainVec * length / 2 + perpVec * (textHeight / 2 + 5);
+        Canvas.SetLeft(text, textPos.X - length / 2);
+        Canvas.SetTop(text, textPos.Y - textHeight / 2);
+
+        Control.Children.Add(text);
+
+        if(Thumb is null && _active && length > 0)
+        {
+            Thumb = new();
+            Thumb.Width = 10;
+            Thumb.Height = 10;
+            Thumb.Cursor = new(StandardCursorType.SizeAll);
+            Thumb.DragDelta += Thumb_DragDelta;
+            Thumb.ZIndex = 1;
+
+            Control.Children.Add(Thumb);
+        }
+
+        if(Thumb is not null)
+        {
+            Thumb.RenderTransform = new RotateTransform(Math.Atan2(mainVec.Y, mainVec.X) * 180 / Math.PI);
+
+            var thumbPos = p1 + offset + mainVec * length / 2;
+            Canvas.SetLeft(Thumb, thumbPos.X - 5);
+            Canvas.SetTop(Thumb, thumbPos.Y - 5);
+        }
+    }
+
+    private Vector Rotate(Vector v, double angle)
+    {
+        angle = Math.Atan2(v.Y, v.X) + angle;
+        var length = v.Length;
+        return new Vector(
+            length * Math.Cos(angle),
+            length * Math.Sin(angle));
+    }
+
+    private void Thumb_DragDelta(object? sender, VectorEventArgs e)
+    {
+        var main = GetMainVec();
+        var v = Vector.Dot(Rotate(e.Vector, Math.Atan2(main.Y, main.X)), GetPerpVec());
+        Offset += v;
+        Update();
+    }
+
+    public void SetActive(bool active)
+    {
+        _active = active;
+        if(Thumb is not null)
+        {
+            Control.Children.Remove(Thumb);
+            Thumb = null;
+        }
+    }
+}

+ 4 - 0
InABox.Avalonia/Components/ImageEditor/Objects/EllipseObject.cs

@@ -41,4 +41,8 @@ internal class EllipseObject : IImageEditorObject
         Control.Width = Math.Abs(Point2.X - Point1.X);
         Control.Height = Math.Abs(Point2.Y - Point1.Y);
     }
+
+    public void SetActive(bool active)
+    {
+    }
 }

+ 2 - 0
InABox.Avalonia/Components/ImageEditor/Objects/IImageEditorObject.cs

@@ -13,6 +13,8 @@ internal interface IImageEditorObject
 {
     IBrush? PrimaryBrush { get; set; }
 
+    void SetActive(bool active);
+
     Control GetControl();
 
     void Update();

+ 4 - 0
InABox.Avalonia/Components/ImageEditor/Objects/Polyline.cs

@@ -28,4 +28,8 @@ internal class PolylineObject : IImageEditorObject
         Control.StrokeLineCap = PenLineCap.Round;
         Control.StrokeJoin = PenLineJoin.Round;
     }
+
+    public void SetActive(bool active)
+    {
+    }
 }

+ 4 - 0
InABox.Avalonia/Components/ImageEditor/Objects/RectangleObject.cs

@@ -41,4 +41,8 @@ internal class RectangleObject : IImageEditorObject
         Control.Width = Math.Abs(Point2.X - Point1.X);
         Control.Height = Math.Abs(Point2.Y - Point1.Y);
     }
+
+    public void SetActive(bool active)
+    {
+    }
 }

+ 95 - 0
InABox.Avalonia/Components/ImageEditor/Objects/SelectionObject.cs

@@ -0,0 +1,95 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Components.ImageEditing;
+
+internal class SelectionObject : IImageEditorObject
+{
+    public IBrush? PrimaryBrush { get; set; }
+
+    public Point Point1 { get; set; }
+
+    public Point Point2 { get; set; }
+
+    private Rectangle Control = new();
+
+    public Control GetControl() => Control;
+
+    public Point GetTopLeft()
+    {
+        return new(
+            Math.Min(Point1.X, Point2.X),
+            Math.Min(Point1.Y, Point2.Y));
+    }
+    public Size GetSize()
+    {
+        return new(
+            Math.Abs(Point2.X - Point1.X),
+            Math.Abs(Point2.Y - Point1.Y));
+    }
+
+    public void Update()
+    {
+        Control.Stroke = GetBrush();
+        Control.StrokeThickness = 5;
+        Control.StrokeDashArray = [2, 2];
+
+        var topLeft = GetTopLeft();
+        var size = GetSize();
+
+        Canvas.SetLeft(Control, topLeft.X);
+        Canvas.SetTop(Control, topLeft.Y);
+        Control.Width = size.Width;
+        Control.Height = size.Height;
+    }
+
+    private IBrush? GetBrush()
+    {
+        var brush = new VisualBrush
+        {
+            TileMode = TileMode.Tile,
+            DestinationRect = new(0, 0, 4, 4, RelativeUnit.Absolute)
+        };
+        var canvas = new Canvas
+        {
+            Width = 10,
+            Height = 10
+        };
+        var rect1 = new Rectangle { Width = 5, Height = 5 };
+        var rect2 = new Rectangle { Width = 5, Height = 5 };
+        var rect3 = new Rectangle { Width = 5, Height = 5 };
+        var rect4 = new Rectangle { Width = 5, Height = 5 };
+
+        Canvas.SetLeft(rect2, 5);
+        Canvas.SetTop(rect2, 0);
+
+        Canvas.SetLeft(rect3, 5);
+        Canvas.SetTop(rect3, 5);
+
+        Canvas.SetLeft(rect4, 0);
+        Canvas.SetTop(rect4, 5);
+
+        rect1.Fill = new SolidColorBrush(Colors.White);
+        rect2.Fill = new SolidColorBrush(Colors.Black);
+        rect3.Fill = new SolidColorBrush(Colors.White);
+        rect4.Fill = new SolidColorBrush(Colors.Black);
+
+        canvas.Children.Add(rect1);
+        canvas.Children.Add(rect2);
+        canvas.Children.Add(rect3);
+
+        brush.Visual = canvas;
+        return brush;
+    }
+
+    public void SetActive(bool active)
+    {
+    }
+}

+ 44 - 0
InABox.Avalonia/Components/ImageEditor/Objects/TextObject.cs

@@ -0,0 +1,44 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Controls.Shapes;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.Avalonia.Components.ImageEditing;
+
+internal class TextObject : IImageEditorObject
+{
+    public IBrush? PrimaryBrush { get; set; }
+
+    public Point Point { get; set; }
+
+    public double FontSize { get; set; }
+
+    public string Text { get; set; } = "";
+
+    private TextBlock Control = new();
+
+    public Control GetControl() => Control;
+
+    public void Update()
+    {
+        Canvas.SetLeft(Control, Point.X);
+        Canvas.SetTop(Control, Point.Y);
+
+        Control.FontSize = FontSize;
+        Control.Text = Text;
+        Control.Foreground = PrimaryBrush;
+    }
+
+    public void SetActive(bool active)
+    {
+    }
+}

+ 158 - 0
InABox.Avalonia/Components/ImageEditor/PanAndZoomGestureRecognizer.cs

@@ -0,0 +1,158 @@
+using Avalonia;
+using Avalonia.Input;
+using Avalonia.Input.GestureRecognizers;
+using Avalonia.Interactivity;
+
+namespace InABox.Avalonia.Components.ImageEditing;
+
+public class PanAndZoomEventArgs : RoutedEventArgs
+{
+    public PanAndZoomEventArgs(double scale, Point scaleOrigin, Vector pan) : base(PanAndZoomGestureRecognizer.PanAndZoomEvent)
+    {
+        Scale = scale;
+        ScaleOrigin = scaleOrigin;
+        Pan = pan;
+    }
+
+    public double Scale { get; } = 1;
+
+    public Point ScaleOrigin { get; }
+
+    public Vector Pan { get; set; }
+}
+
+public class PanAndZoomEndedEventArgs : RoutedEventArgs
+{
+    public PanAndZoomEndedEventArgs() : base(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent)
+    {
+    }
+}
+
+// Adapted from the PinchGestureRecognizer built in to Avalonia, but with panning added.
+public class PanAndZoomGestureRecognizer : GestureRecognizer
+{
+    public static readonly RoutedEvent<PanAndZoomEventArgs> PanAndZoomEvent =
+        RoutedEvent.Register<PanAndZoomEventArgs>(
+            "PanAndZoom", RoutingStrategies.Bubble, typeof(PanAndZoomGestureRecognizer));
+
+    public static readonly RoutedEvent<PanAndZoomEndedEventArgs> PanAndZoomEndedEvent =
+        RoutedEvent.Register<PanAndZoomEndedEventArgs>(
+            "PanAndZoomEnded", RoutingStrategies.Bubble, typeof(PanAndZoomGestureRecognizer));
+
+    private float _initialDistance;
+    private IPointer? _firstContact;
+    private Point _firstPoint;
+    private IPointer? _secondContact;
+    private Point _secondPoint;
+    private Point _previousOrigin;
+
+    protected override void PointerCaptureLost(IPointer pointer)
+    {
+        RemoveContact(pointer);
+    }
+
+    protected override void PointerMoved(PointerEventArgs e)
+    {
+        if (Target is Visual visual)
+        {
+            if (_firstContact == e.Pointer)
+            {
+                _firstPoint = e.GetPosition(visual);
+            }
+            else if (_secondContact == e.Pointer)
+            {
+                _secondPoint = e.GetPosition(visual);
+            }
+            else
+            {
+                return;
+            }
+
+            if (_firstContact != null && _secondContact != null)
+            {
+                var distance = GetDistance(_firstPoint, _secondPoint);
+
+                var scale = distance / _initialDistance;
+
+                var origin = (_firstPoint + _secondPoint) / 2;
+
+                var pinchEventArgs = new PanAndZoomEventArgs(scale, origin, origin - _previousOrigin);
+                _previousOrigin = origin;
+
+                Target?.RaiseEvent(pinchEventArgs);
+                e.Handled = pinchEventArgs.Handled;
+                e.PreventGestureRecognition();
+            }
+        }
+    }
+
+    protected override void PointerPressed(PointerPressedEventArgs e)
+    {
+        if (Target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
+        {
+            if (_firstContact == null)
+            {
+                _firstContact = e.Pointer;
+                _firstPoint = e.GetPosition(visual);
+
+                return;
+            }
+            else if (_secondContact == null && _firstContact != e.Pointer)
+            {
+                _secondContact = e.Pointer;
+                _secondPoint = e.GetPosition(visual);
+            }
+            else
+            {
+                return;
+            }
+
+            if (_firstContact != null && _secondContact != null)
+            {
+                _initialDistance = GetDistance(_firstPoint, _secondPoint);
+
+                _previousOrigin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f);
+
+                Capture(_firstContact);
+                Capture(_secondContact);
+                e.PreventGestureRecognition();
+            }
+        }
+    }
+
+    protected override void PointerReleased(PointerReleasedEventArgs e)
+    {
+        if(RemoveContact(e.Pointer))
+        {
+            e.PreventGestureRecognition();
+        }
+    }
+
+    private bool RemoveContact(IPointer pointer)
+    {
+        if (_firstContact == pointer || _secondContact == pointer)
+        {
+            if (_secondContact == pointer)
+            {
+                _secondContact = null;
+            }
+
+            if (_firstContact == pointer)
+            {
+                _firstContact = _secondContact;
+
+                _secondContact = null;
+            }
+
+            Target?.RaiseEvent(new PanAndZoomEndedEventArgs());
+            return true;
+        }
+        return false;
+    }
+
+    private static float GetDistance(Point a, Point b)
+    {
+        var length = b - a;
+        return (float)new Vector(length.X, length.Y).Length;
+    }
+}

+ 3 - 3
InABox.Client.RPC/InABox.Client.RPC.csproj

@@ -11,10 +11,10 @@
     </ItemGroup>
 
     <ItemGroup>
-      <PackageReference Include="H.Formatters" Version="2.0.59" />
-      <PackageReference Include="H.Formatters.BinaryFormatter" Version="2.0.59" />
+      <PackageReference Include="H.Formatters" Version="15.0.0" />
+      <PackageReference Include="H.Formatters.BinaryFormatter" Version="15.0.0" />
       <PackageReference Include="H.Formatters.MessagePack" Version="15.0.0" />
-      <PackageReference Include="H.Pipes" Version="2.0.59" />
+      <PackageReference Include="H.Pipes" Version="15.0.0" />
       <PackageReference Include="WebSocket4Net" Version="0.15.2" />
     </ItemGroup>
 

+ 5 - 5
InABox.Server/InABox.Server.csproj

@@ -10,11 +10,11 @@
         <PackageReference Include="Fleck" Version="1.2.0" />
         <PackageReference Include="GenHTTP.Core" Version="8.4.1" />
         <PackageReference Include="GenHTTP.Modules.Practices" Version="8.4.0" />
-        <PackageReference Include="H.Formatters.BinaryFormatter" Version="2.0.59" />
-        <PackageReference Include="H.Formatters.Ceras" Version="2.0.59" />
-        <PackageReference Include="H.Formatters.MessagePack" Version="2.0.59" />
-        <PackageReference Include="H.Pipes" Version="2.0.59" />
-        <PackageReference Include="H.Pipes.AccessControl" Version="2.0.59" />
+        <PackageReference Include="H.Formatters.BinaryFormatter" Version="15.0.0" />
+        <PackageReference Include="H.Formatters.Ceras" Version="15.0.0" />
+        <PackageReference Include="H.Formatters.MessagePack" Version="15.0.0" />
+        <PackageReference Include="H.Pipes" Version="15.0.0" />
+        <PackageReference Include="H.Pipes.AccessControl" Version="15.0.0" />
     </ItemGroup>
 
     <ItemGroup>