1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #ifndef MOZILLA_GFX_POLYGON_H
8 #define MOZILLA_GFX_POLYGON_H
9 
10 #include <initializer_list>
11 #include <utility>
12 
13 #include "Matrix.h"
14 #include "Point.h"
15 #include "Triangle.h"
16 #include "nsTArray.h"
17 
18 namespace mozilla {
19 namespace gfx {
20 
21 /**
22  * Calculates the w = 0 intersection point for the edge defined by
23  * |aFirst| and |aSecond|.
24  */
25 template <class Units>
CalculateEdgeIntersect(const Point4DTyped<Units> & aFirst,const Point4DTyped<Units> & aSecond)26 Point4DTyped<Units> CalculateEdgeIntersect(const Point4DTyped<Units>& aFirst,
27                                            const Point4DTyped<Units>& aSecond) {
28   static const float w = 0.00001f;
29   const float t = (w - aFirst.w) / (aSecond.w - aFirst.w);
30   return aFirst + (aSecond - aFirst) * t;
31 }
32 
33 /**
34  * Clips the polygon defined by |aPoints| so that there are no points with
35  * w <= 0.
36  */
37 template <class Units>
ClipPointsAtInfinity(const nsTArray<Point4DTyped<Units>> & aPoints)38 nsTArray<Point4DTyped<Units>> ClipPointsAtInfinity(
39     const nsTArray<Point4DTyped<Units>>& aPoints) {
40   nsTArray<Point4DTyped<Units>> outPoints(aPoints.Length());
41 
42   const size_t pointCount = aPoints.Length();
43   for (size_t i = 0; i < pointCount; ++i) {
44     const Point4DTyped<Units>& first = aPoints[i];
45     const Point4DTyped<Units>& second = aPoints[(i + 1) % pointCount];
46 
47     if (!first.w || !second.w) {
48       // Skip edges at infinity.
49       continue;
50     }
51 
52     if (first.w > 0.0f) {
53       outPoints.AppendElement(first);
54     }
55 
56     if ((first.w <= 0.0f) ^ (second.w <= 0.0f)) {
57       outPoints.AppendElement(CalculateEdgeIntersect(first, second));
58     }
59   }
60 
61   return outPoints;
62 }
63 
64 /**
65  * Calculates the distances between the points in |aPoints| and the plane
66  * defined by |aPlaneNormal| and |aPlanePoint|.
67  */
68 template <class Units>
CalculatePointPlaneDistances(const nsTArray<Point4DTyped<Units>> & aPoints,const Point4DTyped<Units> & aPlaneNormal,const Point4DTyped<Units> & aPlanePoint,size_t & aPos,size_t & aNeg)69 nsTArray<float> CalculatePointPlaneDistances(
70     const nsTArray<Point4DTyped<Units>>& aPoints,
71     const Point4DTyped<Units>& aPlaneNormal,
72     const Point4DTyped<Units>& aPlanePoint, size_t& aPos, size_t& aNeg) {
73   // Point classification might produce incorrect results due to numerical
74   // inaccuracies. Using an epsilon value makes the splitting plane "thicker".
75   const float epsilon = 0.05f;
76 
77   aPos = aNeg = 0;
78   nsTArray<float> distances(aPoints.Length());
79 
80   for (const Point4DTyped<Units>& point : aPoints) {
81     float dot = (point - aPlanePoint).DotProduct(aPlaneNormal);
82 
83     if (dot > epsilon) {
84       aPos++;
85     } else if (dot < -epsilon) {
86       aNeg++;
87     } else {
88       // The point is within the thick plane.
89       dot = 0.0f;
90     }
91 
92     distances.AppendElement(dot);
93   }
94 
95   return distances;
96 }
97 
98 /**
99  * Clips the polygon defined by |aPoints|. The clipping uses previously
100  * calculated plane to point distances and the plane normal |aNormal|.
101  * The result of clipping is stored in |aBackPoints| and |aFrontPoints|.
102  */
103 template <class Units>
ClipPointsWithPlane(const nsTArray<Point4DTyped<Units>> & aPoints,const Point4DTyped<Units> & aNormal,const nsTArray<float> & aDots,nsTArray<Point4DTyped<Units>> & aBackPoints,nsTArray<Point4DTyped<Units>> & aFrontPoints)104 void ClipPointsWithPlane(const nsTArray<Point4DTyped<Units>>& aPoints,
105                          const Point4DTyped<Units>& aNormal,
106                          const nsTArray<float>& aDots,
107                          nsTArray<Point4DTyped<Units>>& aBackPoints,
108                          nsTArray<Point4DTyped<Units>>& aFrontPoints) {
109   static const auto Sign = [](const float& f) {
110     if (f > 0.0f) return 1;
111     if (f < 0.0f) return -1;
112     return 0;
113   };
114 
115   const size_t pointCount = aPoints.Length();
116   for (size_t i = 0; i < pointCount; ++i) {
117     size_t j = (i + 1) % pointCount;
118 
119     const Point4DTyped<Units>& a = aPoints[i];
120     const Point4DTyped<Units>& b = aPoints[j];
121     const float dotA = aDots[i];
122     const float dotB = aDots[j];
123 
124     // The point is in front of or on the plane.
125     if (dotA >= 0) {
126       aFrontPoints.AppendElement(a);
127     }
128 
129     // The point is behind or on the plane.
130     if (dotA <= 0) {
131       aBackPoints.AppendElement(a);
132     }
133 
134     // If the sign of the dot products changes between two consecutive
135     // vertices, then the plane intersects with the polygon edge.
136     // The case where the polygon edge is within the plane is handled above.
137     if (Sign(dotA) && Sign(dotB) && Sign(dotA) != Sign(dotB)) {
138       // Calculate the line segment and plane intersection point.
139       const Point4DTyped<Units> ab = b - a;
140       const float dotAB = ab.DotProduct(aNormal);
141       const float t = -dotA / dotAB;
142       const Point4DTyped<Units> p = a + (ab * t);
143 
144       // Add the intersection point to both polygons.
145       aBackPoints.AppendElement(p);
146       aFrontPoints.AppendElement(p);
147     }
148   }
149 }
150 
151 /**
152  * PolygonTyped stores the points of a convex planar polygon.
153  */
154 template <class Units>
155 class PolygonTyped {
156   typedef Point3DTyped<Units> Point3DType;
157   typedef Point4DTyped<Units> Point4DType;
158 
159  public:
160   PolygonTyped() = default;
161 
162   explicit PolygonTyped(const nsTArray<Point4DType>& aPoints,
163                         const Point4DType& aNormal = DefaultNormal())
mNormal(aNormal)164       : mNormal(aNormal), mPoints(aPoints) {}
165 
166   explicit PolygonTyped(nsTArray<Point4DType>&& aPoints,
167                         const Point4DType& aNormal = DefaultNormal())
mNormal(aNormal)168       : mNormal(aNormal), mPoints(std::move(aPoints)) {}
169 
170   explicit PolygonTyped(const std::initializer_list<Point4DType>& aPoints,
171                         const Point4DType& aNormal = DefaultNormal())
mNormal(aNormal)172       : mNormal(aNormal), mPoints(aPoints) {
173 #ifdef DEBUG
174     EnsurePlanarPolygon();
175 #endif
176   }
177 
178   /**
179    * Returns the smallest 2D rectangle that can fully cover the polygon.
180    */
BoundingBox()181   RectTyped<Units> BoundingBox() const {
182     if (mPoints.IsEmpty()) {
183       return RectTyped<Units>();
184     }
185 
186     float minX, maxX, minY, maxY;
187     minX = maxX = mPoints[0].x;
188     minY = maxY = mPoints[0].y;
189 
190     for (const Point4DType& point : mPoints) {
191       minX = std::min(point.x, minX);
192       maxX = std::max(point.x, maxX);
193 
194       minY = std::min(point.y, minY);
195       maxY = std::max(point.y, maxY);
196     }
197 
198     return RectTyped<Units>(minX, minY, maxX - minX, maxY - minY);
199   }
200 
201   /**
202    * Clips the polygon against the given 2D rectangle.
203    */
ClipPolygon(const RectTyped<Units> & aRect)204   PolygonTyped<Units> ClipPolygon(const RectTyped<Units>& aRect) const {
205     if (aRect.IsEmpty()) {
206       return PolygonTyped<Units>();
207     }
208 
209     return ClipPolygon(FromRect(aRect));
210   }
211 
212   /**
213    * Clips this polygon against |aPolygon| in 2D and returns a new polygon.
214    */
ClipPolygon(const PolygonTyped<Units> & aPolygon)215   PolygonTyped<Units> ClipPolygon(const PolygonTyped<Units>& aPolygon) const {
216     const nsTArray<Point4DType>& points = aPolygon.GetPoints();
217 
218     if (mPoints.IsEmpty() || points.IsEmpty()) {
219       return PolygonTyped<Units>();
220     }
221 
222     nsTArray<Point4DType> clippedPoints(mPoints.Clone());
223 
224     size_t pos, neg;
225     nsTArray<Point4DType> backPoints(4), frontPoints(4);
226 
227     // Iterate over all the edges of the clipping polygon |aPolygon| and clip
228     // this polygon against the edges.
229     const size_t pointCount = points.Length();
230     for (size_t i = 0; i < pointCount; ++i) {
231       const Point4DType p1 = points[(i + 1) % pointCount];
232       const Point4DType p2 = points[i];
233 
234       // Calculate the normal for the edge defined by |p1| and |p2|.
235       const Point4DType normal(p2.y - p1.y, p1.x - p2.x, 0.0f, 0.0f);
236 
237       // Calculate the distances between the points of the polygon and the
238       // plane defined by |aPolygon|.
239       const nsTArray<float> distances =
240           CalculatePointPlaneDistances(clippedPoints, normal, p1, pos, neg);
241 
242       backPoints.ClearAndRetainStorage();
243       frontPoints.ClearAndRetainStorage();
244 
245       // Clip the polygon points using the previously calculated distances.
246       ClipPointsWithPlane(clippedPoints, normal, distances, backPoints,
247                           frontPoints);
248 
249       // Only use the points behind the clipping plane.
250       clippedPoints = std::move(backPoints);
251 
252       if (clippedPoints.Length() < 3) {
253         // The clipping created a polygon with no area.
254         return PolygonTyped<Units>();
255       }
256     }
257 
258     return PolygonTyped<Units>(std::move(clippedPoints), mNormal);
259   }
260 
261   /**
262    * Returns a new polygon containing the bounds of the given 2D rectangle.
263    */
FromRect(const RectTyped<Units> & aRect)264   static PolygonTyped<Units> FromRect(const RectTyped<Units>& aRect) {
265     nsTArray<Point4DType> points{
266         Point4DType(aRect.X(), aRect.Y(), 0.0f, 1.0f),
267         Point4DType(aRect.X(), aRect.YMost(), 0.0f, 1.0f),
268         Point4DType(aRect.XMost(), aRect.YMost(), 0.0f, 1.0f),
269         Point4DType(aRect.XMost(), aRect.Y(), 0.0f, 1.0f)};
270 
271     return PolygonTyped<Units>(std::move(points));
272   }
273 
GetNormal()274   const Point4DType& GetNormal() const { return mNormal; }
275 
GetPoints()276   const nsTArray<Point4DType>& GetPoints() const { return mPoints; }
277 
IsEmpty()278   bool IsEmpty() const {
279     // If the polygon has less than three points, it has no visible area.
280     return mPoints.Length() < 3;
281   }
282 
283   /**
284    * Returns a list of triangles covering the polygon.
285    */
ToTriangles()286   nsTArray<TriangleTyped<Units>> ToTriangles() const {
287     nsTArray<TriangleTyped<Units>> triangles;
288 
289     if (IsEmpty()) {
290       return triangles;
291     }
292 
293     // This fan triangulation method only works for convex polygons.
294     for (size_t i = 1; i < mPoints.Length() - 1; ++i) {
295       TriangleTyped<Units> triangle(Point(mPoints[0].x, mPoints[0].y),
296                                     Point(mPoints[i].x, mPoints[i].y),
297                                     Point(mPoints[i + 1].x, mPoints[i + 1].y));
298       triangles.AppendElement(std::move(triangle));
299     }
300 
301     return triangles;
302   }
303 
TransformToLayerSpace(const Matrix4x4Typed<Units,Units> & aTransform)304   void TransformToLayerSpace(const Matrix4x4Typed<Units, Units>& aTransform) {
305     TransformPoints(aTransform, true);
306     mNormal = DefaultNormal();
307   }
308 
TransformToScreenSpace(const Matrix4x4Typed<Units,Units> & aTransform,const Matrix4x4Typed<Units,Units> & aInverseTransform)309   void TransformToScreenSpace(
310       const Matrix4x4Typed<Units, Units>& aTransform,
311       const Matrix4x4Typed<Units, Units>& aInverseTransform) {
312     TransformPoints(aTransform, false);
313 
314     // Perspective projection transformation might produce points with w <= 0,
315     // so we need to clip these points.
316     mPoints = ClipPointsAtInfinity(mPoints);
317 
318     // Normal vectors should be transformed using inverse transpose.
319     mNormal = aInverseTransform.TransposeTransform4D(mNormal);
320   }
321 
TransformToScreenSpace(const Matrix4x4Typed<Units,Units> & aTransform)322   void TransformToScreenSpace(const Matrix4x4Typed<Units, Units>& aTransform) {
323     MOZ_ASSERT(!aTransform.IsSingular());
324 
325     TransformToScreenSpace(aTransform, aTransform.Inverse());
326   }
327 
328  private:
DefaultNormal()329   static Point4DType DefaultNormal() {
330     return Point4DType(0.0f, 0.0f, 1.0f, 0.0f);
331   }
332 
333 #ifdef DEBUG
EnsurePlanarPolygon()334   void EnsurePlanarPolygon() const {
335     if (mPoints.Length() <= 3) {
336       // Polygons with three or less points are guaranteed to be planar.
337       return;
338     }
339 
340     // This normal calculation method works only for planar polygons.
341     // The resulting normal vector will point towards the viewer when the
342     // polygon has a counter-clockwise winding order from the perspective
343     // of the viewer.
344     Point3DType normal;
345     const Point3DType p0 = mPoints[0].As3DPoint();
346 
347     for (size_t i = 1; i < mPoints.Length() - 1; ++i) {
348       const Point3DType p1 = mPoints[i].As3DPoint();
349       const Point3DType p2 = mPoints[i + 1].As3DPoint();
350 
351       normal += (p1 - p0).CrossProduct(p2 - p0);
352     }
353 
354     // Ensure that at least one component is greater than zero.
355     // This avoids division by zero when normalizing the vector.
356     bool hasNonZeroComponent = std::abs(normal.x) > 0.0f ||
357                                std::abs(normal.y) > 0.0f ||
358                                std::abs(normal.z) > 0.0f;
359 
360     MOZ_ASSERT(hasNonZeroComponent);
361 
362     normal.Normalize();
363 
364     // Ensure that the polygon is planar.
365     // http://mathworld.wolfram.com/Point-PlaneDistance.html
366     const float epsilon = 0.01f;
367     for (const Point4DType& point : mPoints) {
368       const Point3DType p1 = point.As3DPoint();
369       const float d = normal.DotProduct(p1 - p0);
370 
371       MOZ_ASSERT(std::abs(d) < epsilon);
372     }
373   }
374 #endif
375 
TransformPoints(const Matrix4x4Typed<Units,Units> & aTransform,const bool aDivideByW)376   void TransformPoints(const Matrix4x4Typed<Units, Units>& aTransform,
377                        const bool aDivideByW) {
378     for (Point4DType& point : mPoints) {
379       point = aTransform.TransformPoint(point);
380 
381       if (aDivideByW && point.w > 0.0f) {
382         point = point / point.w;
383       }
384     }
385   }
386 
387   Point4DType mNormal;
388   CopyableTArray<Point4DType> mPoints;
389 };
390 
391 typedef PolygonTyped<UnknownUnits> Polygon;
392 
393 }  // namespace gfx
394 }  // namespace mozilla
395 
396 #endif /* MOZILLA_GFX_POLYGON_H */
397