skip to Main Content

CGRect has a very convenient built-in constant: CGRect.null

let aRect = ... //get rect from somewhere
CGRect.null.union(aRect) //is aRect
CGRect.null.intersection(aRect) //is CGRect.null

CGPath doesn’t have anything like this, meaning you can’t do something like:

let bunchOfPaths = [...] //array of CGPaths
let unionPath = bunchOfPaths.reduce(CGPath.null, { $0.union($1) }) //union of all the paths

Nor can you use CGRect.null to create the equivalent:

let nullRectPath = CGPath(rect: .null, transform: nil)
nullRectPath.isEmpty //false (?!)
let zeroRectPath = CGPath(rect: .zero, transform: nil)
nullRectPath.union(zeroRectPath) == zeroRectPath //false

Creating an empty path isn’t helpful either:

let emptyPath = zeroRectPath.subtracting(zeroRectPath)
emptyPath.isEmpty //true
emptyPath.union(zeroRectPath) == zeroRectPath //false (!!)

Does a CGPath with the properties described (its union with any CGPath is the other path, its intersection with any CGPath is itself) exist, and if so, how can it be created?

2

Answers


  1. Instead of CGPath, use CGMutablePath:

    let path = CGMutablePath()
    path.isEmpty // true
    
    Login or Signup to reply.
  2. The problem you’re facing is that CGPath’s == does not mean "contains the same points." It means "contains the same drawing instructions in the same order." And union(using:) does not promise to preserve the order of the drawing instructions. It can’t, since it has a fill rule. It’s not just appending two paths; it’s merging them.

    You can see this by printing out the contents:

    let rect1 = CGPath(rect: CGRect(origin: .zero, size: CGSize(width: 100, height: 100)), 
                       transform: nil)
    
    dump(rect1, name: "rect1")
    =>
    - rect1: Path 0x600002c04680:
      moveto (0, 0)
        lineto (100, 0)
        lineto (100, 100)
        lineto (0, 100)
        closepath
    
    dump(CGMutablePath().union(rect1, using: .evenOdd), name: "union")
    =>
    - union: Path 0x6000030054d0:
      moveto (100, 100)
        lineto (0, 100)
        lineto (0, 0)
        lineto (100, 0)
        closepath
    

    These are not "equal." The tool you want here is .addPath.

    let added = CGMutablePath()
    added.addPath(rect1)
    dump(added, name: "added")
    =>
    - added: Path 0x600003001680:
      moveto (0, 0)
        lineto (100, 0)
        lineto (100, 100)
        lineto (0, 100)
        closepath
    
    added == rect1    // true
    

    Now this is a bit annoying for your use case, but you can clean it up:

    extension CGPath {
        static var zero: CGPath { CGMutablePath() }
        func appending(_ path: CGPath) -> CGPath {
            var copy = self.mutableCopy() ?? CGMutablePath()
            copy.addPath(path)
            return copy
        }
    
        // Maybe this operator is controversial, but I don't think so. Union is not 
        // sensibly an "add" operation given its fill rule semantics. CGPath
        // is best thought of as a Collection of CGPathElements, and Swift
        // consistently appends Collections using +, even though it's not commutative.
        static func + (lhs: CGPath, rhs: CGPath) -> CGPath {
            lhs.appending(rhs)
        }
    }
    
    let bunchOfPaths = [rect1]
    let unionPath = bunchOfPaths.reduce(.zero, +)
    unionPath == rect1    // true
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search