You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何擦除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 {
        // 你的原有绘制逻辑
    }
}

关键注意点

  1. 线段检测的阈值:touchThreshold可以根据你的线条宽度调整,一般设置为线条宽度的1.5-2倍,提升用户体验。
  2. 空线条过滤:分割后如果剩余点不足2个,就不要创建新节点,避免无效的空线条。
  3. 样式一致性:createShapeNode方法要和你原来绘制时的样式保持一致,确保新线条和原线条看起来一样。

内容的提问来源于stack exchange,提问作者freeNinja

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 09:11:47