// 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 #include #import #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 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