英文:
iOS - Detecting path is below another path
问题
以下是翻译好的部分:
这是我的要求。基本上,它是一个橡皮擦的概念。我有一个具有 n
条边的多边形。所有线的 (x,y)
坐标都存储在一个模型中。用户可以在多边形上绘制,如果线完全被自由绘制覆盖,那么特定的线条必须突出显示,随后可以被擦除。
上下文绘图用于绘制多边形和橡皮擦。根据模型中的线数据绘制多边形。当用户处于橡皮擦模式时,用浅蓝色的较大线宽描绘。当绘制完全覆盖多边形中的任何线时,该线可能会以深蓝色突出显示,表示选定。
问题是,我无法确定线条是否完全被橡皮擦覆盖。由于我只有线的起始点和结束点,很难确定。有什么可靠的方法来识别用户在橡皮擦模式下绘制的线条?
编辑
试图回答评论中的问题
对于第一个问题,必须识别并删除底部的直线,因为它完全被覆盖。对于第二种情况的回答,将不会选择/识别任何红线。对于第三种情况的回答,所有完全位于蓝色笔触覆盖范围下的线条必须被选中 - 以绿色突出显示。
英文:
Here is my requirement. Basically it is an eraser concept. I have polygon with n
sides. (x,y)
of all lines are stored in a model. To erase any part in the polygon, user can draw on the polygon over the lines and if the line comes completely under the coverage of the free hand drawing, the particular lines must be highlighted and later it could be erased.
Context drawing is used to draw both polygon and eraser. Based on the line data in model, the polygon is drawn. When user draws in eraser mode, it is stroked with larger line width in light blue colour. When the drawing has a complete coverage over any line in polygon, the line could be highlighted in dark blue indicating selection.
Problem is, I couldn't able to determine if the line has complete coverage over the eraser. Since I have only start and end point of a line, it gets difficult to determine it. What could be reliable way to identify the lines as user draws in eraser mode?
EDIT
Trying to provide answer for the questions in comment
For the first question, the bottom straight line has to be identified and deleted since it is covered completely. To answer second situation, none of the red lines will be selected/identified. To answer third, all lines that comes completely under the coverage of blue stroke must be selected - highlighted in green.
答案1
得分: 2
以下是您要翻译的部分:
如果您的部署目标至少为iOS 16,那么您可以使用CGPath
的lineSubtracting
方法来执行“重型操作”。
Apple仍然没有在网站上提供这种方法的真正文档,但头文件描述如下:
返回一个新的路径,该路径不与给定路径的填充区域重叠。
参数:
- other: 要减去的路径。
- rule: 用于确定将哪些区域视为
other
内部的规则。
如果未指定,默认为CGPathFillRule.winding
规则。- 返回: 一个新的路径。
结果路径的线是不与
other
的填充区域重叠的该路径的线。被剪切的相交子路径创建开放的子路径。不与
other
相交的闭合子路径保持闭合。
所以,这里是策略:
- 创建一个用于直线段之一的
CGPath
。 - 创建用户的擦除手势的
CGPath
。 - 绘制擦除路径。
- 使用
lineSubtracting
方法对直线段路径进行操作,传递描绘的擦除路径,以获取仅包含直线段的部分,不会被橡皮擦覆盖。 - 如果
lineSubtracting
返回的路径为空,则直线已完全被擦除。 - 对每个直线段重复此过程。
让我们来试试。首先,我将编写一个模型类型,用于存储原始路径和擦除后保留的部分:
struct ErasablePath {
var original: Path
var remaining: Path
}
让我们添加一些额外的初始化方法,并添加一个方法,通过减去(描绘的)擦除路径来更新remaining
路径:
extension ErasablePath {
init(_ path: Path) {
self.init(original: path, remaining: path)
}
init(start: CGPoint, end: CGPoint) {
self.init(Path {
$0.move(to: start)
$0.addLine(to: end)
})
}
func erasing(_ eraserPath: CGPath) -> Self {
return Self(
original: original,
remaining: Path(remaining.cgPath.lineSubtracting(eraserPath))
)
}
}
我将使用以下函数将点数组转换为ErasablePath
数组:
func makeErasableLines(points: [CGPoint]) -> [ErasablePath] {
guard let first = points.first, let last = points.dropFirst().last else {
return []
}
return zip(points, points.dropFirst()).map {
ErasablePath(start: $0, end: $1)
} + [ErasablePath(start: last, end: first)]
}
这是玩具应用程序的完整数据模型:
struct Model {
var erasables: [ErasablePath] = makeErasableLines(points: [
CGPoint(x: 50, y: 100),
CGPoint(x: 300, y: 100),
CGPoint(x: 300, y: 400),
CGPoint(x: 175, y: 400),
CGPoint(x: 175, y: 250),
CGPoint(x: 50, y: 250),
])
var eraserPath: Path = Path()
var strokedEraserPath: Path = Path()
var isErasing: Bool = false
let lineWidth: CGFloat = 44
}
为了在用户与应用程序交互时更新模型,我需要对用户开始、移动和结束触摸的方法,并一种重置数据模型的方法:
extension Model {
mutating func startErasing(at point: CGPoint) {
eraserPath.move(to: point)
isErasing = true
}
mutating func continueErasing(to point: CGPoint) {
eraserPath.addLine(to: point)
strokedEraserPath = eraserPath.strokedPath(.init(
lineWidth: 44,
lineCap: .round,
lineJoin: .round
))
let cgEraserPath = strokedEraserPath.cgPath
erasables = erasables
.map { $0.erasing(cgEraserPath) }
}
mutating func endErasing() {
isErasing = false
}
mutating func reset() {
self = .init()
}
}
我们需要一个视图来绘制可擦除的路径和擦除路径。我将在视图中绘制每个原始可擦除路径的绿色部分,如果已完全擦除,则绘制虚线。我将在视图中绘制每个可擦除路径的剩余(未擦除)部分为红色。我将以半透明紫色绘制描绘的擦除路径。
struct DrawingView: View {
@Binding var model: Model
var body: some View {
Canvas { gc, size in
for erasable in model.erasables {
gc.stroke(
erasable.original,
with: .color(.green),
style: .init(
lineWidth: 2,
lineCap: .round,
lineJoin: .round,
miterLimit: 1,
dash: erasable.remaining.isEmpty ? [8, 8] : [],
dashPhase: 4
)
)
}
for erasable in model.erasables {
gc.stroke(
erasable.remaining,
with: .color(.red),
lineWidth: 2
)
}
gc.fill(
model.strokedEraserPath,
with: .color(.purple.opacity(0.5))
)
}
}
}
在我的ContentView
中,我将在绘制视图上添加一个DragGesture
,并显示一个重置按钮:
struct ContentView: View {
@Binding var model: Model
var body: some View {
VStack {
DrawingView(model: $model)
.gesture(eraseGesture)
Button("Reset") { model.reset() }
<details>
<summary>英文:</summary>
So, I guess you want something like this:
[![A shape made of straight lines. As I drag my finger over it, I draw 44-point wide stroke. Where the stroke overlaps the straight lines, the lines turn green. When a straight line is completely green, it becomes a dashed line.][1]][1]
I'm turning the completely “erased” lines dashed instead of removing them entirely.
**If your deployment target is at least iOS 16,** then you can use the [`lineSubtracting` method of `CGPath`](https://developer.apple.com/documentation/coregraphics/cgpath/3994967-linesubtracting) to do the “heavy lifting”.
Apple still hasn't provided real documentation of this method on the web site, but the header file describes it as follows:
> Returns a new path with a line from this path that does not overlap the filled region of the given path.
>
> - Parameters:
> - other: The path to subtract.
> - rule: The rule for determining which areas to treat as the interior of `other`.
> Defaults to the `CGPathFillRule.winding` rule if not specified.
> - Returns: A new path.
>
> The line of the resulting path is the line of this path that does not overlap the filled region of `other`.
>
> Intersected subpaths that are clipped create open subpaths. Closed subpaths that do not intersect `other` remain closed.
So, here's the strategy:
- Create a `CGPath` for one of your straight line segments.
- Create a `CGPath` of the user's erase gesture.
- Stroke the erase path.
- Use `lineSubtracting` on the straight line segment path, passing the stroked erase path, to get a path containing just the part of the straight line segment that is **not** covered by the eraser.
- If the path returned by `lineSubtracting` is empty, the straight line has been completely erased.
- Repeat for each straight line segment.
Let's try it out. First, I'll write a model type to store both an original path and the part of that path that remains after erasing:
struct ErasablePath {
var original: Path
var remaining: Path
}
Let's add a couple of extra initializers, and a method that updates the `remaining` path by subtracting a (stroked) eraser path:
extension ErasablePath {
init(_ path: Path) {
self.init(original: path, remaining: path)
}
init(start: CGPoint, end: CGPoint) {
self.init(Path {
$0.move(to: start)
$0.addLine(to: end)
})
}
func erasing(_ eraserPath: CGPath) -> Self {
return Self(
original: original,
remaining: Path(remaining.cgPath.lineSubtracting(eraserPath))
)
}
}
I'll use the following function to turn an array of points into an array of `ErasablePath`s:
func makeErasableLines(points: [CGPoint]) -> [ErasablePath] {
guard let first = points.first, let last = points.dropFirst().last else {
return []
}
return zip(points, points.dropFirst()).map {
ErasablePath(start: $0, end: $1)
} + [ErasablePath(start: last, end: first)]
}
Here is the complete data model for the toy app:
struct Model {
var erasables: [ErasablePath] = makeErasableLines(points: [
CGPoint(x: 50, y: 100),
CGPoint(x: 300, y: 100),
CGPoint(x: 300, y: 400),
CGPoint(x: 175, y: 400),
CGPoint(x: 175, y: 250),
CGPoint(x: 50, y: 250),
])
var eraserPath: Path = Path()
var strokedEraserPath: Path = Path()
var isErasing: Bool = false
let lineWidth: CGFloat = 44
}
To update the model as the user interacts with the app, I'll need methods to respond to the user starting, moving, and ending a touch, and a way to reset the data model:
extension Model {
mutating func startErasing(at point: CGPoint) {
eraserPath.move(to: point)
isErasing = true
}
mutating func continueErasing(to point: CGPoint) {
eraserPath.addLine(to: point)
strokedEraserPath = eraserPath.strokedPath(.init(
lineWidth: 44,
lineCap: .round,
lineJoin: .round
))
let cgEraserPath = strokedEraserPath.cgPath
erasables = erasables
.map { $0.erasing(cgEraserPath) }
}
mutating func endErasing() {
isErasing = false
}
mutating func reset() {
self = .init()
}
}
We need a view that draws the erasable paths and the eraser path. I'll draw each original erasable path in green, and draw it dashed if it's been fully erased. I'll draw the remaining (unerased) part of each erasable path in red. And I'll draw the stroked eraser path in semitransparent purple.
struct DrawingView: View {
@Binding var model: Model
var body: some View {
Canvas { gc, size in
for erasable in model.erasables {
gc.stroke(
erasable.original,
with: .color(.green),
style: .init(
lineWidth: 2,
lineCap: .round,
lineJoin: .round,
miterLimit: 1,
dash: erasable.remaining.isEmpty ? [8, 8] : [],
dashPhase: 4
)
)
}
for erasable in model.erasables {
gc.stroke(
erasable.remaining,
with: .color(.red),
lineWidth: 2
)
}
gc.fill(
model.strokedEraserPath,
with: .color(.purple.opacity(0.5))
)
}
}
}
In my `ContentView`, I'll add a `DragGesture` on the drawing view, and also show a reset button:
struct ContentView: View {
@Binding var model: Model
var body: some View {
VStack {
DrawingView(model: $model)
.gesture(eraseGesture)
Button("Reset") { model.reset() }
.padding()
}
}
var eraseGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { drag in
if model.isErasing {
model.continueErasing(to: drag.location)
} else {
model.startErasing(at: drag.location)
}
}
.onEnded { drag in
model.endErasing()
}
}
}
That's the code I used to generate the animation at the top of the answer. But I confess that it was a rigged demo. The `lineSubtracting` method is a little buggy and I was careful to avoid triggering the bug. Here's the bug:
[![Bug demo. See text for a description of the bug.][2]][2]
If an `ErasablePath` is a horizontal line segment, and the eraser path starts below that segment, then `lineSubtracting` removes the entire erasable path, even if the eraser path and the line segment have no overlap!
To work around the bug, I insert the following `init` method into `Model`:
struct Model {
... existing code ...
init() {
// lineSubtracting has a bug (still present in iOS 17.0 beta 1):
// If the receiver is a horizontal line, and the argument (this eraserPath) starts below that line, the entire receiver is removed, even if the argument doesn't intersect the receiver at all.
// To work around the bug, I put a zero-length segment at the beginning of eraserPath way above the actual touchable area.
startErasing(at: .init(x: -1000, y: -1000))
continueErasing(to: .init(x: -1000, y: -1000))
endErasing()
}
}
The eraser path always starts above the erasable paths, so it no longer triggers the bug:
[![demo showing the bug not triggering][3]][3]
[1]: https://i.stack.imgur.com/zZyjq.gif
[2]: https://i.stack.imgur.com/vRz36.gif
[3]: https://i.stack.imgur.com/8EVCF.gif
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论