0

I have two intersecting bezier paths of type UIBezierPath like the following picture. How can I get the subpath depicted by red dashed line in Swift?

enter image description here

Asteroid
  • 1,049
  • 2
  • 8
  • 16
  • 1
    You will have to use (gasp) math! https://pomax.github.io/bezierinfo/#curveintersection – matt Apr 22 '22 at 16:04
  • I am trying to use https://github.com/adamwulf/ClippingBezier but no luck yet! – Asteroid Apr 22 '22 at 16:35
  • @Asteroid - what have you tried with that ClippingBezier code? A few minutes of digging with the included example app, and I changed the "Cut Shapes" example to this: https://i.stack.imgur.com/0xCyc.png ... is that what you're going for? – DonMag Apr 23 '22 at 14:48
  • Yes, it’s what I need, in Swift though. With ClippingBezier I tried to get what I need using the intersection functions but it gives the whole closed path intersection – Asteroid Apr 23 '22 at 20:36
  • @Asteroid - you really need to show the work you've done and explain where you're running into trouble. Otherwise (as with your previous question here: https://stackoverflow.com/questions/71917171/mask-a-gradient-layer-with-the-intersection-of-two-shape-layers-in-swift) we end up *guessing* at where you are and what you need help with. I posted an answer with some example code that *might* get you on your way. – DonMag Apr 24 '22 at 15:35

1 Answers1

1

Here's some sample code using ClippingBezier that may get you on your way...

It looks like this when run:

enter image description here enter image description here

enter image description here enter image description here

So, we define two sample shapes:

class SamplePaths: NSObject {

    // each set of 6 values defines:
    //  curve To point
    //  control point 1
    //  control point 2
    
    let start1: CGPoint = CGPoint(x: 29, y: 134)
    let vals1: [CGFloat] = [
        15, 34, 5, 94, -6, 57,
        274, 69, 68, -22, 148, 57,
        626, 7, 420, 82, 590, 22,
        871, 102, 662, -7, 845, 33,
        789, 286, 896, 172, 877, 188,
        700, 490, 702, 383, 719, 393,
        569, 605, 682, 587, 626, 638,
        330, 503, 433, 525, 375, 594,
        180, 227, 283, 282, 295, 271,
        29, 134, 65, 182, 53, 174,
    ]

    let start2: CGPoint = CGPoint(x: 240, y: 452)
    let vals2: [CGFloat] = [
        421.0, 369.0, 289.0, 452.0, 337.0, 369.0,
        676.0, 468.0, 506.0, 369.0, 581.0, 462.0,
        925.0, 369.0, 771.0, 474.0, 875.0, 385.0,
        1120.0, 397.0, 976.0, 354.0, 1090.0, 334.0,
        1086.0, 581.0, 1150.0, 460.0, 1119.0, 519.0,
        1127.0, 770.0, 1053.0, 643.0, 1173.0, 669.0,
        997.0, 845.0, 1081.0, 871.0, 1093.0, 857.0,
        786.0, 790.0, 902.0, 833.0, 910.0, 790.0,
        536.0, 854.0, 663.0, 790.0, 710.0, 860.0,
        290.0, 731.0, 362.0, 848.0, 405.0, 742.0,
        92.0, 770.0, 174.0, 721.0, 181.0, 794.0,
        3.0, 652.0, 3.0, 746.0, 3.0, 705.0,
        92.0, 519.0, 3.0, 587.0, 13.0, 550.0,
        171.0, 452.0, 172.0, 489.0, 131.0, 464.0,
        240.0, 452.0, 211.0, 439.0, 191.0, 452.0
    ]

    func samplePath(_ n: Int) -> UIBezierPath {
        var pt: CGPoint = .zero
        var c1: CGPoint = .zero
        var c2: CGPoint = .zero
        
        var start: CGPoint = .zero
        var vals: [CGFloat] = []
        
        if n == 1 {
            start = start1
            vals = vals1
        } else {
            start = start2
            vals = vals2
        }
        
        let bez = UIBezierPath()
        
        pt = start
        bez.move(to: pt)
        
        for i in stride(from: 0, to: vals.count, by: 6) {
            pt = CGPoint(x: vals[i + 0], y: vals[i + 1])
            c1 = CGPoint(x: vals[i + 2], y: vals[i + 3])
            c2 = CGPoint(x: vals[i + 4], y: vals[i + 5])
            bez.addCurve(to: pt, controlPoint1: c1, controlPoint2: c2)
        }
        
        bez.close()
        
        return bez
    }
    
}

Then we create a UIView subclass that strokes either First, Second, or Both paths, or only the Clipped path:

class TestView: UIView {
    
    enum Show {
        case both
        case first
        case second
        case clippedPath
    }
    
    public var show: Show = .both {
        didSet {
            setNeedsDisplay()
        }
    }
    
    public var path1: UIBezierPath!
    public var path2: UIBezierPath!
    
    override func draw(_ rect: CGRect) {

        // don't do anything if we don't have valid paths
        guard let path1 = self.path1,
              let path2 = self.path2
        else { return }
        
        // this fits the paths into self.bounds
        let margin: CGFloat = 8
        let fittingBounds = self.bounds.insetBy(dx: margin, dy: margin)
        let entireBounds = path1.bounds.union(path2.bounds)
        let scale = min(fittingBounds.size.width / entireBounds.size.width, fittingBounds.size.height / entireBounds.size.height)
        var transform: CGAffineTransform = .identity
        transform = transform.translatedBy(x: margin, y: margin)
        transform = transform.scaledBy(x: scale, y: scale)
        
        let ctx = UIGraphicsGetCurrentContext()
        ctx?.saveGState()
        ctx?.concatenate(transform)

        path1.lineWidth = 4
        path2.lineWidth = 4

        switch show {
        case .first:
            UIColor.systemGreen.setStroke()
            path1.stroke()
            
        case .second:
            UIColor.blue.setStroke()
            path2.stroke()
            
        case .clippedPath:
            // get the unique shapes from slicing path2 with path1
            guard let shapes: [DKUIBezierPathShape] = path2.uniqueShapesCreatedFromSlicing(withUnclosedPath: path1) else { return }
            // get the first shape
            guard let shape: DKUIBezierPathShape = shapes.first else { return }
            // get the first segment
            guard let seg = shape.segments.firstObject as? DKUIBezierPathClippedSegment else { return }
            // get the path from that segment
            let pth: UIBezierPath = seg.pathSegment
            // stroke that segment's path
            UIColor.red.setStroke()
            pth.stroke()
            
            // print the segment's path to the debug console
            print(pth)
            
        default:
            UIColor.systemGreen.setStroke()
            path1.stroke()
            UIColor.blue.setStroke()
            path2.stroke()
            
        }
        
        ctx?.restoreGState()
    }

}

and a sample controller to produce the above images:

// import the ClippingBezier library
import ClippingBezier

class ViewController: UIViewController {

    let testView: TestView = {
        let v = TestView()
        v.backgroundColor = .white
        return v
    }()
    
    let infoLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        return v
    }()

    // index to step through examples
    var idx: Int = 0

    override func viewDidLoad() {
        super.viewDidLoad()
    
        view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)

        // create a stack view
        let stack: UIStackView = {
            let v = UIStackView()
            v.axis = .vertical
            v.spacing = 8
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        // create a button
        let btn: UIButton = {
            let v = UIButton()
            v.setTitle("Next Step", for: [])
            v.setTitleColor(.white, for: .normal)
            v.setTitleColor(.lightGray, for: .highlighted)
            v.backgroundColor = .systemBlue
            v.layer.cornerRadius = 8
            v.addTarget(self, action: #selector(nextStep), for: .touchUpInside)
            return v
        }()
        
        // add elements to stack view
        [infoLabel, testView, btn].forEach { v in
            stack.addArrangedSubview(v)
        }
        
        stack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            stack.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            // let's use 300 x 300 for our test view
            testView.widthAnchor.constraint(equalToConstant: 300.0),
            testView.heightAnchor.constraint(equalTo: testView.widthAnchor),
        ])
        
        // set the bezier path shapes
        testView.path1 = SamplePaths().samplePath(1)
        testView.path2 = SamplePaths().samplePath(2)
        
        // get started
        nextStep()

    }

    
    @objc func nextStep() {
        
        switch idx % 4 {
        case 1:
            infoLabel.text = "Stroke Second Path"
            testView.show = .second
        case 2:
            infoLabel.text = "Stroke Both Paths"
            testView.show = .both
        case 3:
            infoLabel.text = "Stroke only the Clipped Path"
            testView.show = .clippedPath
        default:
            infoLabel.text = "Stroke First Path"
            testView.show = .first
        }

        idx += 1
        
    }

}

Notes:

This is Sample Code Only!!! It is meant to be a starting point.

You will need (probably a lot of) additional logic. For example, if your paths look like this:

enter image description here enter image description here

you will have multiple clipped segments.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • This answered my question, thanks! What if the green path is open and the open part is inside the intersection of the path1 and path2? – Asteroid Apr 25 '22 at 17:05
  • @Asteroid - you'll need to dig a little deeper into the code. Examine the various methods... Use debug to inspect the results of different actions... You may need to write some additional code to do exactly what you want. – DonMag Apr 27 '22 at 12:47