How to get the CGPoint(s) of a CGPath
Asked Answered
Z

4

26

How can I get an array with all the CGPoint(s) contained in a given CGPath (CGMutablePathRef)?

Zooid answered 20/10, 2012 at 20:40 Comment(0)
E
15

You can use CGPathApply() to iterate over every segment in the path and run a custom function with that segment. That will give you all the information the path has.

However, if by "all the CGPoint(s)", you meant every point that has a pixel rendered to it, that's an infinitely-sized set. Although you could certainly use the apply function to get each segment, and then for every non-move segment, evaluate your own math with the segment's control points to get a list of points at whatever density you want.

Ethridge answered 20/10, 2012 at 20:45 Comment(0)
S
65

Using Swift 2.x (for Swift 3.x, Swift 4.x and Swift 5.x read here below..) , i've found this fantastic article about C Callbacks in Swift.

Trying to obtain "all the CGPoint(s)", as explained by Lily Ballard, can be a bad idea as she said.

So, I think maybe the best way is to get the path elements points used to create a particular CGPath:

//MARK: - CGPath extensions
extension CGPath {
    func forEach(@noescape body: @convention(block) (CGPathElement) -> Void) {
        typealias Body = @convention(block) (CGPathElement) -> Void
        func callback(info: UnsafeMutablePointer<Void>, element: UnsafePointer<CGPathElement>) {
            let body = unsafeBitCast(info, Body.self)
            body(element.memory)
        }
        print(sizeofValue(body))
        let unsafeBody = unsafeBitCast(body, UnsafeMutablePointer<Void>.self)
        CGPathApply(self, unsafeBody, callback)
    }

    func getPathElementsPoints() -> [CGPoint] {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.MoveToPoint:
                arrayPoints.append(element.points[0])
            case .AddLineToPoint:
                arrayPoints.append(element.points[0])
            case .AddQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
            case .AddCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
            default: break
            }
        }
        return arrayPoints
    }
}

With this extension you can do for example:

var bezier = UIBezierPath(ovalInRect: CGRectMake(0, 0, 400, 300))
let myOval = bezier.CGPath
let junctionPoints = myOval.getPathElementsPoints()
print("junction points are: \(junctionPoints)")

Swift 3.x and Swift 4.1 (look below for Swift 4.2 or major..)

(there are some corrections due to syntax re-introduction of @convention(c)):

extension CGPath {

    func forEach( body: @convention(block) (CGPathElement) -> Void) {
        typealias Body = @convention(block) (CGPathElement) -> Void
        let callback: @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CGPathElement>) -> Void = { (info, element) in
            let body = unsafeBitCast(info, to: Body.self)
            body(element.pointee)
        }
        print(MemoryLayout.size(ofValue: body))
        let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
        self.apply(info: unsafeBody, function: unsafeBitCast(callback, to: CGPathApplierFunction.self))
    }


    func getPathElementsPoints() -> [CGPoint] {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
            default: break
            }
        }
        return arrayPoints
    }

    func getPathElementsPointsAndTypes() -> ([CGPoint],[CGPathElementType]) {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        var arrayTypes : [CGPathElementType]! = [CGPathElementType]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            default: break
            }
        }
        return (arrayPoints,arrayTypes)
    }
}

Swift > 4.1 (also Swift 5.x) and iOS 9.x and > compatible

