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