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?
CodePudding user response:
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:
you will have multiple clipped segments.