1 // 2 // EllipseEngine.cs 3 // 4 // Author: 5 // Andrew Davis <andrew.3.1415@gmail.com> 6 // 7 // Copyright (c) 2014 Andrew Davis, GSoC 2014 8 // 9 // Permission is hereby granted, free of charge, to any person obtaining a copy 10 // of this software and associated documentation files (the "Software"), to deal 11 // in the Software without restriction, including without limitation the rights 12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 // copies of the Software, and to permit persons to whom the Software is 14 // furnished to do so, subject to the following conditions: 15 // 16 // The above copyright notice and this permission notice shall be included in 17 // all copies or substantial portions of the Software. 18 // 19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 // THE SOFTWARE. 26 27 using System; 28 using System.Collections.Generic; 29 using System.Linq; 30 using System.Text; 31 using Cairo; 32 using Pinta.Core; 33 34 namespace Pinta.Tools 35 { 36 public class EllipseEngine : ShapeEngine 37 { 38 /// <summary> 39 /// Create a new EllipseEngine. 40 /// </summary> 41 /// <param name="parent_layer">The parent UserLayer for the re-editable DrawingLayer.</param> 42 /// <param name="drawing_layer">An existing ReEditableLayer to reuse. This is for cloning only. If not cloning, pass in null.</param> 43 /// <param name="antialiasing">Whether or not antialiasing is enabled.</param> 44 /// <param name="outline_color">The outline color for the shape.</param> 45 /// <param name="fill_color">The fill color for the shape.</param> 46 /// <param name="brush_width">The width of the outline of the shape.</param> EllipseEngine(UserLayer parent_layer, ReEditableLayer drawing_layer, bool antialiasing, Color outline_color, Color fill_color, int brush_width)47 public EllipseEngine (UserLayer parent_layer, ReEditableLayer drawing_layer, 48 bool antialiasing, Color outline_color, Color fill_color, 49 int brush_width) 50 : base (parent_layer, drawing_layer, BaseEditEngine.ShapeTypes.Ellipse, 51 antialiasing, true, outline_color, fill_color, brush_width) 52 { 53 54 } 55 EllipseEngine(EllipseEngine src)56 private EllipseEngine (EllipseEngine src) 57 : base (src) 58 { 59 } 60 Clone()61 public override ShapeEngine Clone () 62 { 63 return new EllipseEngine (this); 64 } 65 66 /// <summary> 67 /// Generate each point in an elliptic shape and store the result in GeneratedPoints. 68 /// <param name="brush_width">The width of the brush that will be used to draw the shape.</param> 69 /// </summary> GeneratePoints(int brush_width)70 public override void GeneratePoints (int brush_width) 71 { 72 List<GeneratedPoint> points = new List<GeneratedPoint>(); 73 74 //An ellipse requires exactly 4 control points in order to draw anything. 75 if (ControlPoints.Count == 4) 76 { 77 //This is mostly for time efficiency/optimization, but it can also help readability. 78 PointD cp0 = ControlPoints[0].Position, cp1 = ControlPoints[1].Position, 79 cp2 = ControlPoints[2].Position, cp3 = ControlPoints[3].Position; 80 81 //An ellipse also requires that all 4 control points compose a perfect rectangle parallel/perpendicular to the window. 82 //So, confirm that it is indeed a perfect rectangle. 83 84 bool perfectRectangle = false; 85 86 if (cp0.X == cp1.X) 87 { 88 if (cp0.Y == cp3.Y && cp1.Y == cp2.Y && cp2.X == cp3.X) 89 { 90 perfectRectangle = true; 91 } 92 } 93 else if (cp0.Y == cp1.Y) 94 { 95 if (cp0.X == cp3.X && cp1.X == cp2.X && cp2.Y == cp3.Y) 96 { 97 perfectRectangle = true; 98 } 99 } 100 101 if (perfectRectangle) 102 { 103 //It is expected that the 4 control points always form a perfect rectangle parallel/perpendicular to the window. 104 //However, we must first determine which control point is at the top left and which is at the bottom right. 105 //It is also expected that the 4 control points are adjacent to each other by index and position, e.g.: 0, 1, 2, 3. 106 107 PointD topLeft = cp0; 108 PointD bottomRight = cp0; 109 110 //Compare the second point with the first. 111 if (cp1.X < topLeft.X || cp1.Y < topLeft.Y) 112 { 113 //The second point is either more left or more up than the first. 114 115 topLeft = cp1; 116 117 //Compare the third point with the second. 118 if (cp2.X < topLeft.X || cp2.Y < topLeft.Y) 119 { 120 //The third point is either more left or more up than the second. 121 122 topLeft = cp2; 123 124 //The first point remains the bottom right. 125 } 126 else 127 { 128 //The third point is neither more left nor more up than the second. 129 130 //The second point remains the top left. 131 132 bottomRight = cp3; 133 } 134 } 135 else 136 { 137 //The second point is neither more left nor more up than the first. 138 139 PointD secondPoint = cp1; 140 141 //Compare the third point with the second. 142 if (cp2.X < secondPoint.X || cp2.Y < secondPoint.Y) 143 { 144 //The third point is either more left or more up than the second. 145 146 topLeft = cp3; 147 bottomRight = cp1; 148 } 149 else 150 { 151 //The third point is neither more left nor more up than the second. 152 153 //The first point remains the top left. 154 155 bottomRight = cp2; 156 } 157 } 158 159 //Now we can calculate the width and height. 160 double width = bottomRight.X - topLeft.X; 161 double height = bottomRight.Y - topLeft.Y; 162 163 //Some elliptic math code taken from Cairo Extensions, and some from DocumentSelection code written for GSoC 2013. 164 165 //Calculate an appropriate interval at which to increment t based on 166 //the bounding rectangle's width and height properties. The increment 167 //for t determines how many intermediate Points to calculate for the 168 //ellipse. For each curve, t will go from tInterval to 1. The lower 169 //the value of tInterval, the higher number of intermediate Points 170 //that will be calculated and stored into the Polygon collection. 171 double tInterval = .02d; 172 173 double rx = width / 2d; //1/2 of the bounding Rectangle Width. 174 double ry = height / 2d; //1/2 of the bounding Rectangle Height. 175 double cx = topLeft.X + rx; //The middle of the bounding Rectangle, horizontally speaking. 176 double cy = topLeft.Y + ry; //The middle of the bounding Rectangle, vertically speaking. 177 double c1 = 0.5522847498307933984022516322796d; //tan(pi / 8d) * 4d / 3d ~= 0.5522847498307933984022516322796d 178 179 points.AddRange(calculateCurvePoints(tInterval, 180 cx + rx, cy, 181 cx + rx, cy - c1 * ry, 182 cx + c1 * rx, cy - ry, 183 cx, cy - ry, 184 3)); 185 186 points.AddRange(calculateCurvePoints(tInterval, 187 cx, cy - ry, 188 cx - c1 * rx, cy - ry, 189 cx - rx, cy - c1 * ry, 190 cx - rx, cy, 191 0)); 192 193 points.AddRange(calculateCurvePoints(tInterval, 194 cx - rx, cy, 195 cx - rx, cy + c1 * ry, 196 cx - c1 * rx, cy + ry, 197 cx, cy + ry, 198 1)); 199 200 points.AddRange(calculateCurvePoints(tInterval, 201 cx, cy + ry, 202 cx + c1 * rx, cy + ry, 203 cx + rx, cy + c1 * ry, 204 cx + rx, cy, 205 2)); 206 207 // Close the curve. 208 points.Add(new GeneratedPoint(new PointD(cx + rx, cy), 3)); 209 } 210 } 211 212 //Make sure there are now generated points; otherwise, one of the ellipse conditions was not met. 213 if (points.Count == 0) 214 { 215 //Something went wrong. Just copy the control points. Note: it's important that there be many generated points even if 216 //everything is just a linear connection of control points. This is because the generated points are used in the check 217 //that determines if the mouse clicks on the shape. 218 219 int nextNum; 220 221 PointD currentPoint, nextPoint; 222 223 //Go through each control point. 224 for (int currentNum = 0; currentNum < ControlPoints.Count; ++currentNum) 225 { 226 //Determine the next control point. 227 228 nextNum = currentNum + 1; 229 230 if (nextNum >= ControlPoints.Count) 231 { 232 nextNum = 0; 233 } 234 235 currentPoint = ControlPoints[currentNum].Position; 236 nextPoint = ControlPoints[nextNum].Position; 237 238 //Lerp from the current point to the next point. 239 for (float lerpPos = 0.0f; lerpPos < 1.0f; lerpPos += 0.01f) 240 { 241 points.Add(new GeneratedPoint(Utility.Lerp(currentPoint, nextPoint, lerpPos), currentNum)); 242 } 243 } 244 } 245 246 GeneratedPoints = points.ToArray(); 247 } 248 249 /// <summary> 250 /// Calculate each intermediate Point in the specified curve, returning Math.Round(1d / tInterval - 1d) number of Points. 251 /// </summary> 252 /// <param name="tInterval">The increment value for t (should be between 0-1).</param> 253 /// <param name="x0">Starting point X (not included in the returned Point(s)).</param> 254 /// <param name="y0">Starting point Y (not included in the returned Point(s)).</param> 255 /// <param name="x1">Control point 1 X.</param> 256 /// <param name="y1">Control point 1 Y.</param> 257 /// <param name="x2">Control point 2 X.</param> 258 /// <param name="y2">Control point 2 Y.</param> 259 /// <param name="x3">Ending point X (included in the returned Point(s)).</param> 260 /// <param name="y3">Ending point Y (included in the returned Point(s)).</param> 261 /// <param name="cPIndex">The index of the previous ControlPoint to the generated points.</param> 262 /// <returns></returns> calculateCurvePoints( double tInterval, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, int cPIndex)263 protected List<GeneratedPoint> calculateCurvePoints( 264 double tInterval, 265 double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, 266 int cPIndex) 267 { 268 List<GeneratedPoint> calculatedPoints = new List<GeneratedPoint>((int)(1d / tInterval)); 269 270 double oneMinusT; 271 double oneMinusTSquared; 272 double oneMinusTCubed; 273 274 double tSquared; 275 double tCubed; 276 277 double oneMinusTSquaredTimesTTimesThree; 278 double oneMinusTTimesTSquaredTimesThree; 279 280 for (double t = 0; t < 1d; t += tInterval) 281 { 282 //There are 3 "layers" in a cubic Bezier curve's calculation. These "layers" 283 //must be calculated for each intermediate Point (for each value of t from 284 //tInterval to 1d). The Points in each "layer" store [the distance between 285 //two consecutive Points from the previous "layer" multipled by the value 286 //of t (which is between 0d-1d)] plus [the position of the first Point of 287 //the two consecutive Points from the previous "layer"]. This must be 288 //calculated for the X and Y of every consecutive Point in every layer 289 //until the last Point possible is reached, which is the Point on the curve. 290 291 //Note: the code below is an optimized version of the commented explanation above. 292 293 oneMinusT = 1d - t; 294 oneMinusTSquared = oneMinusT * oneMinusT; 295 oneMinusTCubed = oneMinusTSquared * oneMinusT; 296 297 tSquared = t * t; 298 tCubed = tSquared * t; 299 300 oneMinusTSquaredTimesTTimesThree = oneMinusTSquared * t * 3d; 301 oneMinusTTimesTSquaredTimesThree = oneMinusT * tSquared * 3d; 302 303 calculatedPoints.Add(new GeneratedPoint(new PointD( 304 (oneMinusTCubed * x0 + oneMinusTSquaredTimesTTimesThree * x1 + oneMinusTTimesTSquaredTimesThree * x2 + tCubed * x3), 305 (oneMinusTCubed * y0 + oneMinusTSquaredTimesTTimesThree * y1 + oneMinusTTimesTSquaredTimesThree * y2 + tCubed * y3)), 306 cPIndex)); 307 } 308 309 //Return the partial Polygon containing the calculated Points in the curve. 310 return calculatedPoints; 311 } 312 } 313 } 314