mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-28 07:13:20 +00:00
2e6385ca34
Differential Revision: https://phabricator.services.mozilla.com/D73635
397 lines
12 KiB
C++
397 lines
12 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#ifndef MOZILLA_GFX_POLYGON_H
|
|
#define MOZILLA_GFX_POLYGON_H
|
|
|
|
#include <initializer_list>
|
|
#include <utility>
|
|
|
|
#include "Matrix.h"
|
|
#include "Point.h"
|
|
#include "Triangle.h"
|
|
#include "nsTArray.h"
|
|
|
|
namespace mozilla {
|
|
namespace gfx {
|
|
|
|
/**
|
|
* Calculates the w = 0 intersection point for the edge defined by
|
|
* |aFirst| and |aSecond|.
|
|
*/
|
|
template <class Units>
|
|
Point4DTyped<Units> CalculateEdgeIntersect(const Point4DTyped<Units>& aFirst,
|
|
const Point4DTyped<Units>& aSecond) {
|
|
static const float w = 0.00001f;
|
|
const float t = (w - aFirst.w) / (aSecond.w - aFirst.w);
|
|
return aFirst + (aSecond - aFirst) * t;
|
|
}
|
|
|
|
/**
|
|
* Clips the polygon defined by |aPoints| so that there are no points with
|
|
* w <= 0.
|
|
*/
|
|
template <class Units>
|
|
nsTArray<Point4DTyped<Units>> ClipPointsAtInfinity(
|
|
const nsTArray<Point4DTyped<Units>>& aPoints) {
|
|
nsTArray<Point4DTyped<Units>> outPoints(aPoints.Length());
|
|
|
|
const size_t pointCount = aPoints.Length();
|
|
for (size_t i = 0; i < pointCount; ++i) {
|
|
const Point4DTyped<Units>& first = aPoints[i];
|
|
const Point4DTyped<Units>& second = aPoints[(i + 1) % pointCount];
|
|
|
|
if (!first.w || !second.w) {
|
|
// Skip edges at infinity.
|
|
continue;
|
|
}
|
|
|
|
if (first.w > 0.0f) {
|
|
outPoints.AppendElement(first);
|
|
}
|
|
|
|
if ((first.w <= 0.0f) ^ (second.w <= 0.0f)) {
|
|
outPoints.AppendElement(CalculateEdgeIntersect(first, second));
|
|
}
|
|
}
|
|
|
|
return outPoints;
|
|
}
|
|
|
|
/**
|
|
* Calculates the distances between the points in |aPoints| and the plane
|
|
* defined by |aPlaneNormal| and |aPlanePoint|.
|
|
*/
|
|
template <class Units>
|
|
nsTArray<float> CalculatePointPlaneDistances(
|
|
const nsTArray<Point4DTyped<Units>>& aPoints,
|
|
const Point4DTyped<Units>& aPlaneNormal,
|
|
const Point4DTyped<Units>& aPlanePoint, size_t& aPos, size_t& aNeg) {
|
|
// Point classification might produce incorrect results due to numerical
|
|
// inaccuracies. Using an epsilon value makes the splitting plane "thicker".
|
|
const float epsilon = 0.05f;
|
|
|
|
aPos = aNeg = 0;
|
|
nsTArray<float> distances(aPoints.Length());
|
|
|
|
for (const Point4DTyped<Units>& point : aPoints) {
|
|
float dot = (point - aPlanePoint).DotProduct(aPlaneNormal);
|
|
|
|
if (dot > epsilon) {
|
|
aPos++;
|
|
} else if (dot < -epsilon) {
|
|
aNeg++;
|
|
} else {
|
|
// The point is within the thick plane.
|
|
dot = 0.0f;
|
|
}
|
|
|
|
distances.AppendElement(dot);
|
|
}
|
|
|
|
return distances;
|
|
}
|
|
|
|
/**
|
|
* Clips the polygon defined by |aPoints|. The clipping uses previously
|
|
* calculated plane to point distances and the plane normal |aNormal|.
|
|
* The result of clipping is stored in |aBackPoints| and |aFrontPoints|.
|
|
*/
|
|
template <class Units>
|
|
void ClipPointsWithPlane(const nsTArray<Point4DTyped<Units>>& aPoints,
|
|
const Point4DTyped<Units>& aNormal,
|
|
const nsTArray<float>& aDots,
|
|
nsTArray<Point4DTyped<Units>>& aBackPoints,
|
|
nsTArray<Point4DTyped<Units>>& aFrontPoints) {
|
|
static const auto Sign = [](const float& f) {
|
|
if (f > 0.0f) return 1;
|
|
if (f < 0.0f) return -1;
|
|
return 0;
|
|
};
|
|
|
|
const size_t pointCount = aPoints.Length();
|
|
for (size_t i = 0; i < pointCount; ++i) {
|
|
size_t j = (i + 1) % pointCount;
|
|
|
|
const Point4DTyped<Units>& a = aPoints[i];
|
|
const Point4DTyped<Units>& b = aPoints[j];
|
|
const float dotA = aDots[i];
|
|
const float dotB = aDots[j];
|
|
|
|
// The point is in front of or on the plane.
|
|
if (dotA >= 0) {
|
|
aFrontPoints.AppendElement(a);
|
|
}
|
|
|
|
// The point is behind or on the plane.
|
|
if (dotA <= 0) {
|
|
aBackPoints.AppendElement(a);
|
|
}
|
|
|
|
// If the sign of the dot products changes between two consecutive
|
|
// vertices, then the plane intersects with the polygon edge.
|
|
// The case where the polygon edge is within the plane is handled above.
|
|
if (Sign(dotA) && Sign(dotB) && Sign(dotA) != Sign(dotB)) {
|
|
// Calculate the line segment and plane intersection point.
|
|
const Point4DTyped<Units> ab = b - a;
|
|
const float dotAB = ab.DotProduct(aNormal);
|
|
const float t = -dotA / dotAB;
|
|
const Point4DTyped<Units> p = a + (ab * t);
|
|
|
|
// Add the intersection point to both polygons.
|
|
aBackPoints.AppendElement(p);
|
|
aFrontPoints.AppendElement(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PolygonTyped stores the points of a convex planar polygon.
|
|
*/
|
|
template <class Units>
|
|
class PolygonTyped {
|
|
typedef Point3DTyped<Units> Point3DType;
|
|
typedef Point4DTyped<Units> Point4DType;
|
|
|
|
public:
|
|
PolygonTyped() = default;
|
|
|
|
explicit PolygonTyped(const nsTArray<Point4DType>& aPoints,
|
|
const Point4DType& aNormal = DefaultNormal())
|
|
: mNormal(aNormal), mPoints(aPoints) {}
|
|
|
|
explicit PolygonTyped(nsTArray<Point4DType>&& aPoints,
|
|
const Point4DType& aNormal = DefaultNormal())
|
|
: mNormal(aNormal), mPoints(std::move(aPoints)) {}
|
|
|
|
explicit PolygonTyped(const std::initializer_list<Point4DType>& aPoints,
|
|
const Point4DType& aNormal = DefaultNormal())
|
|
: mNormal(aNormal), mPoints(aPoints) {
|
|
#ifdef DEBUG
|
|
EnsurePlanarPolygon();
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Returns the smallest 2D rectangle that can fully cover the polygon.
|
|
*/
|
|
RectTyped<Units> BoundingBox() const {
|
|
if (mPoints.IsEmpty()) {
|
|
return RectTyped<Units>();
|
|
}
|
|
|
|
float minX, maxX, minY, maxY;
|
|
minX = maxX = mPoints[0].x;
|
|
minY = maxY = mPoints[0].y;
|
|
|
|
for (const Point4DType& point : mPoints) {
|
|
minX = std::min(point.x, minX);
|
|
maxX = std::max(point.x, maxX);
|
|
|
|
minY = std::min(point.y, minY);
|
|
maxY = std::max(point.y, maxY);
|
|
}
|
|
|
|
return RectTyped<Units>(minX, minY, maxX - minX, maxY - minY);
|
|
}
|
|
|
|
/**
|
|
* Clips the polygon against the given 2D rectangle.
|
|
*/
|
|
PolygonTyped<Units> ClipPolygon(const RectTyped<Units>& aRect) const {
|
|
if (aRect.IsEmpty()) {
|
|
return PolygonTyped<Units>();
|
|
}
|
|
|
|
return ClipPolygon(FromRect(aRect));
|
|
}
|
|
|
|
/**
|
|
* Clips this polygon against |aPolygon| in 2D and returns a new polygon.
|
|
*/
|
|
PolygonTyped<Units> ClipPolygon(const PolygonTyped<Units>& aPolygon) const {
|
|
const nsTArray<Point4DType>& points = aPolygon.GetPoints();
|
|
|
|
if (mPoints.IsEmpty() || points.IsEmpty()) {
|
|
return PolygonTyped<Units>();
|
|
}
|
|
|
|
nsTArray<Point4DType> clippedPoints(mPoints.Clone());
|
|
|
|
size_t pos, neg;
|
|
nsTArray<Point4DType> backPoints(4), frontPoints(4);
|
|
|
|
// Iterate over all the edges of the clipping polygon |aPolygon| and clip
|
|
// this polygon against the edges.
|
|
const size_t pointCount = points.Length();
|
|
for (size_t i = 0; i < pointCount; ++i) {
|
|
const Point4DType p1 = points[(i + 1) % pointCount];
|
|
const Point4DType p2 = points[i];
|
|
|
|
// Calculate the normal for the edge defined by |p1| and |p2|.
|
|
const Point4DType normal(p2.y - p1.y, p1.x - p2.x, 0.0f, 0.0f);
|
|
|
|
// Calculate the distances between the points of the polygon and the
|
|
// plane defined by |aPolygon|.
|
|
const nsTArray<float> distances =
|
|
CalculatePointPlaneDistances(clippedPoints, normal, p1, pos, neg);
|
|
|
|
backPoints.ClearAndRetainStorage();
|
|
frontPoints.ClearAndRetainStorage();
|
|
|
|
// Clip the polygon points using the previously calculated distances.
|
|
ClipPointsWithPlane(clippedPoints, normal, distances, backPoints,
|
|
frontPoints);
|
|
|
|
// Only use the points behind the clipping plane.
|
|
clippedPoints = std::move(backPoints);
|
|
|
|
if (clippedPoints.Length() < 3) {
|
|
// The clipping created a polygon with no area.
|
|
return PolygonTyped<Units>();
|
|
}
|
|
}
|
|
|
|
return PolygonTyped<Units>(std::move(clippedPoints), mNormal);
|
|
}
|
|
|
|
/**
|
|
* Returns a new polygon containing the bounds of the given 2D rectangle.
|
|
*/
|
|
static PolygonTyped<Units> FromRect(const RectTyped<Units>& aRect) {
|
|
nsTArray<Point4DType> points{
|
|
Point4DType(aRect.X(), aRect.Y(), 0.0f, 1.0f),
|
|
Point4DType(aRect.X(), aRect.YMost(), 0.0f, 1.0f),
|
|
Point4DType(aRect.XMost(), aRect.YMost(), 0.0f, 1.0f),
|
|
Point4DType(aRect.XMost(), aRect.Y(), 0.0f, 1.0f)};
|
|
|
|
return PolygonTyped<Units>(std::move(points));
|
|
}
|
|
|
|
const Point4DType& GetNormal() const { return mNormal; }
|
|
|
|
const nsTArray<Point4DType>& GetPoints() const { return mPoints; }
|
|
|
|
bool IsEmpty() const {
|
|
// If the polygon has less than three points, it has no visible area.
|
|
return mPoints.Length() < 3;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of triangles covering the polygon.
|
|
*/
|
|
nsTArray<TriangleTyped<Units>> ToTriangles() const {
|
|
nsTArray<TriangleTyped<Units>> triangles;
|
|
|
|
if (IsEmpty()) {
|
|
return triangles;
|
|
}
|
|
|
|
// This fan triangulation method only works for convex polygons.
|
|
for (size_t i = 1; i < mPoints.Length() - 1; ++i) {
|
|
TriangleTyped<Units> triangle(Point(mPoints[0].x, mPoints[0].y),
|
|
Point(mPoints[i].x, mPoints[i].y),
|
|
Point(mPoints[i + 1].x, mPoints[i + 1].y));
|
|
triangles.AppendElement(std::move(triangle));
|
|
}
|
|
|
|
return triangles;
|
|
}
|
|
|
|
void TransformToLayerSpace(const Matrix4x4Typed<Units, Units>& aTransform) {
|
|
TransformPoints(aTransform, true);
|
|
mNormal = DefaultNormal();
|
|
}
|
|
|
|
void TransformToScreenSpace(
|
|
const Matrix4x4Typed<Units, Units>& aTransform,
|
|
const Matrix4x4Typed<Units, Units>& aInverseTransform) {
|
|
TransformPoints(aTransform, false);
|
|
|
|
// Perspective projection transformation might produce points with w <= 0,
|
|
// so we need to clip these points.
|
|
mPoints = ClipPointsAtInfinity(mPoints);
|
|
|
|
// Normal vectors should be transformed using inverse transpose.
|
|
mNormal = aInverseTransform.TransposeTransform4D(mNormal);
|
|
}
|
|
|
|
void TransformToScreenSpace(const Matrix4x4Typed<Units, Units>& aTransform) {
|
|
MOZ_ASSERT(!aTransform.IsSingular());
|
|
|
|
TransformToScreenSpace(aTransform, aTransform.Inverse());
|
|
}
|
|
|
|
private:
|
|
static Point4DType DefaultNormal() {
|
|
return Point4DType(0.0f, 0.0f, 1.0f, 0.0f);
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
void EnsurePlanarPolygon() const {
|
|
if (mPoints.Length() <= 3) {
|
|
// Polygons with three or less points are guaranteed to be planar.
|
|
return;
|
|
}
|
|
|
|
// This normal calculation method works only for planar polygons.
|
|
// The resulting normal vector will point towards the viewer when the
|
|
// polygon has a counter-clockwise winding order from the perspective
|
|
// of the viewer.
|
|
Point3DType normal;
|
|
const Point3DType p0 = mPoints[0].As3DPoint();
|
|
|
|
for (size_t i = 1; i < mPoints.Length() - 1; ++i) {
|
|
const Point3DType p1 = mPoints[i].As3DPoint();
|
|
const Point3DType p2 = mPoints[i + 1].As3DPoint();
|
|
|
|
normal += (p1 - p0).CrossProduct(p2 - p0);
|
|
}
|
|
|
|
// Ensure that at least one component is greater than zero.
|
|
// This avoids division by zero when normalizing the vector.
|
|
bool hasNonZeroComponent = std::abs(normal.x) > 0.0f ||
|
|
std::abs(normal.y) > 0.0f ||
|
|
std::abs(normal.z) > 0.0f;
|
|
|
|
MOZ_ASSERT(hasNonZeroComponent);
|
|
|
|
normal.Normalize();
|
|
|
|
// Ensure that the polygon is planar.
|
|
// http://mathworld.wolfram.com/Point-PlaneDistance.html
|
|
const float epsilon = 0.01f;
|
|
for (const Point4DType& point : mPoints) {
|
|
const Point3DType p1 = point.As3DPoint();
|
|
const float d = normal.DotProduct(p1 - p0);
|
|
|
|
MOZ_ASSERT(std::abs(d) < epsilon);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void TransformPoints(const Matrix4x4Typed<Units, Units>& aTransform,
|
|
const bool aDivideByW) {
|
|
for (Point4DType& point : mPoints) {
|
|
point = aTransform.TransformPoint(point);
|
|
|
|
if (aDivideByW && point.w > 0.0f) {
|
|
point = point / point.w;
|
|
}
|
|
}
|
|
}
|
|
|
|
Point4DType mNormal;
|
|
CopyableTArray<Point4DType> mPoints;
|
|
};
|
|
|
|
typedef PolygonTyped<UnknownUnits> Polygon;
|
|
|
|
} // namespace gfx
|
|
} // namespace mozilla
|
|
|
|
#endif /* MOZILLA_GFX_POLYGON_H */
|