From f09cd58fece1c547635004238e5415529045c6e5 Mon Sep 17 00:00:00 2001 From: Honghao Zhang Date: Tue, 10 Sep 2024 23:16:22 -0700 Subject: [PATCH] add NSBezierPath UIKit compatible code --- ChouTi | 2 +- .../BezierPath/NSBezierPath+Extensions.swift | 317 +++++++++++++++ .../NSBezierPath+ExtensionsTests.swift | 377 ++++++++++++++++++ 3 files changed, 695 insertions(+), 1 deletion(-) create mode 100644 Sources/ChouTiUI/AppKit/BezierPath/NSBezierPath+Extensions.swift create mode 100644 Tests/ChouTiUITests/AppKit/BezierPath/NSBezierPath+ExtensionsTests.swift diff --git a/ChouTi b/ChouTi index 75c045a..8293e2c 160000 --- a/ChouTi +++ b/ChouTi @@ -1 +1 @@ -Subproject commit 75c045a0e068887cdb6e6a677feb7b60a771837c +Subproject commit 8293e2c14fdf9abc684e5af3bb40106560ecb94b diff --git a/Sources/ChouTiUI/AppKit/BezierPath/NSBezierPath+Extensions.swift b/Sources/ChouTiUI/AppKit/BezierPath/NSBezierPath+Extensions.swift new file mode 100644 index 0000000..e658b80 --- /dev/null +++ b/Sources/ChouTiUI/AppKit/BezierPath/NSBezierPath+Extensions.swift @@ -0,0 +1,317 @@ +// +// NSBezierPath+Extensions.swift +// ChouTiUI +// +// Created by Honghao Zhang on 9/4/21. +// Copyright © 2020 Honghao Zhang. +// +// MIT License +// +// Copyright (c) 2020 Honghao Zhang (github.com/honghaoz) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +#if canImport(AppKit) + +import AppKit + +import ChouTi + +// MARK: - UIBezierPath (UIKit) Compatibility + +public extension NSBezierPath { + + /// Creates and returns a new Bézier path object with an arc of a circle. + /// + /// - Parameters: + /// - center: Specifies the center point of the circle (in the current coordinate system) used to define the arc. + /// - radius: Specifies the radius of the circle used to define the arc. + /// - startAngle: Specifies the starting angle of the arc (measured in radians). + /// - endAngle: Specifies the end angle of the arc (measured in radians). + /// - clockwise: The direction in which to draw the arc. + convenience init(arcCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) { + self.init() + + // iOS uses radians for angles + // macOS uses degrees for angles + // NSBezierPath has a revered clockwise flag 🤨. iOS and macOS uses different directions. + // + // reference: + // - https://stackoverflow.com/a/31219154/3164091 + // - https://gist.github.com/seivan/d360aaec9780692e3520 + appendArc(withCenter: center, radius: radius, startAngle: startAngle.toDegrees, endAngle: endAngle.toDegrees, clockwise: !clockwise) + } + + /// Transforms all points in the path using the specified affine transform matrix. + /// + /// This method applies the specified transform to the path’s points immediately. + /// + /// - Parameter transform: The transform matrix to apply to the path. + @inlinable + @inline(__always) + func apply(_ transform: CGAffineTransform) { + self.transform(using: transform.affineTransform) + } + + /// Appends a straight line to the path. + /// + /// This method creates a straight line segment starting at the current point and ending at the point specified by the point parameter. + /// After adding the line segment, this method updates the current point to the value in point. + /// + /// You must set the path’s current point (using the `move(to:)` method or through the previous creation of a line or curve segment) + /// before you call this method. If the path is empty, this method does nothing. + /// + /// - Parameter point: The destination point of the line segment, specified in the current coordinate system. + @inlinable + @inline(__always) + func addLine(to point: CGPoint) { + line(to: point) + } + + /// Appends a cubic Bézier curve to the path. + /// + /// This method appends a cubic Bézier curve from the current point to the end point specified by the `endPoint` parameter. + /// + /// You must set the path's current point (using the `move(to:)` method or through the creation of a preceding line or curve + /// segment) before you invoke this method. If the path is empty, this method raises an `genericException` exception. + /// + /// - Parameters: + /// - endPoint: The destination point of the curve segment, specified in the current coordinate system + /// - controlPoint1: The point that determines the shape of the curve near the current point. + /// - controlPoint2: The point that determines the shape of the curve near the destination point. + @inlinable + @inline(__always) + func addCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) { + curve(to: endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) + } + + /// Appends a quadratic Bézier curve to the path. + /// + /// This method appends a quadratic Bézier curve from the current point to the end point specified by the endPoint parameter. + /// + /// You must set the path's current point (using the `move(to:)` method or through the creation of a preceding line or curve + /// segment) before you invoke this method. If the path is empty, this method raises an `genericException` exception. + /// + /// - Parameters: + /// - endPoint: The destination point of the curve segment, specified in the current coordinate system + /// - controlPoint: The control point of the curve. + func addQuadCurve(to point: CGPoint, controlPoint: CGPoint) { + if #available(macOS 14.0, *) { + curve(to: point, controlPoint: controlPoint) + } else { + addQuadCurve_below_macOS14(to: point, controlPoint: controlPoint) + } + } + + private func addQuadCurve_below_macOS14(to point: CGPoint, controlPoint: CGPoint) { + let (d1x, d1y) = (controlPoint.x - currentPoint.x, controlPoint.y - currentPoint.y) + let (d2x, d2y) = (point.x - controlPoint.x, point.y - controlPoint.y) + let cp1 = CGPoint(x: controlPoint.x - d1x / 3.0, y: controlPoint.y - d1y / 3.0) + let cp2 = CGPoint(x: controlPoint.x + d2x / 3.0, y: controlPoint.y + d2y / 3.0) + curve(to: point, controlPoint1: cp1, controlPoint2: cp2) + } + + /// Appends an arc of a circle to the path. + /// + /// This method adds the specified arc beginning at the current point. The created arc lies on the perimeter of the specified circle. + /// + /// - Parameters: + /// - center: Specifies the center point of the circle used to define the arc. + /// - radius: Specifies the radius of the circle used to define the arc. + /// - startAngle: Specifies the starting angle of the arc (measured in radians). + /// - endAngle: Specifies the end angle of the arc (measured in radians). + /// - clockwise: The direction in which to draw the arc. + func addArc(withCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) { + // NSBezierPath has a revered clockwise flag 🤨. iOS and macOS uses different directions. + appendArc(withCenter: center, radius: radius, startAngle: startAngle.toDegrees, endAngle: endAngle.toDegrees, clockwise: !clockwise) + } + + /// Creates and returns a new Bézier path object with the reversed contents of the current path. + /// + /// - Returns: A new Bézier path object with the same path shape but for which the path has been created in the reverse direction. + @inlinable + @inline(__always) + func reversing() -> NSBezierPath { + reversed + } + + /// A Boolean value that indicates whether the even-odd winding rule is in use for drawing paths. + /// + /// If true, the path is filled using the even-odd rule. If false, it is filled using the non-zero rule. + /// Both rules are algorithms to determine which areas of a path to fill with the current fill color. + /// A ray is drawn from a point inside a given region to a point anywhere outside the path’s bounds. + /// + /// The total number of crossed path lines (including implicit path lines) and the direction of each path line are then interpreted as follows: + /// + /// - For the even-odd rule, if the total number of path crossings is odd, the point is considered to be inside the path and the corresponding region is filled. + /// If the number of crossings is even, the point is considered to be outside the path and the region is not filled. + /// + /// - For the non-zero rule, the crossing of a left-to-right path counts as +1 and the crossing of a right-to-left path counts as -1. + /// If the sum of the crossings is nonzero, the point is considered to be inside the path and the corresponding region is filled. + /// If the sum is 0, the point is outside the path and the region is not filled. + /// + /// The default value of this property is false. For more information about winding rules and how they are applied to subpaths, + /// see [Quartz 2D Programming Guide](https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html#//apple_ref/doc/uid/TP30001066). + var usesEvenOddFillRule: Bool { + /** + https://www.sitepoint.com/understanding-svg-fill-rule-property/ + - non zero: drawing a line from the point in question through the shape in any direction. + 1. start with a count of 0. + 2. add 1 each time a path segment crosses the line from left to right (clockwise) + 3. subtract 1 each time a path segment crosses from right to left (counterclockwise). + 4. zero is outside, non-zero is inside + + - even odd (winding): drawing a line from the area in question through the entire shape in any direction. + 1. The path segments that cross this line are then counted. + 2. If the final number is even, the point is outside; + 3. if it’s odd, the point is inside. + */ + get { + windingRule == .evenOdd + } + set { + windingRule = newValue ? .evenOdd : .nonZero + } + } + + // MARK: - CGPath + + /// The Core Graphics representation of the path. + var cgPath: CGPath { + /// https://stackoverflow.com/a/39385101/3164091 + get { + let path = CGMutablePath() + var points = [CGPoint](repeating: .zero, count: 3) + + for i in 0 ..< elementCount { + let type = element(at: i, associatedPoints: &points) + switch type { + case .moveTo: + path.move(to: points[0]) + case .lineTo: + path.addLine(to: points[0]) + case .quadraticCurveTo: + path.addQuadCurve(to: points[1], control: points[0]) + case .cubicCurveTo: + path.addCurve(to: points[2], control1: points[0], control2: points[1]) + case .closePath: + path.closeSubpath() + @unknown default: + ChouTi.assertFailure("Unknown CGPath element type", metadata: ["type": "\(type)"]) + continue + } + } + + return path + } + set { + self.removeAllPoints() + self.addCGPath(newValue) + } + } + + /// Creates and returns a new Bézier path object with the contents of a Core Graphics path. + /// + /// - Parameter cgPath: The Core Graphics path from which to obtain the path information + convenience init(cgPath: CGPath) { + /// References: + /// - https://juripakaste.fi/nzbezierpath-cgpath/ + /// - https://gist.github.com/lukaskubanek/1f3585314903dfc66fc7 + self.init() + addCGPath(cgPath) + } + + /// Adds a Core Graphics path to the current Bézier path. + /// + /// - Parameter cgPath: The Core Graphics path to add. + private func addCGPath(_ cgPath: CGPath) { + // Documentation of `applyWithBlock(_:)` + // https://stackoverflow.com/a/53282221/3164091 + cgPath.applyWithBlock { (elementPointer: UnsafePointer) in + let element = elementPointer.pointee + let points = element.points + switch element.type { + case .moveToPoint: + self.move(to: points.pointee) + case .addLineToPoint: + self.line(to: points.pointee) + case .addQuadCurveToPoint: + let control = points.pointee + let target = points.successor().pointee + self.addQuadCurve(to: target, controlPoint: control) + + // use cubic curve: + // + // let qp0 = self.currentPoint + // let qp1 = points.pointee + // let qp2 = points.successor().pointee + // let m = 2.0 / 3.0 + // let cp1 = NSPoint( + // x: qp0.x + ((qp1.x - qp0.x) * m), + // y: qp0.y + ((qp1.y - qp0.y) * m) + // ) + // let cp2 = NSPoint( + // x: qp2.x + ((qp1.x - qp2.x) * m), + // y: qp2.y + ((qp1.y - qp2.y) * m) + // ) + // self.curve(to: qp2, controlPoint1: cp1, controlPoint2: cp2) + case .addCurveToPoint: + let control1 = points.pointee + let control2 = points.advanced(by: 1).pointee + let target = points.advanced(by: 2).pointee + self.curve(to: target, controlPoint1: control1, controlPoint2: control2) + case .closeSubpath: + self.close() + @unknown default: + ChouTi.assertFailure("Unknown CGPath element type", metadata: ["type": "\(element.type)"]) + } + } + } + + // MARK: - Testing + + #if DEBUG + + var test: Test { Test(host: self) } + + class Test { + + private let host: NSBezierPath + + fileprivate init(host: NSBezierPath) { + ChouTi.assert(Thread.isRunningXCTest, "test namespace should only be used in test target.") + self.host = host + } + + func addQuadCurve_below_macOS14(to point: CGPoint, controlPoint: CGPoint) { + host.addQuadCurve_below_macOS14(to: point, controlPoint: controlPoint) + } + } + + #endif +} + +/** + Readings: + - https://gist.github.com/erica/ec3e2a4a8526e3fc3ba1fc95a0d53083 + - [NSBezierPath port](https://gist.github.com/cemolcay/28cb15001cd4786e78830369e074aa5c) + */ + +#endif diff --git a/Tests/ChouTiUITests/AppKit/BezierPath/NSBezierPath+ExtensionsTests.swift b/Tests/ChouTiUITests/AppKit/BezierPath/NSBezierPath+ExtensionsTests.swift new file mode 100644 index 0000000..ab08c72 --- /dev/null +++ b/Tests/ChouTiUITests/AppKit/BezierPath/NSBezierPath+ExtensionsTests.swift @@ -0,0 +1,377 @@ +// +// NSBezierPath+ExtensionsTests.swift +// ChouTiUI +// +// Created by Honghao Zhang on 9/9/24. +// Copyright © 2020 Honghao Zhang. +// +// MIT License +// +// Copyright (c) 2020 Honghao Zhang (github.com/honghaoz) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +#if canImport(AppKit) +import AppKit +#endif + +#if canImport(UIKit) +import UIKit +#endif + +import ChouTiTest + +import ChouTi +@testable import ChouTiUI + +class NSBezierPath_ExtensionsTests: XCTestCase { + + func test_init_arc() { + let path = BezierPath(arcCenter: .zero, radius: 1, startAngle: 0, endAngle: .pi, clockwise: true) + let memoryAddressString = memoryAddressString(path) + + #if canImport(AppKit) + expect(String(describing: path)) == + """ + Path <\(memoryAddressString)> + Bounds: {{-1, 0}, {2, 1}} + Control point bounds: {{-1, 0}, {2, 1}} + 1.000000 0.000000 moveto + 1.000000 0.552285 0.552285 1.000000 0.000000 1.000000 curveto + -0.552285 1.000000 -1.000000 0.552285 -1.000000 0.000000 curveto + """ + #endif + + #if canImport(UIKit) + expect(String(describing: path)) == + """ + , + , + + """ + #endif + } + + func test_apply() { + let path = BezierPath() + path.move(to: .zero) + path.addLine(to: CGPoint(x: 1, y: 1)) + path.addCurve(to: CGPoint(x: 0, y: 0), controlPoint1: CGPoint(x: 1, y: 0.5), controlPoint2: CGPoint(x: 0.5, y: 1)) + path.apply(CGAffineTransform(translationX: 1, y: 2)) + expect(path.currentPoint) == CGPoint(x: 1, y: 2) + + let memoryAddressString = memoryAddressString(path) + + #if canImport(AppKit) + expect(String(describing: path)) == + """ + Path <\(memoryAddressString)> + Bounds: {{1, 2}, {1, 1}} + Control point bounds: {{1, 2}, {1, 1}} + 1.000000 2.000000 moveto + 2.000000 3.000000 lineto + 2.000000 2.500000 1.500000 3.000000 1.000000 2.000000 curveto + """ + #endif + + #if canImport(UIKit) + expect(String(describing: path)) == + """ + , + , + + """ + #endif + } + + func test_addLine() { + let path = BezierPath() + path.move(to: .zero) + path.addLine(to: CGPoint(x: 1, y: 1)) + path.addLine(to: CGPoint(x: 2, y: 2)) + expect(path.currentPoint) == CGPoint(x: 2, y: 2) + + let memoryAddressString = memoryAddressString(path) + + #if canImport(AppKit) + expect(String(describing: path)) == + """ + Path <\(memoryAddressString)> + Bounds: {{0, 0}, {2, 2}} + Control point bounds: {{0, 0}, {2, 2}} + 0.000000 0.000000 moveto + 1.000000 1.000000 lineto + 2.000000 2.000000 lineto + """ + #endif + + #if canImport(UIKit) + expect(String(describing: path)) == + """ + , + , + + """ + #endif + } + + func test_addCurve() { + let path = BezierPath() + path.move(to: .zero) + path.addCurve(to: CGPoint(x: 1, y: 1), controlPoint1: CGPoint(x: 0.5, y: 0), controlPoint2: CGPoint(x: 1, y: 0.5)) + expect(path.currentPoint) == CGPoint(x: 1, y: 1) + + let memoryAddressString = memoryAddressString(path) + + #if canImport(AppKit) + expect(String(describing: path)) == + """ + Path <\(memoryAddressString)> + Bounds: {{0, 0}, {1, 1}} + Control point bounds: {{0, 0}, {1, 1}} + 0.000000 0.000000 moveto + 0.500000 0.000000 1.000000 0.500000 1.000000 1.000000 curveto + """ + #endif + + #if canImport(UIKit) + expect(String(describing: path)) == + """ + , + + """ + #endif + } + + func test_addQuadCurve() { + // Test for macOS 14+ + do { + let path = BezierPath() + let memoryAddressString = memoryAddressString(path) + path.move(to: .zero) + path.addQuadCurve(to: CGPoint(x: 1, y: 1), controlPoint: CGPoint(x: 0.5, y: 0)) + expect(path.currentPoint) == CGPoint(x: 1, y: 1) + + #if canImport(AppKit) + expect(String(describing: path)) == + """ + Path <\(memoryAddressString)> + Bounds: {{0, 0}, {1, 1}} + Control point bounds: {{0, 0}, {1, 1}} + 0.000000 0.000000 moveto + 0.500000 0.000000 1.000000 1.000000 quadcurveto + """ + #endif + + #if canImport(UIKit) + expect(String(describing: path)) == + """ + , + + """ + #endif + } + + #if canImport(AppKit) + // Test for macOS 13- + do { + let path = BezierPath() + let memoryAddressString = memoryAddressString(path) + path.move(to: .zero) + path.test.addQuadCurve_below_macOS14(to: CGPoint(x: 1, y: 1), controlPoint: CGPoint(x: 0.5, y: 0)) + expect(path.currentPoint) == CGPoint(x: 1, y: 1) + + expect(String(describing: path)) == + """ + Path <\(memoryAddressString)> + Bounds: {{0, 0}, {1, 1}} + Control point bounds: {{0, 0}, {1, 1}} + 0.000000 0.000000 moveto + 0.333333 0.000000 0.666667 0.333333 1.000000 1.000000 curveto + """ + } + #endif + } + + func test_addArc() { + let path = BezierPath() + path.move(to: .zero) + path.addArc(withCenter: CGPoint(x: 1, y: 1), radius: 1, startAngle: 0, endAngle: .pi, clockwise: true) + expect(path.currentPoint.isApproximatelyEqual(to: CGPoint(x: 0, y: 1), absoluteTolerance: 1e-6)) == true + + let memoryAddressString = memoryAddressString(path) + + #if canImport(AppKit) + expect(String(describing: path)) == + """ + Path <\(memoryAddressString)> + Bounds: {{0, 0}, {2, 2}} + Control point bounds: {{0, 0}, {2, 2}} + 0.000000 0.000000 moveto + 2.000000 1.000000 lineto + 2.000000 1.552285 1.552285 2.000000 1.000000 2.000000 curveto + 0.447715 2.000000 0.000000 1.552285 0.000000 1.000000 curveto + """ + #endif + + #if canImport(UIKit) + expect(String(describing: path)) == + """ + , + , + , + + """ + #endif + } + + func test_reversing() { + let path = BezierPath() + path.move(to: .zero) + path.addLine(to: CGPoint(x: 1, y: 1)) + path.addCurve(to: CGPoint(x: 0, y: 0), controlPoint1: CGPoint(x: 1, y: 0.5), controlPoint2: CGPoint(x: 0.5, y: 1)) + let reversedPath = path.reversing() + + let memoryAddressString = memoryAddressString(reversedPath) + + #if canImport(AppKit) + expect(String(describing: reversedPath)) == + """ + Path <\(memoryAddressString)> + Bounds: {{0, 0}, {1, 1}} + Control point bounds: {{0, 0}, {1, 1}} + 0.000000 0.000000 moveto + 0.500000 1.000000 1.000000 0.500000 1.000000 1.000000 curveto + 0.000000 0.000000 lineto + """ + #endif + + #if canImport(UIKit) + expect(String(describing: reversedPath)) == + """ + , + , + + """ + #endif + } + + func test_useEvenOddRule() { + let path = BezierPath() + path.move(to: .zero) + path.addLine(to: CGPoint(x: 1, y: 1)) + path.addLine(to: CGPoint(x: 2, y: 2)) + path.close() + + path.usesEvenOddFillRule = true + expect(path.usesEvenOddFillRule) == true + #if canImport(AppKit) + expect(path.windingRule) == .evenOdd + #endif + + path.usesEvenOddFillRule = false + expect(path.usesEvenOddFillRule) == false + #if canImport(AppKit) + expect(path.windingRule) == .nonZero + #endif + } + + func test_cgPath() { + let path = BezierPath() + path.move(to: .zero) + path.addLine(to: CGPoint(x: 1, y: 1)) + path.addLine(to: CGPoint(x: 1, y: 2)) + path.addCurve(to: CGPoint(x: 10, y: 0), controlPoint1: CGPoint(x: 1, y: 1.5), controlPoint2: CGPoint(x: 0.5, y: 2)) + path.addQuadCurve(to: CGPoint(x: 10, y: 10), controlPoint: CGPoint(x: 0.5, y: 1)) + path.close() + + let cgPath = path.cgPath + let cgPathMemoryAddressString = memoryAddressString(cgPath) + + #if canImport(AppKit) + expect(String(describing: cgPath)) == "Path \(cgPathMemoryAddressString):\n moveto (0, 0)\n lineto (1, 1)\n lineto (1, 2)\n curveto (1, 1.5) (0.5, 2) (10, 0)\n quadto (0.5, 1) (10, 10)\n closepath\n moveto (0, 0)\n" + #endif + #if canImport(UIKit) + expect(String(describing: cgPath)) == "Path \(cgPathMemoryAddressString):\n moveto (0, 0)\n lineto (1, 1)\n lineto (1, 2)\n curveto (1, 1.5) (0.5, 2) (10, 0)\n quadto (0.5, 1) (10, 10)\n closepath\n" + #endif + + let newCGPath = CGPath(rect: CGRect(x: 0, y: 0, width: 10, height: 10), transform: nil) + let newCGPathMemoryAddressString = memoryAddressString(newCGPath) + + path.cgPath = newCGPath + let pathCGPathMemoryAddressString = memoryAddressString(path.cgPath) + + expect(String(describing: newCGPath)) == "Path \(newCGPathMemoryAddressString):\n moveto (0, 0)\n lineto (10, 0)\n lineto (10, 10)\n lineto (0, 10)\n closepath\n" + #if canImport(AppKit) + expect(String(describing: path.cgPath)) == "Path \(pathCGPathMemoryAddressString):\n moveto (0, 0)\n lineto (10, 0)\n lineto (10, 10)\n lineto (0, 10)\n closepath\n moveto (0, 0)\n" + #endif + #if canImport(UIKit) + expect(String(describing: path.cgPath)) == "Path \(pathCGPathMemoryAddressString):\n moveto (0, 0)\n lineto (10, 0)\n lineto (10, 10)\n lineto (0, 10)\n closepath\n" + #endif + } + + func test_init_cgPath() { + let cgPath = CGMutablePath() + cgPath.addRect(CGRect(x: 0, y: 0, width: 10, height: 10)) + cgPath.addQuadCurve(to: CGPoint(x: 10, y: 5), control: CGPoint(x: 5, y: 5)) + cgPath.addCurve(to: CGPoint(x: 5, y: 5), control1: CGPoint(x: 7.5, y: 0), control2: CGPoint(x: 2.5, y: 10)) + cgPath.closeSubpath() + + let path = BezierPath(cgPath: cgPath) + + let cgPathMemoryAddressString = memoryAddressString(cgPath) + let pathMemoryAddressString = memoryAddressString(path) + + expect(String(describing: cgPath)) == "Path \(cgPathMemoryAddressString):\n moveto (0, 0)\n lineto (10, 0)\n lineto (10, 10)\n lineto (0, 10)\n closepath\n quadto (5, 5) (10, 5)\n curveto (7.5, 0) (2.5, 10) (5, 5)\n closepath\n" + + #if canImport(AppKit) + expect(String(describing: path)) == + """ + Path <\(pathMemoryAddressString)> + Bounds: {{0, 0}, {10, 10}} + Control point bounds: {{0, 0}, {10, 10}} + 0.000000 0.000000 moveto + 10.000000 0.000000 lineto + 10.000000 10.000000 lineto + 0.000000 10.000000 lineto + closepath + 0.000000 0.000000 moveto + 5.000000 5.000000 10.000000 5.000000 quadcurveto + 7.500000 0.000000 2.500000 10.000000 5.000000 5.000000 curveto + closepath + 0.000000 0.000000 moveto + """ + #endif + + #if canImport(UIKit) + expect(String(describing: path)) == + """ + , + , + , + , + , + , + , + + """ + #endif + } +}