如何擦除SKShapeNode线条的部分线段?含非首尾段分割需求
解决SpriteKit中擦除指定线段并分割线条的问题
看起来你已经搞定了基础的手绘逻辑,但在擦除特定线段时踩了「删除整条线」的坑——核心问题在于你没有准确定位到被触碰的单个线段,也没处理线条分割的逻辑。下面是一套完整的解决方案,包含线段检测、分割处理的具体代码:
第一步:定义数据结构关联线条点与SKShapeNode
首先,我们需要把每条线条的点数组和对应的SKShapeNode绑定起来,方便后续修改和删除操作:
class DrawnLine { var points: [CGPoint] var shapeNode: SKShapeNode init(points: [CGPoint], shapeNode: SKShapeNode) { self.points = points self.shapeNode = shapeNode } } // 场景内存储所有绘制的线条 var drawnLines: [DrawnLine] = []
第二步:实现「触碰线段检测」逻辑
要擦除指定线段,首先得判断用户触碰的是哪条线条上的哪一段线段。这里用点到线段的最短距离来判断(比单纯点击顶点更友好,用户不需要精确点击):
// 查找被触碰的线段,返回对应的线条和线段索引 func findTouchedSegment(touchLocation: CGPoint) -> (targetLine: DrawnLine, segmentIndex: Int)? { let touchThreshold: CGFloat = 20.0 // 可调整的触碰灵敏度,值越大越容易选中 for line in drawnLines { guard line.points.count >= 2 else { continue } // 至少2个点才形成线条 for segmentIdx in 0..<line.points.count - 1 { let p1 = line.points[segmentIdx] let p2 = line.points[segmentIdx + 1] // 计算当前触碰点到该线段的距离 let distance = distanceFromPoint(touchLocation, toSegment: (p1, p2)) if distance < touchThreshold { return (line, segmentIdx) } } } return nil } // 辅助函数:计算点到线段的最短距离 private func distanceFromPoint(_ point: CGPoint, toSegment segment: (p1: CGPoint, p2: CGPoint)) -> CGFloat { let a = segment.p1 let b = segment.p2 let ap = point - a let ab = b - a let projectionRatio = ap.dot(ab) / ab.dot(ab) // 把投影值限制在0-1之间,确保计算的是线段上的最近点 let clampedRatio = max(0, min(1, projectionRatio)) let closestPoint = a + ab * clampedRatio return hypot(point.x - closestPoint.x, point.y - closestPoint.y) } // 给CGPoint扩展一些方便计算的方法 extension CGPoint { static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint { CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) } static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } static func *(lhs: CGPoint, scalar: CGFloat) -> CGPoint { CGPoint(x: lhs.x * scalar, y: lhs.y * scalar) } func dot(_ other: CGPoint) -> CGFloat { x * other.x + y * other.y } }
第三步:实现擦除与线条分割逻辑
在擦除触发的方法里(比如切换到擦除工具后的touchesBegan),调用上面的检测方法,然后根据线段的位置(首段、末段、中间段)做不同处理:
func handleEraseAction(touchLocation: CGPoint) { guard let (targetLine, segmentIdx) = findTouchedSegment(touchLocation: touchLocation) else { return // 没选中任何线段,直接返回 } let originalPoints = targetLine.points let originalNode = targetLine.shapeNode // 先移除原线条节点并从数组中删除 originalNode.removeFromParent() if let lineIndex = drawnLines.firstIndex(where: { $0 === targetLine }) { drawnLines.remove(at: lineIndex) } // 分三种情况处理线段擦除 switch segmentIdx { case 0: // 擦除的是第一条线段:保留从第二个点开始的剩余部分 let remainingPoints = Array(originalPoints.dropFirst()) if remainingPoints.count >= 2 { let newNode = createShapeNode(with: remainingPoints) addChild(newNode) drawnLines.append(DrawnLine(points: remainingPoints, shapeNode: newNode)) } case originalPoints.count - 2: // 擦除的是最后一条线段:保留到倒数第二个点的剩余部分 let remainingPoints = Array(originalPoints.dropLast()) if remainingPoints.count >= 2 { let newNode = createShapeNode(with: remainingPoints) addChild(newNode) drawnLines.append(DrawnLine(points: remainingPoints, shapeNode: newNode)) } default: // 擦除的是中间线段:分割成两条独立线条 let firstPartPoints = Array(originalPoints[0...segmentIdx]) let secondPartPoints = Array(originalPoints[segmentIdx + 1...originalPoints.count - 1]) // 只有当点数组长度>=2时才创建新线条(否则无法形成线段) if firstPartPoints.count >= 2 { let firstNode = createShapeNode(with: firstPartPoints) addChild(firstNode) drawnLines.append(DrawnLine(points: firstPartPoints, shapeNode: firstNode)) } if secondPartPoints.count >= 2 { let secondNode = createShapeNode(with: secondPartPoints) addChild(secondNode) drawnLines.append(DrawnLine(points: secondPartPoints, shapeNode: secondNode)) } } } // 复用你原来的SKShapeNode创建逻辑,确保样式一致 private func createShapeNode(with points: [CGPoint]) -> SKShapeNode { let path = UIBezierPath() path.move(to: points[0]) points.dropFirst().forEach { path.addLine(to: $0) } let shapeNode = SKShapeNode(path: path.cgPath) // 这里设置你原来的线条样式:颜色、宽度等 shapeNode.strokeColor = .blue shapeNode.lineWidth = 3 shapeNode.lineCap = .round return shapeNode }
第四步:整合到你的触摸逻辑中
比如当用户切换到擦除模式后,在touchesBegan中调用:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } let touchLocation = touch.location(in: self) if isEraseMode { // 假设你有一个标记当前是否为擦除模式的变量 handleEraseAction(touchLocation: touchLocation) } else { // 你的原有绘制逻辑 } }
关键注意点
- 线段检测的阈值:
touchThreshold可以根据你的线条宽度调整,一般设置为线条宽度的1.5-2倍,提升用户体验。 - 空线条过滤:分割后如果剩余点不足2个,就不要创建新节点,避免无效的空线条。
- 样式一致性:
createShapeNode方法要和你原来绘制时的样式保持一致,确保新线条和原线条看起来一样。
内容的提问来源于stack exchange,提问作者freeNinja
相关产品推荐
相关产品推荐

