Browse Source

Ellipse rotated outline is in acceptable state

flabbet 11 months ago
parent
commit
29da86d05f

+ 75 - 31
src/ChunkyImageLib/Operations/EllipseHelper.cs

@@ -61,7 +61,7 @@ public class EllipseHelper
     /// Splits the ellipse into a bunch of horizontal lines.
     /// The resulting list contains consecutive pairs of <see cref="VecI"/>s, each pair has one for the start of the line and one for the end.
     /// </summary>
-    public static List<VecI> SplitEllipseIntoLines(List<VecI> ellipse)
+    public static List<VecI> SplitEllipseIntoLines(HashSet<VecI> ellipse)
     {
         List<VecI> lines = new();
         var sorted = ellipse.OrderBy(
@@ -97,7 +97,7 @@ public class EllipseHelper
         return lines;
     }
 
-    public static List<VecI> GenerateEllipseFromRect(RectI rect, double rotationRad = 0)
+    public static HashSet<VecI> GenerateEllipseFromRect(RectI rect, double rotationRad = 0)
     {
         if (rect.IsZeroOrNegativeArea)
             return new();
@@ -120,14 +120,14 @@ public class EllipseHelper
     /// Center is at (2; 2). It's a place where 4 pixels meet
     /// Both radii are 1.5. Making them 2 would make the ellipse touch the edges of pixels, whereas we want it to stay in the middle
     /// </summary>
-    public static List<VecI> GenerateMidpointEllipse(
+    public static HashSet<VecI> GenerateMidpointEllipse(
         double halfWidth,
         double halfHeight,
         double centerX,
         double centerY,
-        List<VecI>? listToFill = null)
+        HashSet<VecI>? listToFill = null)
     {
-        listToFill ??= new List<VecI>();
+        listToFill ??= new HashSet<VecI>();
         if (halfWidth < 1 || halfHeight < 1)
         {
             AddFallbackRectangle(halfWidth, halfHeight, centerX, centerY, listToFill);
@@ -181,74 +181,118 @@ public class EllipseHelper
         return listToFill;
     }
 
-    private static List<VecI> GenerateMidpointEllipse(double halfWidth, double halfHeight, double centerX,
+    private static HashSet<VecI> GenerateMidpointEllipse(double halfWidth, double halfHeight, double centerX,
         double centerY, double rotationRad)
     {
-        var listToFill = new List<VecI>();
-        if (halfWidth < 1 || halfHeight < 1)
-        {
-            AddFallbackRectangle(halfWidth, halfHeight, centerX, centerY, listToFill);
-            return listToFill;
-        }
-        
+        var listToFill = new HashSet<VecI>();
+
         // formula ((x - h)cos(tetha) + (y - k)sin(tetha))^2 / a^2 + (-(x-h)sin(tetha)+(y-k)cos(tetha))^2 / b^2 = 1
 
         //double topMostTetha = GetTopMostAlpha(halfWidth, halfHeight, rotationRad);
 
         //VecD possiblyTopmostPoint = GetTethaPoint(topMostTetha, halfWidth, halfHeight, rotationRad);
         //VecD possiblyMinPoint = GetTethaPoint(topMostTetha + Math.PI, halfWidth, halfHeight, rotationRad);
-        
+
         // less than, because y grows downwards
         //VecD actualTopmost = possiblyTopmostPoint.Y < possiblyMinPoint.Y ? possiblyTopmostPoint : possiblyMinPoint;
+        
+        //rotationRad = double.Round(rotationRad, 1);
 
-        double currentTetha = 0; 
+        double currentTetha = 0;
+
+        double tethaStep = 0.001;
+
+        VecI[] lastPoints = new VecI[2];
 
         do
         {
             VecD point = GetTethaPoint(currentTetha, halfWidth, halfHeight, rotationRad);
-            listToFill.Add(new VecI((int)Math.Round(point.X + centerX), (int)Math.Round(point.Y + centerY)));
+            VecI floored = new((int)Math.Floor(point.X + centerX), (int)Math.Floor(point.Y + centerY));
+
+            AddPoint(listToFill, floored, lastPoints);
 
-            currentTetha += 0.001;
+            currentTetha += tethaStep;
         } while (currentTetha < Math.PI * 2);
         
         return listToFill;
     }
-    
-    private static bool IsInsideEllipse(double x, double y, double centerX, double centerY, double halfWidth, double halfHeight, double rotationRad)
+
+    private static void AddPoint(HashSet<VecI> listToFill, VecI floored, VecI[] lastPoints)
+    {
+        if(!listToFill.Add(floored)) return;
+
+        if (lastPoints[0] == default)
+        {
+            lastPoints[0] = floored;
+            return;
+        }
+
+        if (lastPoints[1] == default)
+        {
+            lastPoints[1] = floored;
+            return;
+        }
+
+        if (IsLShape(lastPoints, floored))
+        {
+            listToFill.Remove(lastPoints[1]);
+
+            lastPoints[0] = floored;
+            lastPoints[1] = default;
+            
+            return;
+        }
+
+        lastPoints[0] = lastPoints[1];
+        lastPoints[1] = floored;
+    }
+
+    private static bool IsLShape(VecI[] points, VecI third)
+    {
+        VecI first = points[0];
+        VecI second = points[1];
+        return first.X != third.X && first.Y != third.Y && (second - first).TaxicabLength == 1 &&
+               (second - third).TaxicabLength == 1;
+    }
+
+    private static bool IsInsideEllipse(double x, double y, double centerX, double centerY, double halfWidth,
+        double halfHeight, double rotationRad)
     {
         double lhs = Math.Pow(x * Math.Cos(rotationRad) + y * Math.Sin(rotationRad), 2) / Math.Pow(halfWidth, 2);
         double rhs = Math.Pow(-x * Math.Sin(rotationRad) + y * Math.Cos(rotationRad), 2) / Math.Pow(halfHeight, 2);
-        
+
         return lhs + rhs <= 1;
     }
-    
-    private static VecD GetDerivative(double x,  double halfWidth, double halfHeight, double rotationRad, double tetha)
+
+    private static VecD GetDerivative(double x, double halfWidth, double halfHeight, double rotationRad, double tetha)
     {
-        double xDerivative = halfWidth * Math.Cos(tetha) * Math.Cos(rotationRad) - halfHeight * Math.Sin(tetha) * Math.Sin(rotationRad);
-        double yDerivative = halfWidth * Math.Cos(tetha) * Math.Sin(rotationRad) + halfHeight * Math.Sin(tetha) * Math.Cos(rotationRad);
-        
+        double xDerivative = halfWidth * Math.Cos(tetha) * Math.Cos(rotationRad) -
+                             halfHeight * Math.Sin(tetha) * Math.Sin(rotationRad);
+        double yDerivative = halfWidth * Math.Cos(tetha) * Math.Sin(rotationRad) +
+                             halfHeight * Math.Sin(tetha) * Math.Cos(rotationRad);
+
         return new VecD(xDerivative, yDerivative);
     }
-    
+
     private static VecD GetTethaPoint(double alpha, double halfWidth, double halfHeight, double rotation)
     {
         double x =
             (halfWidth * Math.Cos(alpha) * Math.Cos(rotation) - halfHeight * Math.Sin(alpha) * Math.Sin(rotation));
         double y = halfWidth * Math.Cos(alpha) * Math.Sin(rotation) + halfHeight * Math.Sin(alpha) * Math.Cos(rotation);
-        
+
         return new VecD(x, y);
     }
-    
+
     private static double GetTopMostAlpha(double halfWidth, double halfHeight, double rotationRad)
     {
-        if(rotationRad == 0)
+        if (rotationRad == 0)
             return 0;
         double tethaRot = Math.Cos(rotationRad) / Math.Sin(rotationRad);
         return Math.Atan((halfHeight * tethaRot) / halfWidth);
     }
 
     private static void AddFallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY,
-        List<VecI> coordinates)
+        HashSet<VecI> coordinates)
     {
         int left = (int)Math.Floor(centerX - halfWidth);
         int top = (int)Math.Floor(centerY - halfHeight);
@@ -270,7 +314,7 @@ public class EllipseHelper
         }
     }
 
-    private static void AddRegionPoints(List<VecI> coordinates, double x, double xc, double y, double yc)
+    private static void AddRegionPoints(HashSet<VecI> coordinates, double x, double xc, double y, double yc)
     {
         int xFloor = (int)Math.Floor(x);
         int yFloor = (int)Math.Floor(y);

+ 2 - 5
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -41,7 +41,7 @@ internal class EllipseOperation : IMirroredDrawOperation
         {
             var ellipseList = EllipseHelper.GenerateEllipseFromRect(location, rotation);
 
-            ellipse = ellipseList.Select(a => new Point(a)).Distinct().ToArray();
+            ellipse = ellipseList.Select(a => new Point(a)).ToArray();
             if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
             {
                 /*(var fill, ellipseFillRect) = EllipseHelper.SplitEllipseFillIntoRegions(ellipseList, location);
@@ -101,10 +101,7 @@ internal class EllipseOperation : IMirroredDrawOperation
     {
         ShapeCorners corners = new((RectD)location);
         corners = corners.AsRotated(rotation, (VecD)location.Center);
-        RectI bounds = (RectI)corners.AABBBounds;
-        
-        /*VecI shift = new VecI(Math.Max(0, -aabb.X), Math.Max(0, -aabb.Y));
-        aabb = aabb.Offset(shift);*/
+        RectI bounds = (RectI)corners.AABBBounds.RoundOutwards();
         
         var chunks = OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
         if (fillColor.A == 0)

+ 3 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs

@@ -1,4 +1,5 @@
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -30,13 +31,13 @@ public class EllipseVectorData : ShapeVectorData
         RectD rotated = new ShapeCorners(RectD.FromTwoPoints(VecD.Zero, imageSize))
             .AsRotated(RotationRadians, imageSize / 2f).AABBBounds;
 
-        VecI shift = new VecI(-(int)rotated.Left, -(int)rotated.Top);
+        VecI shift = new VecI((int)Math.Floor(-rotated.Left), (int)Math.Floor(-rotated.Top));
         RectI drawRect = new(shift, imageSize);
         
         img.EnqueueDrawEllipse(drawRect, StrokeColor, FillColor, StrokeWidth, RotationRadians);
         img.CommitChanges();
 
-        VecI topLeft = new VecI((int)(Position.X - Radius.X), (int)(Position.Y - Radius.Y)) - shift;
+        VecI topLeft = new VecI((int)Math.Round(Position.X - Radius.X), (int)Math.Round(Position.Y - Radius.Y)) - shift;
         
         RectI region = new(VecI.Zero, (VecI)AABB.Size);
 

+ 1 - 1
src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs

@@ -165,7 +165,7 @@ internal class BrushShapeOverlay : Overlay
     private static PathGeometry ConstructEllipseOutline(RectI rectangle)
     {
         var center = rectangle.Center;
-        var points = EllipseHelper.GenerateEllipseFromRect(rectangle, 0);
+        var points = EllipseHelper.GenerateEllipseFromRect(rectangle, 0).ToList();
         points.Sort((vec, vec2) => Math.Sign((vec - center).Angle - (vec2 - center).Angle));
         List<VecI> finalPoints = new();
         for (int i = 0; i < points.Count; i++)