Swiftgram/submodules/DrawingUI/Sources/DrawingGesture.swift
2022-12-17 15:35:00 +04:00

1280 lines
45 KiB
Swift

import Foundation
import UIKit
public typealias TouchEventIdentifier = String
public typealias PointIdentifier = String
public typealias EstimationUpdateIndex = NSNumber
class Touch: Equatable, Hashable {
public let touchIdentifier: UITouchIdentifier
lazy public var pointIdentifier: PointIdentifier = {
if let estimationUpdateIndex = estimationUpdateIndex {
return touchIdentifier + ":\(estimationUpdateIndex)"
} else {
return touchIdentifier + ":" + identifier
}
}()
public let identifier: String
public let timestamp: TimeInterval
public let type: UITouch.TouchType
public let phase: UITouch.Phase
public let force: CGFloat
public let maximumPossibleForce: CGFloat
public let altitudeAngle: CGFloat
public let azimuthUnitVector: CGVector
public let azimuth: CGFloat
public let location: CGPoint
public let estimationUpdateIndex: EstimationUpdateIndex?
public let estimatedProperties: UITouch.Properties
public let estimatedPropertiesExpectingUpdates: UITouch.Properties
public let isUpdate: Bool
public let isPrediction: Bool
public let view: UIView?
public var expectsLocationUpdate: Bool {
return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.location)
}
public var expectsForceUpdate: Bool {
return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.force)
}
public var expectsAzimuthUpdate: Bool {
return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.azimuth)
}
public var expectsUpdate: Bool {
return expectsForceUpdate || expectsAzimuthUpdate || expectsLocationUpdate
}
public convenience init(
coalescedTouch: UITouch,
touch: UITouch,
in view: UIView,
isUpdate: Bool,
isPrediction: Bool,
phase: UITouch.Phase? = nil,
transform: CGAffineTransform = .identity
) {
let originalLocation = coalescedTouch.location(in: view)
let location = !transform.isIdentity ? originalLocation.applying(transform) : originalLocation
self.init(
identifier: UUID.init().uuidString,
touchIdentifier: touch.identifer,
timestamp: coalescedTouch.timestamp,
type: coalescedTouch.type,
phase: phase ?? coalescedTouch.phase,
force: coalescedTouch.force,
maximumPossibleForce: coalescedTouch.maximumPossibleForce,
altitudeAngle: coalescedTouch.altitudeAngle,
azimuthUnitVector: coalescedTouch.azimuthUnitVector(in: view),
azimuth: coalescedTouch.azimuthAngle(in: view),
location: location,
estimationUpdateIndex: coalescedTouch.estimationUpdateIndex,
estimatedProperties: coalescedTouch.estimatedProperties,
estimatedPropertiesExpectingUpdates: coalescedTouch.estimatedPropertiesExpectingUpdates,
isUpdate: isUpdate,
isPrediction: isPrediction,
in: view
)
}
public init(
identifier: TouchEventIdentifier,
touchIdentifier: UITouchIdentifier,
timestamp: TimeInterval,
type: UITouch.TouchType,
phase: UITouch.Phase,
force: CGFloat,
maximumPossibleForce: CGFloat,
altitudeAngle: CGFloat,
azimuthUnitVector: CGVector,
azimuth: CGFloat,
location: CGPoint,
estimationUpdateIndex: EstimationUpdateIndex?,
estimatedProperties: UITouch.Properties,
estimatedPropertiesExpectingUpdates: UITouch.Properties,
isUpdate: Bool,
isPrediction: Bool,
in view: UIView?
) {
self.identifier = identifier
self.touchIdentifier = touchIdentifier
self.timestamp = timestamp
self.type = type
self.phase = phase
self.force = force
self.maximumPossibleForce = maximumPossibleForce
self.altitudeAngle = altitudeAngle
self.azimuthUnitVector = azimuthUnitVector
self.azimuth = azimuth
self.location = location
self.estimationUpdateIndex = estimationUpdateIndex
self.estimatedProperties = estimatedProperties
self.estimatedPropertiesExpectingUpdates = estimatedPropertiesExpectingUpdates
self.isUpdate = isUpdate
self.isPrediction = isPrediction
self.view = view
}
public static func == (lhs: Touch, rhs: Touch) -> Bool {
return lhs.identifier == rhs.identifier
}
public func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
}
class DrawingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
var shouldBegin: (CGPoint) -> Bool = { _ in return true }
var onTouches: ([Touch]) -> Void = { _ in }
var transform: CGAffineTransform = .identity
var usePredictedTouches = false
private var currentTouches = Set<UITouch>()
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.delegate = self
self.cancelsTouchesInView = false
self.delaysTouchesBegan = false
self.delaysTouchesEnded = false
self.allowedTouchTypes = [
NSNumber(value: UITouch.TouchType.direct.rawValue),
NSNumber(value: UITouch.TouchType.stylus.rawValue)
]
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.view)
if self.shouldBegin(location) {
return true
} else {
return false
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIPinchGestureRecognizer {
return true
}
return true
}
override func touchesEstimatedPropertiesUpdated(_ touches: Set<UITouch>) {
self.process(touches: touches, with: nil, isUpdate: true)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let location = touches.first?.location(in: self.view), touches.count == 1 && self.shouldBegin(location) {
super.touchesBegan(touches, with: event)
self.process(touches: touches, with: event)
self.state = .began
} else {
self.state = .cancelled
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
if touches.count > 1 {
self.state = .cancelled
} else {
super.touchesMoved(touches, with: event)
self.process(touches: touches, with: event)
self.state = .changed
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.process(touches: touches, with: event)
self.state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.state = .cancelled
}
func process(touches: Set<UITouch>, with event: UIEvent?, isUpdate: Bool = false) {
guard let view = self.view else {
return
}
var allTouches: [Touch] = []
for touch in touches {
var coalesced = event?.coalescedTouches(for: touch) ?? [touch]
if "".isEmpty || coalesced.isEmpty {
coalesced = [touch]
}
for coalescedTouch in coalesced {
allTouches.append(
Touch(
coalescedTouch: coalescedTouch,
touch: touch,
in: view,
isUpdate: isUpdate,
isPrediction: false,
transform: self.transform
)
)
}
if self.usePredictedTouches {
let predicted = event?.predictedTouches(for: touch) ?? []
for predictedTouch in predicted {
allTouches.append(
Touch(
coalescedTouch: predictedTouch,
touch: touch,
in: view,
isUpdate: isUpdate,
isPrediction: true,
transform: self.transform
)
)
}
}
}
self.onTouches(allTouches)
}
}
class DrawingGesturePipeline {
enum Mode {
case location
case smoothCurve
case polyline
}
enum DrawingGestureState {
case began
case changed
case ended
case cancelled
}
enum DrawingResult {
case location(Polyline.Point)
case smoothCurve(BezierPath)
case polyline(Polyline)
}
private var pendingTouches: [Touch] = []
var onDrawing: (DrawingGestureState, DrawingResult) -> Void = { _, _ in }
var gestureRecognizer: DrawingGestureRecognizer?
var transform: CGAffineTransform = .identity {
didSet {
self.gestureRecognizer?.transform = transform
}
}
var mode: Mode = .location {
didSet {
if [.location, .polyline].contains(self.mode) {
self.gestureRecognizer?.usePredictedTouches = false
} else {
self.gestureRecognizer?.usePredictedTouches = true
}
}
}
init(view: DrawingView) {
let gestureRecognizer = DrawingGestureRecognizer(target: self, action: #selector(self.handleGesture(_:)))
gestureRecognizer.onTouches = { [weak self] touches in
self?.pendingTouches.append(contentsOf: touches)
}
self.gestureRecognizer = gestureRecognizer
view.addGestureRecognizer(gestureRecognizer)
}
@objc private func handleGesture(_ gestureRecognizer: DrawingGestureRecognizer) {
let state: DrawingGestureState
switch gestureRecognizer.state {
case .began:
state = .began
case .changed:
state = .changed
case .ended:
state = .ended
case .cancelled:
state = .cancelled
case .failed:
state = .cancelled
case .possible:
state = .cancelled
@unknown default:
state = .cancelled
}
let touchDeltas = self.processTouchEvents(self.pendingTouches)
let polylineDeltas = self.processTouchPaths(inputDeltas: touchDeltas)
let simplifiedPolylineDeltas = self.simplifyPolylines(inputDeltas: polylineDeltas)
switch self.mode {
case .location:
if let touchPath = self.touchPaths.last, let point = touchPath.points.last {
self.onDrawing(state, .location(Polyline.Point(touchPoint: point)))
}
case .smoothCurve:
if let path = self.processPolylines(inputDeltas: simplifiedPolylineDeltas) {
self.onDrawing(state, .smoothCurve(path))
}
case .polyline:
if let polyline = self.simplifiedPolylines.last {
self.onDrawing(state, .polyline(polyline))
}
}
self.pendingTouches.removeAll()
}
enum TouchPathDelta: Equatable {
case addedTouchPath(index: Int)
case updatedTouchPath(index: Int, updatedIndexes: MinMaxIndex)
case completedTouchPath(index: Int)
}
private var touchPaths: [TouchPath] = []
private var touchToIndex: [UITouchIdentifier: Int] = [:]
private func processTouchEvents(_ touches: [Touch]) -> [TouchPathDelta] {
var deltas: [TouchPathDelta] = []
var processedTouchIdentifiers: [UITouchIdentifier] = []
let updatedEventsPerTouch = touches.reduce(into: [String: [Touch]](), { (result, event) in
if result[event.touchIdentifier] != nil {
result[event.touchIdentifier]?.append(event)
} else {
result[event.touchIdentifier] = [event]
}
})
for touchToProcess in touches {
let touchIdentifier = touchToProcess.touchIdentifier
guard !processedTouchIdentifiers.contains(touchIdentifier), let events = updatedEventsPerTouch[touchIdentifier] else {
continue
}
processedTouchIdentifiers.append(touchIdentifier)
if let index = self.touchToIndex[touchIdentifier] {
let path = self.touchPaths[index]
let updatedIndexes = path.add(touchEvents: events)
deltas.append(.updatedTouchPath(index: index, updatedIndexes: updatedIndexes))
if path.isComplete {
deltas.append(.completedTouchPath(index: index))
}
} else if let touchIdentifier = events.first?.touchIdentifier, let path = TouchPath(touchEvents: events) {
let index = self.touchPaths.count
self.touchToIndex[touchIdentifier] = index
self.touchPaths.append(path)
deltas.append(.addedTouchPath(index: index))
if path.isComplete {
deltas.append(.completedTouchPath(index: index))
}
}
}
return deltas
}
enum PolylineDelta: Equatable {
case addedPolyline(index: Int)
case updatedPolyline(index: Int, updatedIndexes: MinMaxIndex)
case completedPolyline(index: Int)
}
private var indexToIndex: [Int: Int] = [:]
private var polylines: [Polyline] = []
func processTouchPaths(inputDeltas: [TouchPathDelta]) -> [PolylineDelta] {
var deltas: [PolylineDelta] = []
for delta in inputDeltas {
switch delta {
case .addedTouchPath(let pathIndex):
let line = self.touchPaths[pathIndex]
let smoothStroke = Polyline(touchPath: line)
let index = polylines.count
indexToIndex[pathIndex] = index
polylines.append(smoothStroke)
deltas.append(.addedPolyline(index: index))
case .updatedTouchPath(let pathIndex, let indexSet):
let line = self.touchPaths[pathIndex]
if let index = indexToIndex[pathIndex] {
let updates = polylines[index].update(with: line, indexSet: indexSet)
deltas.append(.updatedPolyline(index: index, updatedIndexes: updates))
}
case .completedTouchPath(let pointCollectionIndex):
if let index = indexToIndex[pointCollectionIndex] {
deltas.append(.completedPolyline(index: index))
}
}
}
return deltas
}
var simplifiedPolylines: [Polyline] = []
func simplifyPolylines(inputDeltas: [PolylineDelta]) -> [PolylineDelta] {
var outDeltas: [PolylineDelta] = []
for delta in inputDeltas {
switch delta {
case .addedPolyline(let strokeIndex):
assert(strokeIndex == self.simplifiedPolylines.count)
let line = self.polylines[strokeIndex]
self.simplifiedPolylines.append(line)
let indexes = MinMaxIndex(0..<line.points.count)
let _ = smoothStroke(stroke: &self.simplifiedPolylines[strokeIndex], at: indexes, input: line)
outDeltas.append(delta)
case .completedPolyline(let strokeIndex):
self.simplifiedPolylines[strokeIndex].isComplete = true
outDeltas.append(delta)
case .updatedPolyline(let strokeIndex, let indexes):
let line = self.polylines[strokeIndex]
let updatedIndexes = smoothStroke(stroke: &self.simplifiedPolylines[strokeIndex], at: indexes, input: line)
outDeltas.append(.updatedPolyline(index: strokeIndex, updatedIndexes: updatedIndexes))
}
}
return outDeltas
}
class Coeffs {
private let index: Int
private let windowSize: Int
private var cache: [CGFloat]?
init(index: Int, windowSize: Int) {
self.index = index
self.windowSize = windowSize
}
func weight(_ windowLoc: Int, _ order: Int, _ derivative: Int) -> CGFloat {
guard abs(windowLoc) <= windowSize else { fatalError("Invalid coefficient") }
if let cached = cache {
return cached[abs(windowLoc)]
}
var coeffs: [CGFloat] = []
for windowLoc in 0...windowSize {
coeffs.append(Self.calcWeight(index, windowLoc, windowSize, order, derivative))
}
cache = coeffs
return coeffs[abs(windowLoc)]
}
// MARK: - Coefficients
/// calculates the generalised factorial (a)(a-1)...(a-b+1)
private static func genFact(_ a: Int, _ b: Int) -> CGFloat {
var gf: CGFloat = 1.0
for jj in (a - b + 1) ..< (a + 1) {
gf *= CGFloat(jj)
}
return gf
}
/// Calculates the Gram Polynomial ( s = 0 ), or its s'th
/// derivative evaluated at i, order k, over 2m + 1 points
private static func gramPoly(_ index: Int, _ window: Int, _ order: Int, _ derivative: Int) -> CGFloat {
var gp_val: CGFloat
if order > 0 {
let g1 = gramPoly(index, window, order - 1, derivative)
let g2 = gramPoly(index, window, order - 1, derivative - 1)
let g3 = gramPoly(index, window, order - 2, derivative)
let i: CGFloat = CGFloat(index)
let m: CGFloat = CGFloat(window)
let k: CGFloat = CGFloat(order)
let s: CGFloat = CGFloat(derivative)
gp_val = (4.0 * k - 2.0) / (k * (2.0 * m - k + 1.0)) * (i * g1 + s * g2)
- ((k - 1.0) * (2.0 * m + k)) / (k * (2.0 * m - k + 1.0)) * g3
} else if order == 0 && derivative == 0 {
gp_val = 1.0
} else {
gp_val = 0.0
}
return gp_val
}
/// calculates the weight of the i'th data point for the t'th Least-square
/// point of the s'th derivative, over 2m + 1 points, order n
private static func calcWeight(_ index: Int, _ windowLoc: Int, _ windowSize: Int, _ order: Int, _ derivative: Int) -> CGFloat {
var sum: CGFloat = 0.0
for k in 0 ..< order + 1 {
sum += CGFloat(2 * k + 1) * CGFloat(genFact(2 * windowSize, k) / genFact(2 * windowSize + k + 1, k + 1))
* gramPoly(index, windowSize, k, 0) * gramPoly(windowLoc, windowSize, k, derivative)
}
return sum
}
}
private var window: Int = 2
private var strength: CGFloat = 1
var deriv: Int = 0
var order: Int = 3
var coeffs: [Coeffs] = []
private func smoothStroke(stroke: inout Polyline, at indexes: MinMaxIndex?, input: Polyline) -> MinMaxIndex {
if input.points.count > stroke.points.count {
stroke.points.append(contentsOf: input.points[stroke.points.count...])
} else if input.points.count < stroke.points.count {
stroke.points.removeSubrange(input.points.count...)
}
let outIndexes = { () -> MinMaxIndex in
if let indexes = indexes,
let minIndex = indexes.first,
let maxIndex = indexes.last {
var outIndexes = MinMaxIndex()
let start = max(0, minIndex - window)
let end = min(stroke.points.count - 1, maxIndex + window)
outIndexes.insert(integersIn: start...end)
return outIndexes
}
return MinMaxIndex(stroke.points.indices)
}()
for pIndex in outIndexes {
let minWin = min(min(window, pIndex), stroke.points.count - 1 - pIndex)
// copy over the point in question so that not only our location will be smoothed below,
// but also the azimuth/altitude/etc will be the same
stroke.points[pIndex] = input.points[pIndex]
while coeffs.count < minWin + 1 {
coeffs.append(Coeffs(index: 0, windowSize: coeffs.count))
}
if minWin > 1 {
var outPoint = CGPoint.zero
for windowPos in -minWin ... minWin {
let wght = coeffs[minWin].weight(windowPos, order, deriv)
outPoint.x += wght * input.points[pIndex + windowPos].location.x
outPoint.y += wght * input.points[pIndex + windowPos].location.y
}
let origPoint = stroke.points[pIndex].location
stroke.points[pIndex].location = origPoint * CGFloat(1 - strength) + outPoint * strength
}
}
return outIndexes
}
private var builders: [BezierBuilder] = []
private var bezierIndexToIndex: [Int: Int] = [:]
func processPolylines(inputDeltas: [PolylineDelta]) -> BezierPath? {
for delta in inputDeltas {
switch delta {
case .addedPolyline(let lineIndex):
assert(bezierIndexToIndex[lineIndex] == nil, "Cannot add existing line")
let line = self.simplifiedPolylines[lineIndex]
let builder = BezierBuilder(smoother: Smoother())
builder.update(with: line, at: MinMaxIndex(0 ..< line.points.count))
let builderIndex = builders.count
bezierIndexToIndex[lineIndex] = builderIndex
builders.append(builder)
case .updatedPolyline(let lineIndex, let updatedIndexes):
let line = self.simplifiedPolylines[lineIndex]
guard let builderIndex = bezierIndexToIndex[lineIndex] else {
continue
}
let builder = builders[builderIndex]
let _ = builder.update(with: line, at: updatedIndexes)
case .completedPolyline:
break
}
}
return builders.last?.path
}
}
private var TOUCH_IDENTIFIER: UInt8 = 0
public typealias UITouchIdentifier = String
extension UITouch {
var identifer: UITouchIdentifier {
if let identifier = objc_getAssociatedObject(self, &TOUCH_IDENTIFIER) as? String {
return identifier
} else {
let identifier = UUID().uuidString
objc_setAssociatedObject(self, &TOUCH_IDENTIFIER, identifier, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return identifier
}
}
}
public struct MinMaxIndex: Sequence, Equatable {
private var start: Int
private var end: Int
public static let null = MinMaxIndex()
public init() {
start = .max
end = .max
}
public init(_ indexes: ClosedRange<Int>) {
guard let first = indexes.first, let last = indexes.last else { self = .null; return }
start = first
end = last
}
public init(_ indexes: Range<Int>) {
guard let first = indexes.first, let last = indexes.last else { self = .null; return }
start = first
end = last
}
public init(_ integers: [Int]) {
guard !integers.isEmpty else { self = .null; return }
start = integers.min()!
end = integers.max()!
}
public init(_ integer: Int) {
start = integer
end = integer
}
public init(_ indexSet: IndexSet) {
guard !indexSet.isEmpty else { self = .null; return }
start = indexSet.min()!
end = indexSet.max()!
}
public var count: Int {
guard self != .null else { return 0 }
return end - start + 1
}
public var first: Int? {
guard self != .null else { return nil }
return start
}
public var last: Int? {
guard self != .null else { return nil }
return end
}
@inlinable @inline(__always)
public func asIndexSet() -> IndexSet {
guard let first = first, let last = last else { return IndexSet() }
return IndexSet(integersIn: first...last)
}
public mutating func insert(_ index: Int) {
if self == Self.null {
start = index
end = index
} else {
start = Swift.min(start, index)
end = Swift.max(end, index)
}
}
@inlinable @inline(__always)
public mutating func insert(integersIn indexes: ClosedRange<Int>) {
guard let first = indexes.first, let last = indexes.last else { return }
insert(first)
insert(last)
}
public mutating func remove(_ index: Int) {
if start == index {
start += 1
}
if end == index {
end -= 1
}
if start > end {
start = .max
end = .max
}
}
@inlinable @inline(__always)
public func contains(_ index: Int) -> Bool {
guard let first = first, let last = last else { return false }
return index >= first && index <= last
}
public func makeIterator() -> Iterator {
return Iterator(min: start, max: end)
}
public struct Iterator: IteratorProtocol {
public typealias Element = Int
var min: Int
let max: Int
init(min: Int, max: Int) {
self.min = min
self.max = max
}
public mutating func next() -> Int? {
if min == max, min == .max {
return nil
} else if min > max {
return nil
} else {
let ret = min
min += 1
return ret
}
}
}
}
extension Array {
@inline(__always) @inlinable
mutating func pop() -> Element? {
guard !isEmpty else { return nil }
return removeLast()
}
@inline(__always) @inlinable
mutating func dequeue() -> Element? {
guard !isEmpty else { return nil }
return removeFirst()
}
}
class TouchPath: Hashable {
class Point: Hashable {
public private(set) var events: [Touch]
public var event: Touch {
return events.last!
}
public var expectsUpdate: Bool {
return self.event.isPrediction || self.event.expectsUpdate
}
public var isPrediction: Bool {
return events.allSatisfy({ $0.isPrediction })
}
public init(event: Touch) {
events = [event]
events.reserveCapacity(10)
}
func add(event: Touch) {
events.append(event)
}
static func == (lhs: Point, rhs: Point) -> Bool {
return lhs.expectsUpdate == rhs.expectsUpdate && lhs.events == rhs.events
}
func hash(into hasher: inout Hasher) {
hasher.combine(events)
}
}
public private(set) var touchIdentifier: String
private var _points: [Point]?
public var points: [Point] {
if let _points = _points {
return _points
}
let ret = confirmedPoints + predictedPoints
_points = ret
return ret
}
public var bounds: CGRect {
return points.reduce(.null) { partialResult, point -> CGRect in
return CGRect(x: min(partialResult.origin.x, point.event.location.x),
y: min(partialResult.origin.y, point.event.location.y),
width: max(partialResult.origin.x, point.event.location.x),
height: max(partialResult.origin.y, point.event.location.y))
}
}
public var isComplete: Bool {
let phase = confirmedPoints.last?.event.phase
return (phase == .ended || phase == .cancelled) && predictedPoints.isEmpty
}
private var confirmedPoints: [Point] {
didSet {
_points = nil
}
}
private var predictedPoints: [Point] {
didSet {
_points = nil
}
}
private var consumable: [Point]
private var expectingUpdate: Set<String>
private var eventToPoint: [PointIdentifier: Point]
private var eventToIndex: [PointIdentifier: Int]
init?(touchEvents: [Touch]) {
guard !touchEvents.isEmpty else { return nil }
self.confirmedPoints = []
self.predictedPoints = []
self.consumable = []
self.eventToPoint = [:]
self.eventToIndex = [:]
self.expectingUpdate = Set()
self.touchIdentifier = touchEvents.first!.touchIdentifier
add(touchEvents: touchEvents)
}
@discardableResult
func add(touchEvents: [Touch]) -> MinMaxIndex {
var indexSet = MinMaxIndex()
let startingCount = points.count
for event in touchEvents {
assert(touchIdentifier == event.touchIdentifier)
if event.isPrediction {
if let prediction = consumable.dequeue() {
prediction.add(event: event)
predictedPoints.append(prediction)
let index = confirmedPoints.count + predictedPoints.count - 1
indexSet.insert(index)
} else {
let prediction = Point(event: event)
predictedPoints.append(prediction)
let index = confirmedPoints.count + predictedPoints.count - 1
indexSet.insert(index)
}
} else if eventToPoint[event.pointIdentifier] != nil, let index = eventToIndex[event.pointIdentifier] {
eventToPoint[event.pointIdentifier]?.add(event: event)
if !event.expectsUpdate {
self.expectingUpdate.remove(event.pointIdentifier)
}
indexSet.insert(index)
if event.phase == .ended || event.phase == .cancelled {
consumable.append(contentsOf: predictedPoints)
predictedPoints.removeAll()
}
} else if isComplete {
} else {
consumable.append(contentsOf: predictedPoints)
predictedPoints.removeAll()
if let point = consumable.dequeue() ?? predictedPoints.dequeue() {
if event.expectsUpdate {
self.expectingUpdate.insert(event.pointIdentifier)
}
point.add(event: event)
eventToPoint[event.pointIdentifier] = point
confirmedPoints.append(point)
let index = confirmedPoints.count - 1
eventToIndex[event.pointIdentifier] = index
indexSet.insert(index)
} else {
if event.expectsUpdate {
self.expectingUpdate.insert(event.pointIdentifier)
}
let point = Point(event: event)
eventToPoint[event.pointIdentifier] = point
confirmedPoints.append(point)
let index = confirmedPoints.count - 1
eventToIndex[event.pointIdentifier] = index
indexSet.insert(index)
}
}
}
for index in consumable.indices {
let possiblyRemovedIndex = confirmedPoints.count + predictedPoints.count + index
if possiblyRemovedIndex < startingCount {
indexSet.insert(possiblyRemovedIndex)
} else {
indexSet.remove(possiblyRemovedIndex)
}
}
return indexSet
}
public static func == (lhs: TouchPath, rhs: TouchPath) -> Bool {
return lhs.touchIdentifier == rhs.touchIdentifier && lhs.points == rhs.points
}
public func hash(into hasher: inout Hasher) {
hasher.combine(touchIdentifier)
}
}
struct Polyline {
struct Point: Equatable {
var location: CGPoint
var force: CGFloat
var altitudeAngle: CGFloat
var azimuth: CGFloat
var velocity: CGFloat = 0.0
let touchPoint: TouchPath.Point
var event: Touch {
return touchPoint.event
}
var expectsUpdate: Bool {
return touchPoint.expectsUpdate
}
var x: CGFloat {
return self.location.x
}
var y: CGFloat {
return self.location.y
}
init(
location: CGPoint,
force: CGFloat,
altitudeAngle: CGFloat,
azimuth: CGFloat,
velocity: CGFloat,
touchPoint: TouchPath.Point
) {
self.location = location
self.force = force
self.altitudeAngle = altitudeAngle
self.azimuth = azimuth
self.touchPoint = touchPoint
}
init(touchPoint: TouchPath.Point) {
self.location = touchPoint.event.location
self.force = touchPoint.event.force
self.altitudeAngle = touchPoint.event.altitudeAngle
self.azimuth = touchPoint.event.azimuth
self.touchPoint = touchPoint
}
func offsetBy(_ point: CGPoint) -> Polyline.Point {
return Point(
location: self.location.offsetBy(dx: point.x, dy: point.y),
force: self.force,
altitudeAngle: self.altitudeAngle,
azimuth: self.azimuth,
velocity: self.velocity,
touchPoint: self.touchPoint
)
}
func withLocation(_ point: CGPoint) -> Polyline.Point {
return Point(
location: point,
force: self.force,
altitudeAngle: self.altitudeAngle,
azimuth: self.azimuth,
velocity: self.velocity,
touchPoint: self.touchPoint
)
}
}
public internal(set) var isComplete: Bool
public let touchIdentifier: String
public var points: [Point]
public var bounds: CGRect {
return self.points.reduce(.null) { partialResult, point -> CGRect in
return CGRect(x: min(partialResult.origin.x, point.x),
y: min(partialResult.origin.y, point.y),
width: max(partialResult.size.width, point.x),
height: max(partialResult.size.height, point.y))
}
}
init(touchPath: TouchPath) {
isComplete = touchPath.isComplete
touchIdentifier = touchPath.touchIdentifier
var points: [Point] = []
var previousTouchPoint: TouchPath.Point?
for touchPoint in touchPath.points {
var point = Point(touchPoint: touchPoint)
if let previousTouchPoint = previousTouchPoint {
let distance = touchPoint.event.location.distance(to: previousTouchPoint.event.location)
let elapsed = max(0.0, touchPoint.event.timestamp - previousTouchPoint.event.timestamp)
let velocity = elapsed > 0.0 ? distance / elapsed : 0.0
point.velocity = velocity
}
points.append(point)
previousTouchPoint = touchPoint
}
self.points = points
}
init(points: [Point]) {
self.isComplete = true
self.touchIdentifier = points.first?.event.touchIdentifier ?? ""
self.points = points
}
mutating func update(with path: TouchPath, indexSet: MinMaxIndex) -> MinMaxIndex {
var indexesToRemove = MinMaxIndex()
for index in indexSet {
if index < path.points.count {
if index < points.count {
points[index].location = path.points[index].event.location
points[index].force = path.points[index].event.force
points[index].azimuth = path.points[index].event.azimuth
points[index].altitudeAngle = path.points[index].event.altitudeAngle
if index > 0 {
let previousTouchPoint = points[index - 1]
let distance = path.points[index].event.location.distance(to: previousTouchPoint.event.location)
let elapsed = max(0.0, path.points[index].event.timestamp - previousTouchPoint.event.timestamp)
let velocity = elapsed > 0.0 ? distance / elapsed : 0.0
points[index].velocity = velocity
}
} else if index == points.count {
points.append(Point(touchPoint: path.points[index]))
if index > 0 {
let previousTouchPoint = points[index - 1]
let distance = path.points[index].event.location.distance(to: previousTouchPoint.event.location)
let elapsed = max(0.0, path.points[index].event.timestamp - previousTouchPoint.event.timestamp)
let velocity = elapsed > 0.0 ? distance / elapsed : 0.0
points[index].velocity = velocity
}
} else {
assertionFailure("Attempting to modify a point that doesn't yet exist. maybe an update is out of order?")
}
} else {
indexesToRemove.insert(index)
}
}
for index in indexesToRemove.reversed() {
guard index < points.count else {
print("Error: unknown polyline index \(index)")
continue
}
points.remove(at: index)
}
isComplete = path.isComplete
return indexSet
}
}
private class BezierBuilder {
private var elements: [BezierPath.Element] = []
private let smoother: Smoother
private(set) var path = BezierPath()
init(smoother: Smoother) {
self.smoother = smoother
}
@discardableResult
func update(with line: Polyline, at lineIndexes: MinMaxIndex) -> MinMaxIndex {
let updatedPathIndexes = smoother.elementIndexes(for: line, at: lineIndexes, with: path)
guard
let min = updatedPathIndexes.first,
let max = updatedPathIndexes.last
else {
return updatedPathIndexes
}
let updatedPath: BezierPath
if min - 1 < path.elementCount,
min - 1 >= 0 {
updatedPath = path.trimming(to: min - 1)
} else {
updatedPath = BezierPath()
}
for elementIndex in min ... max {
assert(elementIndex <= elements.count, "Invalid element index")
if updatedPathIndexes.contains(elementIndex) {
if elementIndex > smoother.maxIndex(for: line) {
} else {
let element = smoother.element(for: line, at: elementIndex)
if elementIndex == elements.count {
elements.append(element)
} else {
elements[elementIndex] = element
}
updatedPath.append(element)
}
} else {
// use the existing element
let element = elements[elementIndex]
updatedPath.append(element)
}
}
for elementIndex in max + 1 ..< elements.count {
let element = elements[elementIndex]
updatedPath.append(element)
}
path = updatedPath
return updatedPathIndexes
}
}
private class Smoother {
let smoothFactor: CGFloat
init(smoothFactor: CGFloat = 0.7) {
self.smoothFactor = smoothFactor
}
func element(for line: Polyline, at elementIndex: Int) -> BezierPath.Element {
assert(elementIndex >= 0 && elementIndex <= maxIndex(for: line))
if elementIndex == 0 {
return BezierPath.Element(type: .moveTo, startPoint: line.points[0], endPoint: line.points[0], controlPoints: [])
}
if elementIndex == 1 {
return Self.newCurve(smoothFactor: smoothFactor,
startPoint: line.points[0],
p1: line.points[0].location,
p2: line.points[1],
p3: line.points[2].location)
}
if line.isComplete && elementIndex == maxIndex(for: line) {
return Self.newCurve(smoothFactor: smoothFactor,
startPoint: line.points[elementIndex - 1],
p0: line.points[elementIndex - 2].location,
p1: line.points[elementIndex - 1].location,
p2: line.points[elementIndex],
p3: line.points[elementIndex].location)
}
return Self.newCurve(smoothFactor: smoothFactor,
startPoint: line.points[elementIndex - 1],
p0: line.points[elementIndex - 2].location,
p1: line.points[elementIndex - 1].location,
p2: line.points[elementIndex],
p3: line.points[elementIndex + 1].location)
}
func maxIndex(for line: Polyline) -> Int {
let lastIndex = line.points.count - 1
return Swift.max(0, lastIndex - 1) + (line.points.count > 2 && line.isComplete ? 1 : 0)
}
func elementIndexes(for line: Polyline, at lineIndexes: MinMaxIndex, with bezier: BezierPath) -> MinMaxIndex {
var curveIndexes = MinMaxIndex()
for index in lineIndexes {
elementIndexes(for: line, at: index, with: bezier, into: &curveIndexes)
}
return curveIndexes
}
func elementIndexes(for line: Polyline, at lineIndex: Int, with bezier: BezierPath) -> MinMaxIndex {
var ret = MinMaxIndex()
elementIndexes(for: line, at: lineIndex, with: bezier, into: &ret)
return ret
}
// Below are the examples of input indexes, and which smoothed elements that point index affects
// 0 => 2, 1, 0
// 1 => 3, 2, 1, 0
// 2 => 4, 3, 2, 1
// 3 => 5, 4, 3, 2
// 4 => 6, 5, 4, 3
// 5 => 7, 6, 5, 4
// 6 => 8, 7, 6, 5
// 7 => 9, 8, 7, 6
private func elementIndexes(for line: Polyline, at lineIndex: Int, with bezier: BezierPath, into indexes: inout MinMaxIndex) {
guard lineIndex >= 0 else {
return
}
let max = maxIndex(for: line)
if lineIndex > 1,
(lineIndex - 1 <= max) || (lineIndex - 1 < bezier.elementCount) {
indexes.insert(lineIndex - 1)
}
if (lineIndex <= max) || (lineIndex < bezier.elementCount) {
indexes.insert(lineIndex)
}
if (lineIndex + 1 <= max) || (lineIndex + 1 < bezier.elementCount) {
indexes.insert(lineIndex + 1)
}
if (lineIndex + 2 <= max) || (lineIndex + 2 < bezier.elementCount) {
indexes.insert(lineIndex + 2)
}
}
// MARK: - Helper
private static func newCurve(
smoothFactor: CGFloat,
startPoint: Polyline.Point,
p0: CGPoint? = nil,
p1: CGPoint,
p2: Polyline.Point,
p3: CGPoint
) -> BezierPath.Element {
let p0 = p0 ?? p1
let c1 = CGPoint(x: (p0.x + p1.x) / 2.0, y: (p0.y + p1.y) / 2.0)
let c2 = CGPoint(x: (p1.x + p2.x) / 2.0, y: (p1.y + p2.y) / 2.0)
let c3 = CGPoint(x: (p2.x + p3.x) / 2.0, y: (p2.y + p3.y) / 2.0)
let len1 = sqrt((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y))
let len2 = sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y))
let len3 = sqrt((p3.x - p2.x) * (p3.x - p2.x) + (p3.y - p2.y) * (p3.y - p2.y))
let k1 = len1 / (len1 + len2)
let k2 = len2 / (len2 + len3)
let m1 = CGPoint(x: c1.x + (c2.x - c1.x) * k1, y: c1.y + (c2.y - c1.y) * k1)
let m2 = CGPoint(x: c2.x + (c3.x - c2.x) * k2, y: c2.y + (c3.y - c2.y) * k2)
// Resulting control points. Here smooth_value is mentioned
// above coefficient K whose value should be in range [0...1].
var ctrl1 = CGPoint(x: m1.x + (c2.x - m1.x) * smoothFactor + p1.x - m1.x,
y: m1.y + (c2.y - m1.y) * smoothFactor + p1.y - m1.y)
var ctrl2 = CGPoint(x: m2.x + (c2.x - m2.x) * smoothFactor + p2.x - m2.x,
y: m2.y + (c2.y - m2.y) * smoothFactor + p2.y - m2.y)
if ctrl1.x.isNaN || ctrl1.y.isNaN {
ctrl1 = p1
}
if ctrl2.x.isNaN || ctrl2.y.isNaN {
ctrl2 = p2.location
}
return BezierPath.Element(type: .cubicCurve, startPoint: startPoint, endPoint: p2, controlPoints: [ctrl1, ctrl2])
}
}