英文:
iOS - Get CGRect around CGPoints
问题
我有一组以红色表示的CGPoint
数组。我必须连接所有连续的点,使用CGRect
如示意图所示,以形成一个CGRect
数组。考虑到所有的矩形都有固定的宽度,每个点都可以是矩形宽度形成的边的中点。我如何形成这个CGRect
数组?
在这里是我正在尝试做的事情。如我之前的问题中所描述,我正在尝试引入一个橡皮擦功能。
我有一组直线(绿色)。基本上,我将线的起点和终点包含在模型中。在橡皮擦模式下,用户被允许自由绘制,所有绘制点都由笔画连接(紫色)。
当任何绿色线完全被覆盖时,必须确定该线要被擦除。但是,我无法确定橡皮擦的绘制点是否完全覆盖了线。
如评论中所述,我尝试按照这里的答案进行操作。我只有CGPoint
,没有CGRect
。我检查绿色和紫色路径的交集如下,并返回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("重叠")
} else {
print("不重叠")
}
}
为了更清楚地阐明要求,当用户在橡皮擦模式下绘制时,根据绘制点,我必须确定哪些绿色线完全位于紫色笔画的覆盖范围内,并且已识别的绿色线必须进行进一步处理。可以同时选择多条绿色线。
英文:
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.
答案1
得分: 3
CGPath
具有一个.lineIntersection(_:using:)
方法,在这里非常有用...
让我们从一个单线路径开始:
我们创建一个“橡皮擦”路径 - 使用strokingWithWidth
而不是.lineWidth
,这样我们就得到了一个“填充”的轮廓路径 - 并穿越路径:
当我们调用:
let iPth = segPth.lineIntersection(eraserPth)
这将返回一个新的路径,包括.move(to: pt1)
和.addLine(to: pt2)
- 如果我们以青色绘制iPth
,我们会看到:
然后,我们可以轻松比较生成的路径与原始路径,从而确定原始路径未完全包围。
如果我们继续定义我们的橡皮擦路径:
并再次调用segPth.lineIntersection(eraserPth)
,它将返回一个路径,其中包括move to
+ line to
+ move to
+ line to
:
[![enter image description here][6]][6]
再次,我们可以轻松确定它与原始线段不同。
如果我们继续向橡皮擦路径添加点,直到得到这样的路径:
[![enter image description here][7]][7]
segPth.lineIntersection(eraserPth)
现在将返回一个路径:
[![enter image description here][8]][8]
与原始路径匹配。
所以...首先注意几点...
将你的“墙壁”路径定义为一系列“线段”而不是单一路径:
struct LineSegment: Equatable {
var pt1: CGPoint = .zero
var pt2: CGPoint = .zero
}
这使得可以轻松:
- 循环遍历这些线段
- 将它们与
.lineIntersection()
返回的路径进行比较 - 删除单独的墙壁段
- 创建不连续的墙壁
接下来,因为points
使用浮点值,而UIKit喜欢整数,我们可以通过四舍五入
一切来节省一些麻烦。
例如,如果我们有一条线段从20, 10
到80, 10
,并且我们有一个橡皮擦路径,包含整个线段路径,那么我们可能会从.lineIntersection()
得到像20.00001, 10.0
到79.99997, 10.000001
这样的点。
这是一个完整的示例供您参考...
LineSegment
结构体:
struct LineSegment: Equatable {
var pt1: CGPoint = .zero
var pt2: CGPoint = .zero
}
用于四舍五入点的扩展:
extension CGPoint {
var rounded: CGPoint { .init(x: self.x.rounded(), y: self.y.rounded()) }
}
这个扩展用于获取路径的点(在这里找到:https://stackoverflow.com/questions/12992462/how-to-get-the-cgpoints-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
}
}
一个UIView
子类,具有形状图层、路径逻辑和触摸处理:
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)),
<details>
<summary>英文:</summary>
`CGPath` has an [`.lineIntersection(_:using:)`][1] method that can come in really handy here...
Let's start with a single line path:
[![enter image description here][2]][2]
We create an "eraser" path - using `strokingWithWidth` instead of `.lineWidth` so we get a "filled" outline path - and cross the path:
[![enter image description here][3]][3]
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:
[![enter image description here][4]][4]
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:
[![enter image description here][5]][5]
and call `segPth.lineIntersection(eraserPth)` again, it will return a path of `move to` + `line to` + `move to` + `line to`:
[![enter image description here][6]][6]
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:
[![enter image description here][7]][7]
`segPth.lineIntersection(eraserPth)` will now return a path:
[![enter image description here][8]][8]
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:
- loop through the segments
- compare them to the returned path from `.lineIntersection()`
- **remove** individual wall-segments
- and... create non-contiguous walls
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: https://stackoverflow.com/questions/12992462/how-to-get-the-cgpoints-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:
[![enter image description here][9]][9]
[1]: https://developer.apple.com/documentation/coregraphics/cgpath/3994966-lineintersection
[2]: https://i.stack.imgur.com/YWXK3.png
[3]: https://i.stack.imgur.com/jX3C4.png
[4]: https://i.stack.imgur.com/BTXZA.png
[5]: https://i.stack.imgur.com/sEXrz.png
[6]: https://i.stack.imgur.com/YM9GS.png
[7]: https://i.stack.imgur.com/ZWp0w.png
[8]: https://i.stack.imgur.com/nIUDQ.png
[9]: https://i.stack.imgur.com/QgRec.gif
</details>
# 答案2
**得分**: 2
你有两个`CGPaths`,一个用于橡皮擦,另一个用于线条。你用特定的线宽描绘这些路径,从而创建了两个区域。你想要检查一个区域是否完全覆盖另一个。
首先,不要担心"完全" - 只需检查这两个区域是否在任何点交叉。你可以通过创建新的`CGPath`来表示使用特定线宽描边时创建的区域来实现这一点。有一个相关的API - [`copy(strokingWithWidth:lineCap:lineJoin:miterLimit:transform:)`][1]。
然后,你可以在这些路径上使用`intersect`。
以下是实现的示例:
```swift
func eraserIntersectsLine(p1: CGPoint, p2: CGPoint, lineWidth: CGFloat, eraserPath: CGPath, eraserWidth: CGFloat) -> Bool {
let eraserArea = eraserPath.copy(
// 你应该根据橡皮擦的工作方式更改线帽和线连接参数
strokingWithWidth: eraserWidth, lineCap: .butt, lineJoin: .miter, miterLimit: 10
)
let line = UIBezierPath()
line.move(to: p1)
line.addLine(to: p2)
let lineArea = line.cgPath.copy(
// 类似地,更改这些参数以匹配你的“铅笔”工具的工作方式
strokingWithWidth: lineWidth, lineCap: .butt, lineJoin: .miter, miterLimit: 10
)
return lineArea.intersects(eraserArea)
}
确定这些区域是否完全相交并不容易。我认为如果允许线条只通过交叉擦除,而不是完全覆盖,那将是更好的用户体验。
不过,如果你确实希望如此,这里是一个近似的方法。将线条分成小的“片段”,对于每个“片段”,检查该区域是否与橡皮擦区域相交。
以下是实现的示例:
func eraserCompletelyCoversLine(p1: CGPoint, p2: CGPoint, lineWidth: CGFloat, eraserPath: CGPath, eraserWidth: CGFloat) -> Bool {
let angle = atan2(p2.y - p1.y, p2.x - p1.x)
let distance = hypot(p2.x - p1.x, p2.y - p1.y)
// 将整条线分成0.1(任意选择的小长度)的片段
for start in stride(from: 0, to: distance, by: 0.1) {
// 检查每个“片段”是否与橡皮擦路径相交
let startX = p1.x + (start * cos(angle))
let startY = p1.y + (start * sin(angle))
let endX = p1.x + ((start + 0.1) * cos(angle))
let endY = p1.y + ((start + 0.1) * sin(angle))
guard eraserIntersectsLine(
p1: .init(x: startX, y: startY),
p2: .init(x: endX, y: endY),
lineWidth: lineWidth,
eraserPath: eraserPath,
eraserWidth: eraserWidth
) else { return false }
}
return true
}
请注意,我在这里每次都调用eraserIntersectsLine
,以便方便地创建橡皮擦区域的副本。如果需要的话,可以轻松进行优化,以便仅在需要时创建一个橡皮擦区域的副本。
为了更准确的近似,你还可以考虑线宽。将线宽也分成小块。
英文:
So you have two CGPaths
, one for the eraser and one for a line. You stroke those paths with a certain line width, which creates two areas. You want to check if one area completely covers another.
Let's first not worry about "completely" - just check if the two areas intersect at any point at all. You can do this by creating new CGPath
s that represent the area created when stroking with a certain line width. There is an API this - copy(strokingWithWidth:lineCap:lineJoin:miterLimit:transform:)
.
Then you can just use intersect
on those paths.
Here is an idea of how that would look:
func eraserIntersectsLine(p1: CGPoint, p2: CGPoint, lineWidth: CGFloat, eraserPath: CGPath, eraserWidth: CGFloat) -> Bool {
let eraserArea = eraserPath.copy(
// you should change the line cap and line join parameters to match how your eraser works
strokingWithWidth: eraserWidth, lineCap: .butt, lineJoin: .miter, miterLimit: 10
)
let line = UIBezierPath()
line.move(to: p1)
line.addLine(to: p2)
let lineArea = line.cgPath.copy(
// similarly, change these parameters to match how your "pencil" tool works
strokingWithWidth: lineWidth, lineCap: .butt, lineJoin: .miter, miterLimit: 10
)
return lineArea.intersects(eraserArea)
}
Determining if these areas completely intersect is not trivial at all. And I think it would be a better UX if you allow a line to be erased just by intersection, and not completely covering.
If you really want though, here is an approximation you could do. Divide the line into small "pieces", and for each of those "piece", check if the area of that intersects with the eraser area.
Here is an idea of how that would look:
func eraserCompletelyCoversLine(p1: CGPoint, p2: CGPoint, lineWidth: CGFloat, eraserPath: CGPath, eraserWidth: CGFloat) -> Bool {
let angle = atan2(p2.y - p1.y, p2.x - p1.x)
let distance = hypot(p2.x - p1.x, p2.y - p1.y)
// split the whole line into pieces of 0.1 (some arbitrarily chosen small length)
for start in stride(from: 0, to: distance, by: 0.1) {
// check if each "piece" intersects with the eraser path
let startX = p1.x + (start * cos(angle))
let startY = p1.y + (start * sin(angle))
let endX = p1.x + ((start + 0.1) * cos(angle))
let endY = p1.y + ((start + 0.1) * sin(angle))
guard eraserIntersectsLine(
p1: .init(x: startX, y: startY),
p2: .init(x: endX, y: endY),
lineWidth: lineWidth,
eraserPath: eraserPath,
eraserWidth: eraserWidth
) else { return false }
}
return true
}
Note that I'm calling eraserIntersectsLine
every time here for convenience, which creates the same path for the eraser area every time. It should be trivial to optimise this so that it only creates one copy of the eraser area if you need to.
For a more accurate approximation, you could consider the width of the line too. Split the line width into small pieces too.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论