iOS – 获取围绕 CGPoints 的 CGRect

huangapple go评论56阅读模式
英文:

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?

iOS – 获取围绕 CGPoints 的 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).

iOS – 获取围绕 CGPoints 的 CGRect

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 CGPoints and I have no CGRects. 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")
    }
}

iOS – 获取围绕 CGPoints 的 CGRect

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:)方法,在这里非常有用...

让我们从一个单线路径开始:

iOS – 获取围绕 CGPoints 的 CGRect

我们创建一个“橡皮擦”路径 - 使用strokingWithWidth而不是.lineWidth,这样我们就得到了一个“填充”的轮廓路径 - 并穿越路径:

iOS – 获取围绕 CGPoints 的 CGRect

当我们调用:

let iPth = segPth.lineIntersection(eraserPth)

这将返回一个新的路径,包括.move(to: pt1).addLine(to: pt2) - 如果我们以青色绘制iPth,我们会看到:

iOS – 获取围绕 CGPoints 的 CGRect

然后,我们可以轻松比较生成的路径与原始路径,从而确定原始路径未完全包围。

如果我们继续定义我们的橡皮擦路径:

iOS – 获取围绕 CGPoints 的 CGRect

并再次调用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, 1080, 10,并且我们有一个橡皮擦路径,包含整个线段路径,那么我们可能会从.lineIntersection()得到像20.00001, 10.079.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&#39;s start with a single line path:

[![enter image description here][2]][2]

We create an &quot;eraser&quot; path - using `strokingWithWidth` instead of `.lineWidth` so we get a &quot;filled&quot; 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 &quot;walls&quot; path as a series of &quot;line segments&quot; 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 &quot;outline&quot; 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 &quot;wall&quot; 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 &quot;stroked&quot; path
    		eraserLayer.fillColor = UIColor.systemBlue.withAlphaComponent(0.25).cgColor
    		eraserLayer.lineJoin = .round
    		eraserLayer.lineCap = .round
    
    		// this layer will &quot;highlight&quot; 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&lt;UITouch&gt;, 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&lt;UITouch&gt;, 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 &quot;rubber-band&quot; 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 &quot;walls&quot; path
    			let wallPth = CGMutablePath()
    			
    			wallSegments.forEach { seg in
    				wallPth.move(to: seg.pt1)
    				wallPth.addLine(to: seg.pt2)
    			}
    			
    			// set &quot;walls&quot; layer path
    			wallLayer.path = wallPth
    			
    			// return if we have no eraser points yet
    			guard eraserPoints.count &gt; 0 else { return }
    
    			// create eraser path
    			let eraserPth = CGMutablePath()
    			
    			// create highlight path (will &quot;highlight&quot; 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..&lt;eraserPoints.count {
    				eraserPth.addLine(to: eraserPoints[i])
    			}
    			
    			// get a &quot;stroked&quot; 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 = &quot;Remove Highlighted&quot;
    		let btnA = UIButton(configuration: cfg, primaryAction: UIAction() { _ in
    			self.testView.removeSegments()
    		})
    		
    		cfg = UIButton.Configuration.filled()
    		
    		cfg.title = &quot;Reset&quot;
    		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&#39;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 CGPaths 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) -&gt; 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 &quot;pencil&quot; 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) -&gt; 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 &quot;piece&quot; 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.

huangapple
  • 本文由 发表于 2023年6月19日 14:34:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/76504143.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定