blob: d1d4a99234e38574b21376612bb89fb843b8a890 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/gfx/path_mac.h"
#include <cmath>
#include <vector>
#import <Cocoa/Cocoa.h>
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkRegion.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/path.h"
namespace gfx {
namespace {
// Returns the point at a distance of |radius| from the point (|centre_x|,
// |centre_y|), and angle |degrees| from the positive horizontal axis, measured
// anti-clockwise.
NSPoint GetRadialPoint(double radius,
double degrees,
double centre_x,
double centre_y) {
const double radian = (degrees * SK_ScalarPI) / 180;
return NSMakePoint(centre_x + radius * std::cos(radian),
centre_y + radius * std::sin(radian));
}
// Returns the area of a circle with the given |radius|.
double CalculateCircleArea(double radius) {
return SK_ScalarPI * radius * radius;
}
// Returns the area of a simple polygon. |path| should represent a simple
// polygon.
double CalculatePolygonArea(NSBezierPath* path) {
// If path represents a single polygon, it will have MoveTo, followed by
// multiple LineTo, followed By ClosePath, followed by another MoveTo
// NSBezierPathElement.
const size_t element_count = [path elementCount];
NSPoint points[3];
std::vector<NSPoint> poly;
for (size_t i = 0; i < element_count - 1; i++) {
NSBezierPathElement element =
[path elementAtIndex:i associatedPoints:points];
poly.push_back(points[0]);
DCHECK_EQ(element,
i ? (i == element_count - 2 ? NSClosePathBezierPathElement
: NSLineToBezierPathElement)
: NSMoveToBezierPathElement);
}
DCHECK_EQ([path elementAtIndex:element_count - 1], NSMoveToBezierPathElement);
// Shoelace Algorithm to find the area of a simple polygon.
DCHECK(NSEqualPoints(poly.front(), poly.back()));
double area = 0;
for (size_t i = 0; i < poly.size() - 1; i++)
area += poly[i].x * poly[i + 1].y - poly[i].y * poly[i + 1].x;
return std::fabs(area) / 2.0;
}
// Returns the area of a rounded rectangle with the given |width|, |height| and
// |radius|.
double CalculateRoundedRectangleArea(double width,
double height,
double radius) {
const double inside_width = width - 2 * radius;
const double inside_height = height - 2 * radius;
return inside_width * inside_height +
2 * radius * (inside_width + inside_height) +
CalculateCircleArea(radius);
}
// Returns the bounding box of |path| as a Rect.
Rect GetBoundingBox(NSBezierPath* path) {
const NSRect bounds = [path bounds];
return ToNearestRect(RectF(bounds.origin.x, bounds.origin.y,
bounds.size.width, bounds.size.height));
}
} // namespace
// Check that empty NSBezierPath is returned for empty SkPath.
TEST(CreateNSBezierPathFromSkPathTest, EmptyPath) {
NSBezierPath* result = CreateNSBezierPathFromSkPath(SkPath());
EXPECT_TRUE([result isEmpty]);
}
// Check that the returned NSBezierPath has the correct winding rule.
TEST(CreateNSBezierPathFromSkPathTest, FillType) {
SkPath path;
path.setFillType(SkPath::kWinding_FillType);
NSBezierPath* result = CreateNSBezierPathFromSkPath(path);
EXPECT_EQ(NSNonZeroWindingRule, [result windingRule]);
path.setFillType(SkPath::kEvenOdd_FillType);
result = CreateNSBezierPathFromSkPath(path);
EXPECT_EQ(NSEvenOddWindingRule, [result windingRule]);
}
// Check that a path containing multiple subpaths, in this case two rectangles,
// is correctly converted to a NSBezierPath.
TEST(CreateNSBezierPathFromSkPathTest, TwoRectanglesPath) {
const SkRect rects[] = {
{0, 0, 50, 50}, {100, 100, 150, 150},
};
const NSPoint inside_points[] = {
{1, 1}, {1, 49}, {49, 49}, {49, 1}, {25, 25},
{101, 101}, {101, 149}, {149, 149}, {149, 101}, {125, 125}};
const NSPoint outside_points[] = {{-1, -1}, {-1, 51}, {51, 51}, {51, -1},
{99, 99}, {99, 151}, {151, 151}, {151, 99},
{75, 75}, {-5, -5}};
ASSERT_EQ(arraysize(inside_points), arraysize(outside_points));
const Rect expected_bounds(0, 0, 150, 150);
SkPath path;
path.addRect(rects[0]);
path.addRect(rects[1]);
NSBezierPath* result = CreateNSBezierPathFromSkPath(path);
// Check points near the boundary of the path and verify that they are
// reported correctly as being inside/outside the path.
for (size_t i = 0; i < arraysize(inside_points); i++) {
EXPECT_TRUE([result containsPoint:inside_points[i]]);
EXPECT_FALSE([result containsPoint:outside_points[i]]);
}
// Check that the returned result has the correct bounding box. GetBoundingBox
// rounds the coordinates to nearest integer values.
EXPECT_EQ(expected_bounds, GetBoundingBox(result));
}
// Test that an SKPath containing a circle is converted correctly to a
// NSBezierPath.
TEST(CreateNSBezierPathFromSkPathTest, CirclePath) {
const int kRadius = 5;
const int kCentreX = 10;
const int kCentreY = 15;
const double kCushion = 0.1;
// Expected bounding box of the circle.
const Rect expected_bounds(kCentreX - kRadius, kCentreY - kRadius,
2 * kRadius, 2 * kRadius);
SkPath path;
path.addCircle(SkIntToScalar(kCentreX), SkIntToScalar(kCentreY),
SkIntToScalar(kRadius));
NSBezierPath* result = CreateNSBezierPathFromSkPath(path);
// Check points near the boundary of the circle and verify that they are
// reported correctly as being inside/outside the path.
for (size_t deg = 0; deg < 360; deg++) {
NSPoint inside_point =
GetRadialPoint(kRadius - kCushion, deg, kCentreX, kCentreY);
NSPoint outside_point =
GetRadialPoint(kRadius + kCushion, deg, kCentreX, kCentreY);
EXPECT_TRUE([result containsPoint:inside_point]);
EXPECT_FALSE([result containsPoint:outside_point]);
}
// Check that the returned result has the correct bounding box. GetBoundingBox
// rounds the coordinates to nearest integer values.
EXPECT_EQ(expected_bounds, GetBoundingBox(result));
// Check area of converted path is correct up to a certain tolerance value. To
// find the area of the NSBezierPath returned, flatten it i.e. convert it to a
// polygon.
[NSBezierPath setDefaultFlatness:0.01];
NSBezierPath* polygon = [result bezierPathByFlatteningPath];
const double kTolerance = 0.14;
EXPECT_NEAR(CalculateCircleArea(kRadius), CalculatePolygonArea(polygon),
kTolerance);
}
// Test that an SKPath containing a rounded rectangle is converted correctly to
// a NSBezierPath.
TEST(CreateNSBezierPathFromSkPathTest, RoundedRectanglePath) {
const int kRectangleWidth = 50;
const int kRectangleHeight = 100;
const int kCornerRadius = 5;
const double kCushion = 0.1;
// Expected bounding box of the rounded rectangle.
const Rect expected_bounds(kRectangleWidth, kRectangleHeight);
SkRRect rrect;
rrect.setRectXY(SkRect::MakeWH(kRectangleWidth, kRectangleHeight),
kCornerRadius, kCornerRadius);
const NSPoint inside_points[] = {
// Bottom left corner.
{kCornerRadius / 2.0, kCornerRadius / 2.0},
// Bottom right corner.
{kRectangleWidth - kCornerRadius / 2.0, kCornerRadius / 2.0},
// Top Right corner.
{kRectangleWidth - kCornerRadius / 2.0,
kRectangleHeight - kCornerRadius / 2.0},
// Top left corner.
{kCornerRadius / 2.0, kRectangleHeight - kCornerRadius / 2.0},
// Bottom middle.
{kRectangleWidth / 2.0, kCushion},
// Right middle.
{kRectangleWidth - kCushion, kRectangleHeight / 2.0},
// Top middle.
{kRectangleWidth / 2.0, kRectangleHeight - kCushion},
// Left middle.
{kCushion, kRectangleHeight / 2.0}};
const NSPoint outside_points[] = {
// Bottom left corner.
{0, 0},
// Bottom right corner.
{kRectangleWidth, 0},
// Top right corner.
{kRectangleWidth, kRectangleHeight},
// Top left corner.
{0, kRectangleHeight},
// Bottom middle.
{kRectangleWidth / 2.0, -kCushion},
// Right middle.
{kRectangleWidth + kCushion, kRectangleHeight / 2.0},
// Top middle.
{kRectangleWidth / 2.0, kRectangleHeight + kCushion},
// Left middle.
{-kCushion, kRectangleHeight / 2.0}};
ASSERT_EQ(arraysize(inside_points), arraysize(outside_points));
SkPath path;
path.addRRect(rrect);
NSBezierPath* result = CreateNSBezierPathFromSkPath(path);
// Check points near the boundary of the path and verify that they are
// reported correctly as being inside/outside the path.
for (size_t i = 0; i < arraysize(inside_points); i++) {
EXPECT_TRUE([result containsPoint:inside_points[i]]);
EXPECT_FALSE([result containsPoint:outside_points[i]]);
}
// Check that the returned result has the correct bounding box. GetBoundingBox
// rounds the coordinates to nearest integer values.
EXPECT_EQ(expected_bounds, GetBoundingBox(result));
// Check area of converted path is correct up to a certain tolerance value. To
// find the area of the NSBezierPath returned, flatten it i.e. convert it to a
// polygon.
[NSBezierPath setDefaultFlatness:0.01];
NSBezierPath* polygon = [result bezierPathByFlatteningPath];
const double kTolerance = 0.14;
EXPECT_NEAR(CalculateRoundedRectangleArea(kRectangleWidth, kRectangleHeight,
kCornerRadius),
CalculatePolygonArea(polygon), kTolerance);
}
} // namespace gfx