Home > Software design >  Get subpath from two intersecting Bezier paths
Get subpath from two intersecting Bezier paths

Time:04-26

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

CodePudding user response:

Here's some sample code using 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.

  • Related