extension CGPath {
    func forEach( body: @escaping @convention(block) (CGPathElement) -> Void) {
        typealias Body = @convention(block) (CGPathElement) -> Void
        let callback: @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CGPathElement>) -> Void = { (info, element) in
            let body = unsafeBitCast(info, to: Body.self)
            body(element.pointee)
        }
        //print(MemoryLayout.size(ofValue: body))
        let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
        self.apply(info: unsafeBody, function: unsafeBitCast(callback, to: CGPathApplierFunction.self))
    }
    func getPathElementsPoints() -> [CGPoint] {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
            default: break
            }
        }
        return arrayPoints
    }
    func getPathElementsPointsAndTypes() -> ([CGPoint],[CGPathElementType]) {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        var arrayTypes : [CGPathElementType]! = [CGPathElementType]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            default: break
            }
        }
        return (arrayPoints,arrayTypes)
    }
}
Shanelleshaner answered 2/4, 2016 at 14:0 Comment(6)
Thank you! This works in Xcode 8.3.2. (Though I had to rename forEach() to prevent conflicts with other CGPath/UIBezierPath extensions.) On that topic, I now prefix all extensions, to give them a poor-man's namespace.Toadeater
@Alessandro Ornano: After I use linePath.apply(CGAffineTransform(rotationAngle: the getPathElementsPoints() returns me an empty array for some reason. Do you have an idea why this is happening and how to fix?Glassine
I get the error message Converting non-escaping value to 'T' may allow it to escape on let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self) when using Xcode10 beta6, Swift 4.2Asbestosis
@smnk Thank you for your report, I've update my answerShanelleshaner
Thank you so much, again. This is extremely helpful.Toadeater
@Alessandro Ornano Why do you need unsafeBitCast(callback, to: CGPathApplierFunction.self) if callback can be modified to use optional UnsafeMutableRawPointer??Diameter
K
20

Apple added instance method CGPath.applyWithBlock, available for iOS11.0+ and macOS10.13+

You still can examine each element of a path with CGPath.apply as explained in previous answers. But if you want to avoid using C style pointers and unsafeBitCast, you should use the much more convenient instance method applyWithBlock. For example, if you want to get an array of CGPoints of a CGPath you can add an extension to CGPath, in this example a computed property(points) that collects the CGPoints of the CGPath:

/// Extension to collect CGPath points
extension CGPath {

  /// this is a computed property, it will hold the points we want to extract
  var points: [CGPoint] {

     /// this is a local transient container where we will store our CGPoints
     var arrPoints: [CGPoint] = []

     // applyWithBlock lets us examine each element of the CGPath, and decide what to do
     self.applyWithBlock { element in

        switch element.pointee.type
        {
        case .moveToPoint, .addLineToPoint:
          arrPoints.append(element.pointee.points.pointee)

        case .addQuadCurveToPoint:
          arrPoints.append(element.pointee.points.pointee)
          arrPoints.append(element.pointee.points.advanced(by: 1).pointee)

        case .addCurveToPoint:
          arrPoints.append(element.pointee.points.pointee)
          arrPoints.append(element.pointee.points.advanced(by: 1).pointee)
          arrPoints.append(element.pointee.points.advanced(by: 2).pointee)

        default:
          break
        }
     }

    // We are now done collecting our CGPoints and so we can return the result
    return arrPoints

  }
}
Karaganda answered 13/11, 2018 at 13:36 Comment(1)
This works really well. I don't use the .addQuadCurveToPoint and .addCurveToPoint though. This answer should be uprated!!Cassiecassil
E
15

You can use CGPathApply() to iterate over every segment in the path and run a custom function with that segment. That will give you all the information the path has.

However, if by "all the CGPoint(s)", you meant every point that has a pixel rendered to it, that's an infinitely-sized set. Although you could certainly use the apply function to get each segment, and then for every non-move segment, evaluate your own math with the segment's control points to get a list of points at whatever density you want.

Ethridge answered 20/10, 2012 at 20:45 Comment(0)
A
6

A CGPath is an opaque data type and does not necessarily store all the points used. In addition to this, a path may not actually draw all the points used as input (for example, consider Bézier control points).

The only two documented ways of getting information out of a path is to use CGPathGetBoundingBox to get the bounding box, or the more complicated method of using CGPathApply to call a callback function that will give you a sequence if CGPathElement types.

Amatruda answered 20/10, 2012 at 20:48 Comment(4)
The aim is to draw a stored CGpath as a Bezier path. It's a drawing program. Basically the path that is drawn freehand on screen is stored in an array for undo/redo purposes. I am looking for a way of retrieving the stored path from the array and draw it as a Bezier rather than a standard path. That's why I was trying to obtain all the points in the original path and then process them through the Bezier routine. Is this the wrong approach? Do you have any better solution for this? I don't want to use NSundomanager and I cannot store the paths as images as this is too CPU intensive. CheersZooid
Why not just store the points in an array and generate the paths when you draw them?Amatruda
I thought about this too but then I thought it would be easier to store the paths in an array for undo purposes (I will be dealing with different paths). If I do store each path's points as you suggest I would then need to create an array of CGPoints arrays. I am not too sure how to do this! Any idea pls?Zooid
Basically, a CGPath is an array of points, only a more complex and not as transparent to the application programmer. If you want to work with the points, it is probably easier to manage the data structure yourself.Amatruda

© 2022 - 2024 — McMap. All rights reserved.