ImageEditor.axaml.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  1. using Avalonia;
  2. using Avalonia.Controls;
  3. using Avalonia.Controls.Shapes;
  4. using Avalonia.Data;
  5. using Avalonia.Input;
  6. using Avalonia.Interactivity;
  7. using Avalonia.Markup.Xaml;
  8. using Avalonia.Media;
  9. using Avalonia.Media.Imaging;
  10. using Avalonia.Layout;
  11. using Avalonia.Skia.Helpers;
  12. using CommunityToolkit.Mvvm.Input;
  13. using FluentResults;
  14. using InABox.Avalonia.Components.ImageEditing;
  15. using InABox.Avalonia.Converters;
  16. using InABox.Core;
  17. using SkiaSharp;
  18. using System.Collections.ObjectModel;
  19. using System.Threading.Tasks;
  20. using Avalonia.LogicalTree;
  21. using CommunityToolkit.Mvvm.ComponentModel;
  22. namespace InABox.Avalonia.Components;
  23. public enum ImageEditingMode
  24. {
  25. Polyline,
  26. Rectangle,
  27. Ellipse,
  28. Text,
  29. Dimension
  30. }
  31. public partial class ImageEditorModeButton(ImageEditingMode mode, Control? content, bool active) : ObservableObject
  32. {
  33. public ImageEditingMode Mode { get; set; } = mode;
  34. public Control? Content { get; set; } = content;
  35. [ObservableProperty]
  36. private bool _active = active;
  37. }
  38. public class ImageEditorTransparentImageBrushConverter : AbstractConverter<IBrush?, IBrush?>
  39. {
  40. public static readonly ImageEditorTransparentImageBrushConverter Instance = new ImageEditorTransparentImageBrushConverter();
  41. protected override IBrush? Convert(IBrush? value, object? parameter = null)
  42. {
  43. if (value is SolidColorBrush solid && solid.Color.A == 255) return solid;
  44. var brush = new VisualBrush
  45. {
  46. TileMode = TileMode.Tile,
  47. DestinationRect = new(0, 0, 10, 10, RelativeUnit.Absolute)
  48. };
  49. var canvas = new Canvas
  50. {
  51. Width = 10,
  52. Height = 10
  53. };
  54. var rect1 = new Rectangle { Width = 5, Height = 5 };
  55. var rect2 = new Rectangle { Width = 5, Height = 5 };
  56. Canvas.SetLeft(rect2, 5);
  57. Canvas.SetTop(rect2, 5);
  58. rect1.Fill = new SolidColorBrush(Colors.LightGray);
  59. rect2.Fill = new SolidColorBrush(Colors.LightGray);
  60. var rect3 = new Rectangle { Width = 10, Height = 10 };
  61. rect3.Fill = value;
  62. canvas.Children.Add(rect1);
  63. canvas.Children.Add(rect2);
  64. canvas.Children.Add(rect3);
  65. brush.Visual = canvas;
  66. return brush;
  67. }
  68. }
  69. public class ImageEditorRemoveOpacityConverter : AbstractConverter<IBrush?, IBrush?>
  70. {
  71. public static readonly ImageEditorRemoveOpacityConverter Instance = new();
  72. protected override IBrush? Convert(IBrush? value, object? parameter = null)
  73. {
  74. if (value is SolidColorBrush solid)
  75. {
  76. return new SolidColorBrush(new Color(255, solid.Color.R, solid.Color.G, solid.Color.B));
  77. }
  78. return value;
  79. }
  80. }
  81. // TODO: Make it so we don't re-render everything everytime 'Objects' changes.
  82. public partial class ImageEditor : UserControl
  83. {
  84. public static readonly StyledProperty<IImage?> SourceProperty =
  85. AvaloniaProperty.Register<ImageEditor, IImage?>(nameof(Source));
  86. public static readonly StyledProperty<IBrush?> PrimaryBrushProperty =
  87. AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(PrimaryBrush), new SolidColorBrush(Colors.Black));
  88. public static readonly StyledProperty<IBrush?> SecondaryBrushProperty =
  89. AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(SecondaryBrush), new SolidColorBrush(Colors.White));
  90. public static readonly StyledProperty<double> LineThicknessProperty =
  91. AvaloniaProperty.Register<ImageEditor, double>(nameof(LineThickness), 3.0);
  92. public static readonly StyledProperty<int> ImageWidthProperty =
  93. AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageWidth), 100);
  94. public static readonly StyledProperty<int> ImageHeightProperty =
  95. AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageHeight), 100);
  96. public static readonly StyledProperty<ImageEditingMode> ModeProperty =
  97. AvaloniaProperty.Register<ImageEditor, ImageEditingMode>(nameof(Mode), ImageEditingMode.Polyline);
  98. public static readonly StyledProperty<bool> ShowButtonsProperty =
  99. AvaloniaProperty.Register<ImageEditor, bool>(nameof(ShowButtons), true);
  100. public IImage? Source
  101. {
  102. get => GetValue(SourceProperty);
  103. set => SetValue(SourceProperty, value);
  104. }
  105. public int ImageWidth
  106. {
  107. get => GetValue(ImageWidthProperty);
  108. set => SetValue(ImageWidthProperty, value);
  109. }
  110. public int ImageHeight
  111. {
  112. get => GetValue(ImageHeightProperty);
  113. set => SetValue(ImageHeightProperty, value);
  114. }
  115. public bool ShowButtons
  116. {
  117. get => GetValue(ShowButtonsProperty);
  118. set => SetValue(ShowButtonsProperty, value);
  119. }
  120. #region Editing Properties
  121. public ImageEditingMode Mode
  122. {
  123. get => GetValue(ModeProperty);
  124. set => SetValue(ModeProperty, value);
  125. }
  126. public IBrush? PrimaryBrush
  127. {
  128. get => GetValue(PrimaryBrushProperty);
  129. set => SetValue(PrimaryBrushProperty, value);
  130. }
  131. public IBrush? SecondaryBrush
  132. {
  133. get => GetValue(SecondaryBrushProperty);
  134. set => SetValue(SecondaryBrushProperty, value);
  135. }
  136. public double LineThickness
  137. {
  138. get => GetValue(LineThicknessProperty);
  139. set => SetValue(LineThicknessProperty, value);
  140. }
  141. #endregion
  142. #region Events
  143. public event EventHandler? Changed;
  144. #endregion
  145. #region Private Properties
  146. public ObservableCollection<ImageEditorModeButton> ModeButtons { get; set; } = new();
  147. private ObservableCollection<IImageEditorObject> Objects = new();
  148. private IImageEditorObject? _currentObject;
  149. private IImageEditorObject? CurrentObject
  150. {
  151. get => _currentObject;
  152. set
  153. {
  154. _currentObject?.SetActive(false);
  155. _currentObject = value;
  156. }
  157. }
  158. private Stack<IImageEditorObject> RedoStack = new();
  159. private double ScaleFactor = 1.0;
  160. #endregion
  161. static ImageEditor()
  162. {
  163. SourceProperty.Changed.AddClassHandler<ImageEditor>(Source_Changed);
  164. }
  165. private static void Source_Changed(ImageEditor editor, AvaloniaPropertyChangedEventArgs args)
  166. {
  167. if(editor.Source is not null)
  168. {
  169. editor.ImageWidth = (int)Math.Floor(editor.Source.Size.Width);
  170. editor.ImageHeight = (int)Math.Floor(editor.Source.Size.Height);
  171. editor.PositionImage();
  172. }
  173. }
  174. public ImageEditor()
  175. {
  176. InitializeComponent();
  177. Objects.CollectionChanged += Objects_CollectionChanged;
  178. AddModeButtons();
  179. SetMode(Mode);
  180. OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
  181. }
  182. #region Layout
  183. private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
  184. {
  185. PositionImage();
  186. }
  187. protected override void OnLoaded(RoutedEventArgs e)
  188. {
  189. base.OnLoaded(e);
  190. PositionImage();
  191. }
  192. private void PositionImage()
  193. {
  194. var canvasWidth = OuterCanvas.Bounds.Width;
  195. var canvasHeight = OuterCanvas.Bounds.Height;
  196. var scaleFactor = Math.Min(canvasWidth / ImageWidth, canvasHeight / ImageHeight);
  197. ScaleFactor = scaleFactor;
  198. var imageWidth = ImageWidth * scaleFactor;
  199. var imageHeight = ImageHeight * scaleFactor;
  200. ImageBorder.Width = imageWidth;
  201. ImageBorder.Height = imageHeight;
  202. Canvas.SetLeft(ImageBorder, OuterCanvas.Bounds.Width / 2 - imageWidth / 2);
  203. Canvas.SetTop(ImageBorder, OuterCanvas.Bounds.Height / 2 - imageHeight / 2);
  204. Canvas.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
  205. Canvas.Width = ImageWidth;
  206. Canvas.Height = ImageHeight;
  207. }
  208. #endregion
  209. #region Editing Commands
  210. private void UpdateUndoRedoButtons()
  211. {
  212. UndoButton.IsEnabled = Objects.Count > 0;
  213. RedoButton.IsEnabled = RedoStack.Count > 0;
  214. }
  215. [RelayCommand]
  216. private void Undo()
  217. {
  218. if (Objects.Count == 0) return;
  219. RedoStack.Push(Objects[^1]);
  220. Objects.RemoveAt(Objects.Count - 1);
  221. UpdateUndoRedoButtons();
  222. Changed?.Invoke(this, new EventArgs());
  223. }
  224. [RelayCommand]
  225. private void Redo()
  226. {
  227. if (!RedoStack.TryPop(out var top)) return;
  228. Objects.Add(top);
  229. UpdateUndoRedoButtons();
  230. Changed?.Invoke(this, new EventArgs());
  231. }
  232. private void AddObject(IImageEditorObject obj)
  233. {
  234. Objects.Add(obj);
  235. RedoStack.Clear();
  236. UpdateUndoRedoButtons();
  237. Changed?.Invoke(this, new EventArgs());
  238. }
  239. [RelayCommand]
  240. private void SetMode(ImageEditingMode mode)
  241. {
  242. foreach(var button in ModeButtons)
  243. {
  244. button.Active = button.Mode == mode;
  245. }
  246. Mode = mode;
  247. // ShapeButton.Content = CreateModeButtonContent(mode);
  248. SecondaryColour.IsVisible = HasSecondaryColour();
  249. LineThicknessButton.IsVisible = HasLineThickness();
  250. }
  251. private bool HasSecondaryColour()
  252. {
  253. return Mode == ImageEditingMode.Rectangle || Mode == ImageEditingMode.Ellipse;
  254. }
  255. private bool HasLineThickness()
  256. {
  257. return Mode == ImageEditingMode.Rectangle
  258. || Mode == ImageEditingMode.Ellipse
  259. || Mode == ImageEditingMode.Polyline
  260. || Mode == ImageEditingMode.Dimension;
  261. }
  262. #endregion
  263. #region Mode Buttons
  264. private void AddModeButtons()
  265. {
  266. AddModeButton(ImageEditingMode.Polyline);
  267. AddModeButton(ImageEditingMode.Rectangle);
  268. AddModeButton(ImageEditingMode.Ellipse);
  269. AddModeButton(ImageEditingMode.Text);
  270. AddModeButton(ImageEditingMode.Dimension);
  271. }
  272. private void AddModeButton(ImageEditingMode mode)
  273. {
  274. ModeButtons.Add(new(mode, CreateModeButtonContent(mode), mode == Mode));
  275. }
  276. private Control? CreateModeButtonContent(ImageEditingMode mode, bool bindColour = false)
  277. {
  278. switch (mode)
  279. {
  280. case ImageEditingMode.Polyline:
  281. var canvas = new Canvas();
  282. {
  283. var points = new Point[] { new(0, 0), new(20, 8), new(5, 16), new(25, 25) };
  284. var line1 = new Polyline { Points = points, Width = 25, Height = 25 };
  285. var line2 = new Polyline { Points = points, Width = 25, Height = 25 };
  286. line1.StrokeThickness = 4;
  287. line1.StrokeLineCap = PenLineCap.Round;
  288. line1.StrokeJoin = PenLineJoin.Round;
  289. line1.Stroke = new SolidColorBrush(Colors.Black);
  290. canvas.Children.Add(line1);
  291. if (bindColour)
  292. {
  293. line1.StrokeThickness = 5;
  294. line2.StrokeThickness = 4;
  295. line2.StrokeLineCap = PenLineCap.Round;
  296. line2.StrokeJoin = PenLineJoin.Round;
  297. line2.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  298. {
  299. Source = this,
  300. Converter = ImageEditorRemoveOpacityConverter.Instance
  301. });
  302. canvas.Children.Add(line2);
  303. }
  304. }
  305. return canvas;
  306. case ImageEditingMode.Rectangle:
  307. canvas = new Canvas();
  308. canvas.Width = 25;
  309. canvas.Height = 25;
  310. var rectangle = new Rectangle();
  311. if (bindColour)
  312. {
  313. rectangle.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
  314. {
  315. Source = this,
  316. Converter = ImageEditorRemoveOpacityConverter.Instance
  317. });
  318. rectangle.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
  319. {
  320. Source = this,
  321. Converter = ImageEditorTransparentImageBrushConverter.Instance
  322. });
  323. }
  324. else
  325. {
  326. rectangle.Stroke = new SolidColorBrush(Colors.Black);
  327. rectangle.Fill = new SolidColorBrush(Colors.White);
  328. }
  329. rectangle.StrokeThickness = 1.0;
  330. rectangle.Width = 25;
  331. rectangle.Height = 25;
  332. canvas.Children.Add(rectangle);
  333. return canvas;
  334. case ImageEditingMode.Ellipse:
  335. canvas = new Canvas();
  336. canvas.Width = 25;
  337. canvas.Height = 25;
  338. var ellipse = new Ellipse();
  339. if (bindColour)
  340. {
  341. ellipse.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
  342. {
  343. Source = this,
  344. Converter = ImageEditorRemoveOpacityConverter.Instance
  345. });
  346. ellipse.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
  347. {
  348. Source = this,
  349. Converter = ImageEditorTransparentImageBrushConverter.Instance
  350. });
  351. }
  352. else
  353. {
  354. ellipse.Stroke = new SolidColorBrush(Colors.Black);
  355. ellipse.Fill = new SolidColorBrush(Colors.White);
  356. }
  357. ellipse.StrokeThickness = 1.0;
  358. ellipse.Width = 25;
  359. ellipse.Height = 25;
  360. canvas.Children.Add(ellipse);
  361. return canvas;
  362. case ImageEditingMode.Text:
  363. var textBox = new TextBlock();
  364. textBox.Text = "T";
  365. textBox.FontSize = 25;
  366. textBox.TextAlignment = TextAlignment.Center;
  367. textBox.HorizontalAlignment = HorizontalAlignment.Center;
  368. textBox.VerticalAlignment = VerticalAlignment.Center;
  369. if (bindColour)
  370. {
  371. textBox.Bind(TextBlock.ForegroundProperty, new Binding(nameof(PrimaryBrush))
  372. {
  373. Source = this,
  374. Converter = ImageEditorRemoveOpacityConverter.Instance
  375. });
  376. }
  377. return textBox;
  378. case ImageEditingMode.Dimension:
  379. canvas = new Canvas();
  380. canvas.Width = 25;
  381. canvas.Height = 25;
  382. {
  383. var dimLines = new List<Line>();
  384. dimLines.Add(new Line
  385. {
  386. StartPoint = new(2, 10),
  387. EndPoint = new(23, 10),
  388. StrokeLineCap = PenLineCap.Round
  389. });
  390. dimLines.Add(new Line
  391. {
  392. StartPoint = new(2, 10),
  393. EndPoint = new(5, 7),
  394. StrokeLineCap = PenLineCap.Square
  395. });
  396. dimLines.Add(new Line
  397. {
  398. StartPoint = new(2, 10),
  399. EndPoint = new(5, 13),
  400. StrokeLineCap = PenLineCap.Square
  401. });
  402. dimLines.Add(new Line
  403. {
  404. StartPoint = new(23, 10),
  405. EndPoint = new(20, 7),
  406. StrokeLineCap = PenLineCap.Square
  407. });
  408. dimLines.Add(new Line
  409. {
  410. StartPoint = new(23, 10),
  411. EndPoint = new(20, 13),
  412. StrokeLineCap = PenLineCap.Square
  413. });
  414. var dotLines = new List<Line>();
  415. dotLines.Add(new Line
  416. {
  417. StartPoint = new(2, 10),
  418. EndPoint = new(2, 24),
  419. StrokeDashArray = [2, 2]
  420. });
  421. dotLines.Add(new Line
  422. {
  423. StartPoint = new(23, 10),
  424. EndPoint = new(23, 24),
  425. StrokeDashArray = [2, 2]
  426. });
  427. var number = new TextBlock
  428. {
  429. Text = "10",
  430. FontSize = 9,
  431. TextAlignment = TextAlignment.Center,
  432. Width = 25
  433. };
  434. Canvas.SetLeft(number, 0);
  435. Canvas.SetTop(number, -1);
  436. foreach (var line in dimLines)
  437. {
  438. line.StrokeThickness = 2;
  439. line.Stroke = new SolidColorBrush(Colors.Black);
  440. }
  441. foreach (var line in dotLines)
  442. {
  443. line.StrokeThickness = 1;
  444. line.Stroke = new SolidColorBrush(Colors.Black);
  445. }
  446. if (bindColour)
  447. {
  448. foreach (var line in dimLines)
  449. {
  450. line.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  451. {
  452. Source = this,
  453. Converter = ImageEditorRemoveOpacityConverter.Instance
  454. });
  455. }
  456. foreach (var line in dotLines)
  457. {
  458. line.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  459. {
  460. Source = this,
  461. Converter = ImageEditorRemoveOpacityConverter.Instance
  462. });
  463. }
  464. }
  465. foreach (var line in dimLines)
  466. {
  467. canvas.Children.Add(line);
  468. }
  469. foreach (var line in dotLines)
  470. {
  471. canvas.Children.Add(line);
  472. }
  473. canvas.Children.Add(number);
  474. }
  475. return canvas;
  476. default:
  477. return null;
  478. }
  479. }
  480. #endregion
  481. #region Public Interface
  482. public void Reset()
  483. {
  484. Objects.Clear();
  485. RedoStack.Clear();
  486. UpdateUndoRedoButtons();
  487. Changed?.Invoke(this, new EventArgs());
  488. }
  489. public Bitmap GetImage()
  490. {
  491. var renderBitmap = new RenderTargetBitmap(new PixelSize(ImageWidth, ImageHeight));
  492. renderBitmap.Render(Image);
  493. using var context = renderBitmap.CreateDrawingContext();
  494. if(Source is not null)
  495. {
  496. context.DrawImage(Source, new(0, 0, ImageWidth, ImageHeight));
  497. }
  498. CurrentObject = null;
  499. foreach (var obj in Objects)
  500. {
  501. var control = obj.GetControl();
  502. Render(context, control);
  503. }
  504. return renderBitmap;
  505. }
  506. private void Render(DrawingContext context, Control control)
  507. {
  508. var left = Canvas.GetLeft(control);
  509. var top = Canvas.GetTop(control);
  510. if (double.IsNaN(left)) left = 0;
  511. if (double.IsNaN(top)) top = 0;
  512. var matrix = Matrix.CreateTranslation(new(left, top));
  513. if(control.RenderTransform is not null)
  514. {
  515. Vector offset;
  516. if(control.RenderTransformOrigin.Unit == RelativeUnit.Relative)
  517. {
  518. offset = new Vector(
  519. control.Bounds.Width * control.RenderTransformOrigin.Point.X,
  520. control.Bounds.Height * control.RenderTransformOrigin.Point.Y);
  521. }
  522. else
  523. {
  524. offset = new Vector(control.RenderTransformOrigin.Point.X, control.RenderTransformOrigin.Point.Y);
  525. }
  526. matrix = (Matrix.CreateTranslation(-offset) * control.RenderTransform.Value * Matrix.CreateTranslation(offset)) * matrix;
  527. }
  528. using (context.PushTransform(matrix))
  529. {
  530. control.Render(context);
  531. if(control is Panel panel)
  532. {
  533. foreach(var child in panel.Children)
  534. {
  535. Render(context, child);
  536. }
  537. }
  538. }
  539. }
  540. public byte[] SaveImage()
  541. {
  542. var bitmap = GetImage();
  543. var stream = new MemoryStream();
  544. bitmap.Save(stream);
  545. return stream.ToArray();
  546. }
  547. #endregion
  548. #region Editing
  549. private void RefreshObjects()
  550. {
  551. Canvas.Children.Clear();
  552. foreach(var item in Objects)
  553. {
  554. item.Update();
  555. Canvas.Children.Add(item.GetControl());
  556. }
  557. }
  558. private void Objects_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
  559. {
  560. RefreshObjects();
  561. }
  562. Point ConvertToImageCoordinates(Point canvasCoordinates)
  563. {
  564. return canvasCoordinates;// new(canvasCoordinates.X / ScaleFactor, canvasCoordinates.Y / ScaleFactor);
  565. }
  566. private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
  567. {
  568. CurrentObject = null;
  569. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  570. switch (Mode)
  571. {
  572. case ImageEditingMode.Polyline:
  573. CurrentObject = new PolylineObject
  574. {
  575. Points = [position],
  576. PrimaryBrush = PrimaryBrush,
  577. Thickness = LineThickness
  578. };
  579. AddObject(CurrentObject);
  580. break;
  581. case ImageEditingMode.Rectangle:
  582. CurrentObject = new RectangleObject
  583. {
  584. Point1 = position,
  585. Point2 = position,
  586. PrimaryBrush = PrimaryBrush,
  587. SecondaryBrush = SecondaryBrush,
  588. Thickness = LineThickness
  589. };
  590. AddObject(CurrentObject);
  591. break;
  592. case ImageEditingMode.Ellipse:
  593. CurrentObject = new EllipseObject
  594. {
  595. Point1 = position,
  596. Point2 = position,
  597. PrimaryBrush = PrimaryBrush,
  598. SecondaryBrush = SecondaryBrush,
  599. Thickness = LineThickness
  600. };
  601. AddObject(CurrentObject);
  602. break;
  603. case ImageEditingMode.Text:
  604. CurrentObject = new SelectionObject
  605. {
  606. Point1 = position,
  607. Point2 = position,
  608. PrimaryBrush = PrimaryBrush
  609. };
  610. AddObject(CurrentObject);
  611. break;
  612. case ImageEditingMode.Dimension:
  613. CurrentObject = new DimensionObject
  614. {
  615. Point1 = position,
  616. Point2 = position,
  617. PrimaryBrush = PrimaryBrush,
  618. Text = "",
  619. Offset = 30,
  620. LineThickness = LineThickness
  621. };
  622. AddObject(CurrentObject);
  623. break;
  624. }
  625. }
  626. private void Canvas_PointerMoved(object? sender, PointerEventArgs e)
  627. {
  628. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  629. switch (CurrentObject)
  630. {
  631. case PolylineObject polyline:
  632. polyline.Points.Add(position);
  633. polyline.Update();
  634. Changed?.Invoke(this, new EventArgs());
  635. break;
  636. case RectangleObject rectangle:
  637. rectangle.Point2 = position;
  638. rectangle.Update();
  639. Changed?.Invoke(this, new EventArgs());
  640. break;
  641. case EllipseObject ellipse:
  642. ellipse.Point2 = position;
  643. ellipse.Update();
  644. Changed?.Invoke(this, new EventArgs());
  645. break;
  646. case SelectionObject textSelection:
  647. textSelection.Point2 = position;
  648. textSelection.Update();
  649. Changed?.Invoke(this, new EventArgs());
  650. break;
  651. case DimensionObject dimension:
  652. if (!dimension.Complete)
  653. {
  654. dimension.Point2 = position;
  655. dimension.Update();
  656. Changed?.Invoke(this, new EventArgs());
  657. }
  658. break;
  659. }
  660. }
  661. private void Canvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
  662. {
  663. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  664. switch (CurrentObject)
  665. {
  666. case PolylineObject polyline:
  667. polyline.Points.Add(position);
  668. polyline.Update();
  669. CurrentObject = null;
  670. Changed?.Invoke(this, new EventArgs());
  671. break;
  672. case RectangleObject rectangle:
  673. rectangle.Point2 = position;
  674. rectangle.Update();
  675. CurrentObject = null;
  676. Changed?.Invoke(this, new EventArgs());
  677. break;
  678. case EllipseObject ellipse:
  679. ellipse.Point2 = position;
  680. ellipse.Update();
  681. CurrentObject = null;
  682. Changed?.Invoke(this, new EventArgs());
  683. break;
  684. case SelectionObject selection:
  685. selection.Point2 = position;
  686. Objects.Remove(selection);
  687. CurrentObject = null;
  688. CreateObjectFromSelection(selection).ContinueWith(task =>
  689. {
  690. if(task.Exception != null)
  691. {
  692. MobileLogging.LogExceptionMessage(task.Exception);
  693. }
  694. });
  695. break;
  696. case DimensionObject dimension:
  697. dimension.Point2 = position;
  698. if(dimension.Point1 == dimension.Point2)
  699. {
  700. Objects.Remove(dimension);
  701. CurrentObject = null;
  702. return;
  703. }
  704. dimension.Complete = true;
  705. Navigation.Popup<TextEditViewModel, string?>(x => { }).ContinueWith(task =>
  706. {
  707. dimension.Text = task.Result ?? "";
  708. dimension.Update();
  709. }, TaskScheduler.FromCurrentSynchronizationContext());
  710. Changed?.Invoke(this, new EventArgs());
  711. break;
  712. }
  713. }
  714. private async Task CreateObjectFromSelection(SelectionObject selection)
  715. {
  716. switch (Mode)
  717. {
  718. case ImageEditingMode.Text:
  719. var text = await Navigation.Popup<TextEditViewModel, string?>(x => { });
  720. if(text is null)
  721. {
  722. return;
  723. }
  724. CurrentObject = new TextObject
  725. {
  726. Point = selection.GetTopLeft(),
  727. Size = selection.GetSize(),
  728. Text = text,
  729. PrimaryBrush = selection.PrimaryBrush
  730. };
  731. AddObject(CurrentObject);
  732. break;
  733. }
  734. }
  735. #endregion
  736. }