Home > Back-end >  CGPath intersection(_:using:) and friends (new in iOS 16) broken?
CGPath intersection(_:using:) and friends (new in iOS 16) broken?

Time:01-03

There is new functionality in iOS 16 for CGPath (Core Graphics), more precisely there is a function intersection(_:using:) for determining the intersection of two CGPaths (see Blend of red and blue

Wrong intersection

EDIT: I have done more tests of all the 10 functions added in iOS16 (with correcting the mistake in my path formulation according to DonMag), and tried to find the most simple case where I think something goes wrong. So this (all the other code stays the same, only the paths change)

      let pathRedFill = CGPath(rect: CGRect(origin: CGPoint(x: 200, y: 300),
                                             size: CGSize(width: 200, height: 200)),
                                transform: nil)
       
       var pathBlueFill = CGMutablePath()
       pathBlueFill.move(to: CGPoint(x: 302.854156, y: 369.438843))
       pathBlueFill.addQuadCurve(to: CGPoint(x: 296.932526, y: 386.031342), control: CGPoint(x: 218.5, y: 376.0))
       pathBlueFill.closeSubpath()

gives the result:

enter image description here

The green line should circle the purple but does not.

This first was part of an example where there were lots of self-intersections both for the red and for the blue path and I thought the self-intersections of the paths were the problem (since I had some other quite complex examples without self-intersections, but with holes, where everything seemed to go fine). But when I was done stripping to minimum, it turns out that there is something else at odds, as there is no more self-intersections in either path... When rounding the numbers of the blue path, the error goes away, so it is this kind of degenerate corner case regarding the roots maybe... Also my blue curve is a quadratic (and then there is the line to close the path) whereas it turns out the intersection, stroked in green, is a cubic Bezier. I am not sure if I am still doing something wrong...

CodePudding user response:

Several possibilities...

  1. the new path intersection function (and maybe others) are a little "buggy"

  2. it's working correctly, but our understanding of how the paths are built is a bit fuzzy

  3. a combination of 1 & 2

After some experimentation, I get this with your original code on iOS (not sure why it's flipped vertically -- are you running this on MacOS app?):

enter image description here

Now, as I understand it, this line:

    var pathRedFill = CGMutablePath(rect: CGRect(origin: CGPoint(x: 10, y: 10),
                                                 size: CGSize(width: 400, height: 700)),
                                    transform: nil)
    

is equivalent to this:

    var pathRedFill = CGMutablePath()
    pathRedFill.move(to: CGPoint(x: 10, y: 10))
    pathRedFill.addLine(to: CGPoint(x: 410, y: 10))
    pathRedFill.addLine(to: CGPoint(x: 410, y: 710))
    pathRedFill.addLine(to: CGPoint(x: 10, y: 710))
    pathRedFill.closeSubpath()

The next thing we do is:

    pathRedFill.addQuadCurve(to: CGPoint(x: 267, y: 121), control: CGPoint(x: 395, y: 735))
    pathRedFill.closeSubpath()

And we get our expected output... but, when we complete the rest of the code we're getting (what appears to be) an incorrect intersection.

My guess is that there is some sort of "disconnect" when calling .addQuadCurve after the first subpath has been closed.

If we make this change:

    var pathRedFill = CGMutablePath(rect: CGRect(origin: CGPoint(x: 10, y: 10),
                                                 size: CGSize(width: 400, height: 700)),
                                    transform: nil)
    
    // add this line:
    pathRedFill.move(to: pathRedFill.currentPoint)
    
    pathRedFill.addQuadCurve(to: CGPoint(x: 267, y: 121), control: CGPoint(x: 395, y: 735))
    pathRedFill.closeSubpath()
    
    var pathBlueFill = CGMutablePath(rect: CGRect(origin: CGPoint(x: 120, y: 120),
                                                  size: CGSize(width: 200, height: 200)), transform: nil)

    // add this line:
    pathBlueFill.move(to: pathBlueFill.currentPoint)

    pathBlueFill.addQuadCurve(to: CGPoint(x: 637, y: 176), control: CGPoint(x: 343, y: 451))
    pathBlueFill.closeSubpath()

here's the output:

enter image description here

So it seems that calling .moveTo perhaps "explicitly starts a new subpath."

And then, internally, the paths/subpaths are more... complete?


Edit

For the second part of your question...

I think paths can be much more complex than it would seem on the surface.

For your specific example:

let pathRedFill = CGPath(rect: CGRect(origin: CGPoint(x: 200, y: 300),
                                      size: CGSize(width: 200, height: 200)),
                         transform: nil)

var pathBlueFill = CGMutablePath()
pathBlueFill.move(to: CGPoint(x: 302.854156, y: 369.438843))
pathBlueFill.addQuadCurve(to: CGPoint(x: 296.932526, y: 386.031342), control: CGPoint(x: 218.5, y: 376.0))
pathBlueFill.closeSubpath()

pathRedFill is a clockwise path, and pathBlueFill is a counter-clockwise path.

To see the difference, try changing the clockwise var in this code... when set to true we get a correct intersectionPath:

override func draw(_ rect: CGRect) {
    
    guard let context = UIGraphicsGetCurrentContext()
    else { return }
    
    var pathRedFill = CGMutablePath()
    var pathBlueFill = CGMutablePath()

    // 200 x 300 rect path
    pathRedFill = CGMutablePath(rect: CGRect(origin: CGPoint(x: 200, y: 300),
                                             size: CGSize(width: 200, height: 200)),
                                transform: nil)

    // switch between true and false to see the difference
    let clockwise: Bool = false
    
    var y1: CGFloat = 369.438843
    var y2: CGFloat = 386.031342
    
    if clockwise {
        let bez = UIBezierPath(cgPath: pathBlueFill)
        if let p = bez.reversing().cgPath.mutableCopy() {
            pathBlueFill = p
        }
    }
    
    pathBlueFill = CGMutablePath()
    
    pathBlueFill.move(to: CGPoint(x: 302.854156, y: y1))
    pathBlueFill.addQuadCurve(to: CGPoint(x: 296.932526, y: y2), control: CGPoint(x: 218.5, y: 376.0))
    
    pathBlueFill.closeSubpath()
    
    
    context.setBlendMode(.normal)
    
    context.setFillColor(red: 1, green: 0, blue: 0, alpha: 0.5)
    context.addPath(pathRedFill)
    context.drawPath(using: .eoFill)
    
    context.setFillColor(red: 0, green: 0, blue: 1, alpha: 0.5)
    context.addPath(pathBlueFill)
    context.drawPath(using: .eoFill)
    
    context.setLineWidth(1)
    context.setStrokeColor(red: 0, green: 1, blue: 0, alpha: 1)
    let intersectionPath = pathRedFill.intersection(pathBlueFill, using: .evenOdd)
    context.addPath(intersectionPath)
    context.drawPath(using: .stroke)

}

Edit 2

The above was meant to demonstrate that mixing clockwise and counter-clockwise subpaths can be problematic. I swapped the y1 and y2 values, but didn't take into account the controlPoint y value.

To avoid changing the geometry of the blue path, instead of swapping the y1 and y2 values, we can try reversing the path:

    pathBlueFill.closeSubpath()

    // reverse the blue path so it's going clockwise
    let bez = UIBezierPath(cgPath: pathBlueFill)
    if let p = bez.reversing().cgPath.mutableCopy() {
        pathBlueFill = p
    }

Now we get (I believe) the same geometry, along with the desired intersection path:

enter image description here

  • Related