I have array of CGPoint
indicated in red. I must connect all consecutive points with CGRect
as shown in the rough figure to form an array of CGRect
. Consider all rect has a constant width and each point could be the mid point of the sides formed from width of the rect. How could I form this array of CGRect
?
EDIT:
Here is what I am trying to do. As described in my previous question, I am trying to introduce an eraser function.
I have a set of straight lines (in green). I basically contain the start and end points of the line in model. When in eraser mode, user is allowed to draw freely and all draw points are connected by stroke (in purple).
When any green line is completely covered, the line must be identified to be erased. But, I couldn't able to determine if the line is completed covered by the eraser's draw points.
As in comments, I tried to follow the answer here. All I have is CGPoint
s and I have no CGRect
s. I check the intersection of both green and purple path as below and it returns false
.
public override func draw(_ rect: CGRect) {
let wallPath = UIBezierPath()
wallPath.move(to: CGPoint(x: 50.0, y: 50.0))
wallPath.addLine(to: CGPoint(x: 50.0, y: 400.0))
wallPath.addLine(to: CGPoint(x: 300.0, y: 400.0))
wallPath.addLine(to: CGPoint(x: 300.0, y: 50.0))
let wallLayer = CAShapeLayer()
wallLayer.path = wallPath.cgPath
wallLayer.lineWidth = 10
wallLayer.strokeColor = UIColor.green.cgColor
wallLayer.fillColor = nil
layer.addSublayer(wallLayer)
let eraserPath = UIBezierPath()
eraserPath.move(to: CGPoint(x: 40.0, y: 75.0))
eraserPath.addLine(to: CGPoint(x: 120.0, y: 75.0))
let eraserLayer = CAShapeLayer()
eraserLayer.path = eraserPath.cgPath
eraserLayer.lineWidth = 15
eraserLayer.strokeColor = UIColor.purple.cgColor
layer.addSublayer(eraserLayer)
if wallPath.cgPath.intersects(eraserPath.cgPath) {
print("Overlapping")
} else {
print("Not overlapping")
}
}
To make it more clear about the requirement, when user draws in eraser mode, based on the draw points, I have to identify which green line falls completely in the purple stroke's coverage and the identified green line must be taken for further processing. Multiple green lines could be selected at the same time.
CGPath
has an .lineIntersection(_:using:)
method that can come in really handy here...
Let's start with a single line path:
We create an "eraser" path - using strokingWithWidth
instead of .lineWidth
so we get a "filled" outline path - and cross the path:
When we call:
let iPth = segPth.lineIntersection(eraserPth)
That returns a new path, consisting of .move(to: pt1)
and .addLine(to: pt2)
- if we draw that iPth
in cyan we see:
We can then easily compare the resulting path to the original path and determine that the original is not completely encompassed.
If we continue defining our eraser path:
and call segPth.lineIntersection(eraserPth)
again, it will return a path of move to
+ line to
+ move to
+ line to
:
and again, we can easily determine that it is not the same path as the original line segment.
If we keep adding points to the eraser path until we get this:
segPth.lineIntersection(eraserPth)
will now return a path:
That matches the original.
So... a couple notes first...
Define your "walls" path as a series of "line segments" rather than a single path:
struct LineSegment: Equatable {
var pt1: CGPoint = .zero
var pt2: CGPoint = .zero
}
That makes it easy to:
.lineIntersection()
Next, because points
use floating-point values, and UIKit likes whole numbers, we can save ourselves some headaches by rounding
everything.
For example, if we have a line from 20, 10
to 80, 10
, and we have an eraser path that encompasses the entire line path, we might get a returned path from .lineIntersection()
with points like 20.00001, 10.0
to 79.99997, 10.000001
.
Here is a complete example to play with...
The LineSegment
struct:
struct LineSegment: Equatable {
var pt1: CGPoint = .zero
var pt2: CGPoint = .zero
}
extension
to round the x,y values of a point:
extension CGPoint {
var rounded: CGPoint { .init(x: self.x.rounded(), y: self.y.rounded()) }
}
This extension
to return the points of a path (found here: How to get the CGPoint(s) of a CGPath):
extension CGPath {
/// this is a computed property, it will hold the points we want to extract
var points: [CGPoint] {
/// this is a local transient container where we will store our CGPoints
var arrPoints: [CGPoint] = []
// applyWithBlock lets us examine each element of the CGPath, and decide what to do
self.applyWithBlock { element in
switch element.pointee.type
{
case .moveToPoint, .addLineToPoint:
arrPoints.append(element.pointee.points.pointee)
case .addQuadCurveToPoint:
arrPoints.append(element.pointee.points.pointee)
arrPoints.append(element.pointee.points.advanced(by: 1).pointee)
case .addCurveToPoint:
arrPoints.append(element.pointee.points.pointee)
arrPoints.append(element.pointee.points.advanced(by: 1).pointee)
arrPoints.append(element.pointee.points.advanced(by: 2).pointee)
default:
break
}
}
// We are now done collecting our CGPoints and so we can return the result
return arrPoints
}
}
A UIView
subclass, with shape layers, path logic, and touch handling:
class StrokedView: UIView {
let sampleWallSegments: [LineSegment] = [
// an "outline" box
.init(pt1: .init(x: 40.0, y: 40.0), pt2: .init(x: 260.0, y: 40.0)),
.init(pt1: .init(x: 260.0, y: 40.0), pt2: .init(x: 260.0, y: 120.0)),
.init(pt1: .init(x: 260.0, y: 120.0), pt2: .init(x: 120.0, y: 120.0)),
.init(pt1: .init(x: 120.0, y: 120.0), pt2: .init(x: 120.0, y: 80.0)),
.init(pt1: .init(x: 120.0, y: 80.0), pt2: .init(x: 60.0, y: 80.0)),
.init(pt1: .init(x: 60.0, y: 80.0), pt2: .init(x: 60.0, y: 120.0)),
.init(pt1: .init(x: 60.0, y: 120.0), pt2: .init(x: 40.0, y: 120.0)),
.init(pt1: .init(x: 40.0, y: 120.0), pt2: .init(x: 40.0, y: 40.0)),
// couple criss-crossed lines
.init(pt1: .init(x: 180.0, y: 50.0), pt2: .init(x: 220.0, y: 70.0)),
.init(pt1: .init(x: 220.0, y: 50.0), pt2: .init(x: 180.0, y: 70.0)),
// some short vertical lines
.init(pt1: .init(x: 150.0, y: 90.0), pt2: .init(x: 150.0, y: 110.0)),
.init(pt1: .init(x: 180.0, y: 90.0), pt2: .init(x: 180.0, y: 100.0)),
.init(pt1: .init(x: 210.0, y: 90.0), pt2: .init(x: 210.0, y: 100.0)),
.init(pt1: .init(x: 240.0, y: 90.0), pt2: .init(x: 240.0, y: 110.0)),
]
// this holds the "wall" line segments
// will initially be set to asmpleWallSegments
// segments may be removed
var wallSegments: [LineSegment] = []
// holds the points as the user touches/drags
var eraserPoints: [CGPoint] = []
// will hold the indexes of the wallSegments that are completely
// encompassed by the eraser line
var encompassedSegments: [Int] = []
let wallLayer = CAShapeLayer()
let eraserLayer = CAShapeLayer()
let highlightLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
private func commonInit() {
// this layer will hold our line segments path
wallLayer.fillColor = UIColor.clear.cgColor
wallLayer.strokeColor = UIColor.systemGreen.cgColor
wallLayer.lineWidth = 1.0
// instead of using a path with a line-width of 10,
// eraser layer will be filled with no line/stroke,
// because we will set its .path to the "stroked" path
eraserLayer.fillColor = UIColor.systemBlue.withAlphaComponent(0.25).cgColor
eraserLayer.lineJoin = .round
eraserLayer.lineCap = .round
// this layer will "highlight" the fully encompassed segments
highlightLayer.fillColor = UIColor.clear.cgColor
highlightLayer.strokeColor = UIColor.red.withAlphaComponent(0.9).cgColor
highlightLayer.lineWidth = 2.0
[wallLayer, eraserLayer, highlightLayer].forEach { lay in
layer.addSublayer(lay)
}
reset()
}
func reset() {
eraserPoints = []
wallSegments = sampleWallSegments
setNeedsLayout()
}
func removeSegments() {
encompassedSegments.reversed().forEach { i in
wallSegments.remove(at: i)
}
eraserPoints = []
setNeedsLayout()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let pt = t.location(in: self).rounded
// append a new point
eraserPoints.append(pt)
setNeedsLayout()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let pt = t.location(in: self).rounded
// always append a new point, or
eraserPoints.append(pt)
// if we want to "rubber-band" the highlight, use this instead
//points[points.count - 1] = pt
setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 16.0, *) {
// clear the layer paths
[wallLayer, eraserLayer, highlightLayer].forEach { lay in
lay.path = nil
}
// clear the encompassed segments array
encompassedSegments = []
// create the "walls" path
let wallPth = CGMutablePath()
wallSegments.forEach { seg in
wallPth.move(to: seg.pt1)
wallPth.addLine(to: seg.pt2)
}
// set "walls" layer path
wallLayer.path = wallPth
// return if we have no eraser points yet
guard eraserPoints.count > 0 else { return }
// create eraser path
let eraserPth = CGMutablePath()
// create highlight path (will "highlight" the encompassed segments in red)
let highlightPath = CGMutablePath()
// add lines to the eraser path
eraserPth.move(to: eraserPoints[0])
if eraserPoints.count == 1 {
eraserPth.addLine(to: .init(x: eraserPoints[0].x + 1.0, y: eraserPoints[0].y))
}
for i in 1..<eraserPoints.count {
eraserPth.addLine(to: eraserPoints[i])
}
// get a "stroked" path from the eraser path
let strokedPth = eraserPth.copy(strokingWithWidth: 10.0, lineCap: .round, lineJoin: .round, miterLimit: 1.0)
// normalize it
let normedPth = strokedPth.normalized()
// set eraser layer path
eraserLayer.path = normedPth
// for each wall segment
for (i, thisSeg) in wallSegments.enumerated() {
// create a new two-point path for the segment
let segPth = CGMutablePath()
segPth.move(to: thisSeg.pt1)
segPth.addLine(to: thisSeg.pt2)
// get the intersection with the normalized path
let iPth = segPth.lineIntersection(normedPth)
// get the points from that intersecting path
let iPoints = iPth.points
// if we have Zero or any number of points other than Two,
// the segment will not be completely encompassed
if iPoints.count == 2 {
// create a LineSegment with rounded points from the intersecting path
let thatSeg = LineSegment(pt1: iPoints[0].rounded, pt2: iPoints[1].rounded)
// if that segment is equal to this segment, add a line to the hightlight path
// and append the index of this segment to encompassed array (so we can remove them on demand)
if thatSeg == thisSeg {
highlightPath.move(to: thisSeg.pt1)
highlightPath.addLine(to: thisSeg.pt2)
encompassedSegments.append(i)
}
}
// set the highlight layer path
highlightLayer.path = highlightPath
}
}
}
}
and a simple view controller to show it in use:
class StrokedVC: UIViewController {
let testView = StrokedView()
override func viewDidLoad() {
super.viewDidLoad()
var cfg = UIButton.Configuration.filled()
cfg.title = "Remove Highlighted"
let btnA = UIButton(configuration: cfg, primaryAction: UIAction() { _ in
self.testView.removeSegments()
})
cfg = UIButton.Configuration.filled()
cfg.title = "Reset"
let btnB = UIButton(configuration: cfg, primaryAction: UIAction() { _ in
self.testView.reset()
})
[btnA, btnB, testView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
btnA.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
btnA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
btnA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
testView.topAnchor.constraint(equalTo: btnA.bottomAnchor, constant: 20.0),
testView.widthAnchor.constraint(equalToConstant: 300.0),
testView.heightAnchor.constraint(equalToConstant: 160.0),
testView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
btnB.topAnchor.constraint(equalTo: testView.bottomAnchor, constant: 20.0),
btnB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
btnB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
testView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
}
}
Here's what it looks like when running: