CircleTool.cs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Linq;
  5. using System.Numerics;
  6. using System.Windows.Media;
  7. using PixiEditor.Helpers.Extensions;
  8. using PixiEditor.Models.DataHolders;
  9. using PixiEditor.Models.Layers;
  10. using PixiEditor.Models.Position;
  11. namespace PixiEditor.Models.Tools.Tools
  12. {
  13. public class CircleTool : ShapeTool
  14. {
  15. public override ToolType ToolType => ToolType.Circle;
  16. public CircleTool()
  17. {
  18. Tooltip = "Draws circle on canvas (C). Hold Shift to draw even circle.";
  19. }
  20. public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
  21. {
  22. int thickness = (int) Toolbar.GetSetting("ToolSize").Value;
  23. DoubleCords fixedCoordinates = CalculateCoordinatesForShapeRotation(coordinates[^1], coordinates[0]);
  24. Coordinates[] outline = CreateEllipse(fixedCoordinates.Coords1, fixedCoordinates.Coords2, thickness);
  25. BitmapPixelChanges pixels = BitmapPixelChanges.FromSingleColoredArray(outline, color);
  26. if ((bool) Toolbar.GetSetting("Fill").Value)
  27. {
  28. Color fillColor = (Color) Toolbar.GetSetting("FillColor").Value;
  29. pixels.ChangedPixels.AddRangeNewOnly(
  30. BitmapPixelChanges.FromSingleColoredArray(CalculateFillForEllipse(outline), fillColor)
  31. .ChangedPixels);
  32. }
  33. return new[] {new LayerChange(pixels, layer)};
  34. }
  35. /// <summary>
  36. /// Calculates ellipse points for specified coordinates and thickness.
  37. /// </summary>
  38. /// <param name="startCoordinates">Top left coordinate of ellipse</param>
  39. /// <param name="endCoordinates">Bottom right coordinate of ellipse</param>
  40. /// <param name="thickness">Thickness of ellipse</param>
  41. /// <param name="filled">Should ellipse be filled</param>
  42. /// <returns>Coordinates for ellipse</returns>
  43. public Coordinates[] CreateEllipse(Coordinates startCoordinates, Coordinates endCoordinates, int thickness,
  44. bool filled)
  45. {
  46. List<Coordinates> output = new List<Coordinates>();
  47. Coordinates[] outline = CreateEllipse(startCoordinates, endCoordinates, thickness);
  48. output.AddRange(outline);
  49. if (filled)
  50. {
  51. output.AddRange(CalculateFillForEllipse(outline));
  52. return output.Distinct().ToArray();
  53. }
  54. return output.ToArray();
  55. }
  56. /// <summary>
  57. /// Calculates ellipse points for specified coordinates and thickness.
  58. /// </summary>
  59. /// <param name="startCoordinates">Top left coordinate of ellipse</param>
  60. /// <param name="endCoordinates">Bottom right coordinate of ellipse</param>
  61. /// <param name="thickness">Thickness of ellipse</param>
  62. /// <returns>Coordinates for ellipse</returns>
  63. public Coordinates[] CreateEllipse(Coordinates startCoordinates, Coordinates endCoordinates, int thickness)
  64. {
  65. double radiusX = (endCoordinates.X - startCoordinates.X) / 2.0;
  66. double radiusY = (endCoordinates.Y - startCoordinates.Y) / 2.0;
  67. double centerX = (startCoordinates.X + endCoordinates.X + 1) / 2.0;
  68. double centerY = (startCoordinates.Y + endCoordinates.Y + 1) / 2.0;
  69. List<Coordinates> output = new List<Coordinates>();
  70. Coordinates[] ellipse = MidpointEllipse(radiusX, radiusY, centerX, centerY);
  71. if (thickness == 1)
  72. output.AddRange(ellipse);
  73. else
  74. output.AddRange(GetThickShape(ellipse, thickness));
  75. return output.Distinct().ToArray();
  76. }
  77. public Coordinates[] MidpointEllipse(double halfWidth, double halfHeight, double centerX, double centerY)
  78. {
  79. if (halfWidth < 1 || halfHeight < 1)
  80. return FallbackRectangle(halfWidth, halfHeight, centerX, centerY);
  81. //ellipse formula: halfHeight^2 * x^2 + halfWidth^2 * y^2 - halfHeight^2 * halfWidth^2 = 0
  82. //Make sure we are always at the center of a pixel
  83. double currentX = Math.Ceiling(centerX - 0.5) + 0.5;
  84. double currentY = centerY + halfHeight;
  85. List<Coordinates> outputCoordinates = new List<Coordinates>();
  86. double currentSlope;
  87. //from PI/2 to middle
  88. do
  89. {
  90. outputCoordinates.AddRange(GetRegionPoints(currentX, centerX, currentY, centerY));
  91. //calculate next pixel coords
  92. currentX++;
  93. if (Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX, 2) +
  94. Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY - 0.5, 2) -
  95. Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2) >= 0)
  96. {
  97. currentY--;
  98. }
  99. //calculate how far we've advanced
  100. double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
  101. double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
  102. currentSlope = -(derivativeX / derivativeY);
  103. } while (currentSlope > -1 && currentY - centerY > 0.5);
  104. //from middle to 0
  105. while (currentY - centerY >= 0)
  106. {
  107. outputCoordinates.AddRange(GetRegionPoints(currentX, centerX, currentY, centerY));
  108. currentY--;
  109. if (Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX + 0.5, 2) +
  110. Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY, 2) -
  111. Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2) < 0)
  112. {
  113. currentX++;
  114. }
  115. }
  116. return outputCoordinates.ToArray();
  117. }
  118. private Coordinates[] FallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY)
  119. {
  120. List<Coordinates> coordinates = new List<Coordinates>();
  121. for (double x = centerX - halfWidth; x <= centerX + halfWidth; x++)
  122. {
  123. coordinates.Add(new Coordinates((int)x, (int)(centerY - halfHeight)));
  124. coordinates.Add(new Coordinates((int)x, (int)(centerY + halfHeight)));
  125. }
  126. for (double y = centerY - halfHeight + 1; y <= centerY + halfHeight - 1; y++)
  127. {
  128. coordinates.Add(new Coordinates((int)(centerX - halfWidth), (int)y));
  129. coordinates.Add(new Coordinates((int)(centerX + halfWidth), (int)y));
  130. }
  131. return coordinates.ToArray();
  132. }
  133. private Coordinates[] CalculateFillForEllipse(Coordinates[] outlineCoordinates)
  134. {
  135. List<Coordinates> finalCoordinates = new List<Coordinates>();
  136. int bottom = outlineCoordinates.Max(x => x.Y);
  137. int top = outlineCoordinates.Min(x => x.Y);
  138. for (int i = top + 1; i < bottom; i++)
  139. {
  140. var rowCords = outlineCoordinates.Where(x => x.Y == i);
  141. int right = rowCords.Max(x => x.X);
  142. int left = rowCords.Min(x => x.X);
  143. for (int j = left + 1; j < right; j++) finalCoordinates.Add(new Coordinates(j, i));
  144. }
  145. return finalCoordinates.ToArray();
  146. }
  147. private Coordinates[] GetRegionPoints(double x, double xc, double y, double yc)
  148. {
  149. Coordinates[] outputCoordinates = new Coordinates[4];
  150. outputCoordinates[0] = new Coordinates((int)Math.Floor(x), (int)Math.Floor(y));
  151. outputCoordinates[1] = new Coordinates((int)Math.Floor((-(x - xc) + xc)), (int)Math.Floor(y));
  152. outputCoordinates[2] = new Coordinates((int)Math.Floor(x), (int)Math.Floor((-(y - yc) + yc)));
  153. outputCoordinates[3] = new Coordinates((int)Math.Floor((-(x - xc) + xc)), (int)Math.Floor((-(y - yc) + yc)));
  154. return outputCoordinates;
  155. }
  156. }
  157. }