From 854c7cd732258aaacd1e3effb7e118acb7f3af36 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 5 May 2024 17:46:25 +0700 Subject: [PATCH 1/2] bug: sync pen drawing and stroke generating --- Memola.xcodeproj/project.pbxproj | 20 ++- Memola/Canvas/Contexts/GraphicContext.swift | 8 + Memola/Canvas/Core/Canvas.swift | 8 +- .../SolidPointStrokeGenerator.swift | 27 +++- Memola/Canvas/Geometries/Stroke/Stroke.swift | 2 - Memola/Canvas/History/History.swift | 11 ++ Memola/Canvas/View/Bridge/DrawingView.swift | 68 --------- .../CanvasViewController.swift | 62 ++++---- .../View/Bridge/Views/DrawingView.swift | 144 ++++++++++++++++++ 9 files changed, 244 insertions(+), 106 deletions(-) delete mode 100644 Memola/Canvas/View/Bridge/DrawingView.swift rename Memola/Canvas/View/Bridge/{ => ViewController}/CanvasViewController.swift (86%) create mode 100644 Memola/Canvas/View/Bridge/Views/DrawingView.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index cef513d..9ba2c66 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -142,6 +142,22 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + EC1437B42BE748E60022C903 /* Views */ = { + isa = PBXGroup; + children = ( + ECA738AC2BE60CC600A4542E /* DrawingView.swift */, + ); + path = Views; + sourceTree = ""; + }; + EC1437B52BE748EF0022C903 /* ViewController */ = { + isa = PBXGroup; + children = ( + ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; EC7F6BDF2BE5E6E300A34A7B = { isa = PBXGroup; children = ( @@ -338,8 +354,8 @@ ECA738AE2BE60CEC00A4542E /* Bridge */ = { isa = PBXGroup; children = ( - ECA738AC2BE60CC600A4542E /* DrawingView.swift */, - ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */, + EC1437B52BE748EF0022C903 /* ViewController */, + EC1437B42BE748E60022C903 /* Views */, ); path = Bridge; sourceTree = ""; diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 6c8a24d..39d5e24 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -118,6 +118,14 @@ extension GraphicContext { self.currentPoint = nil delegate?.didUpdate.send() } + + func cancelStroke() { + if !strokes.isEmpty { + strokes.removeLast() + } + currentStroke = nil + currentPoint = nil + } } extension GraphicContext { diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index baac125..9c81794 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -12,7 +12,7 @@ import Foundation final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicContextDelegate { let size: CGSize - let maximumZoomScale: CGFloat = 30 + let maximumZoomScale: CGFloat = 25 let minimumZoomScale: CGFloat = 3.1 var transform: simd_float4x4 = .init() @@ -32,7 +32,7 @@ final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicCo @Published var state: State = .initial lazy var didUpdate = PassthroughSubject() - init(size: CGSize = .init(width: 8_000, height: 8_000)) { + init(size: CGSize = .init(width: 4_000, height: 4_000)) { self.size = size } @@ -132,6 +132,10 @@ extension Canvas { graphicContext.endStroke(at: point) } + func cancelTouch() { + graphicContext.cancelStroke() + } + func setGraphicRenderType(_ renderType: GraphicContext.RenderType) { graphicContext.renderType = renderType } diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index 5f0add9..7a06d70 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -89,8 +89,12 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let averageX = (prev.x + current.x) / 2 let averageY = (prev.y + current.y) / 2 let point = CGPoint(x: averageX, y: averageY) - stroke.keyPoints[index] = point - stroke.keyPoints[index - 1] = point + if index != 0 { + stroke.keyPoints[index] = point + } + if index - 1 != 0 { + stroke.keyPoints[index - 1] = point + } } private func addPoint(_ point: CGPoint, on stroke: Stroke) { @@ -117,9 +121,9 @@ struct SolidPointStrokeGenerator: StrokeGenerator { case .none: factor = 1 / (stroke.thickness * 10 / 500) } - let segements = max(Int(distance * factor), 1) - for i in 0...stride * vertexCount) bytes") } } diff --git a/Memola/Canvas/History/History.swift b/Memola/Canvas/History/History.swift index 1cb2279..87f6911 100644 --- a/Memola/Canvas/History/History.swift +++ b/Memola/Canvas/History/History.swift @@ -12,6 +12,8 @@ class History: ObservableObject { @Published var undoStack: [HistoryEvent] = [] @Published var redoStack: [HistoryEvent] = [] + var redoCache: [HistoryEvent] = [] + let historyPublisher = PassthroughSubject() var undoDisabled: Bool { @@ -46,6 +48,15 @@ class History: ObservableObject { } func resetRedo() { + redoCache = redoStack redoStack.removeAll() } + + func restoreUndo() { + if !undoStack.isEmpty { + undoStack.removeLast() + } + redoStack = redoCache + redoCache.removeAll() + } } diff --git a/Memola/Canvas/View/Bridge/DrawingView.swift b/Memola/Canvas/View/Bridge/DrawingView.swift deleted file mode 100644 index 8791cf5..0000000 --- a/Memola/Canvas/View/Bridge/DrawingView.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// DrawingView.swift -// Memola -// -// Created by Dscyre Scotti on 5/4/24. -// - -import UIKit -import MetalKit -import Foundation - -class DrawingView: UIView { - let tool: Tool - let canvas: Canvas - let history: History - let renderView: MTKView - - var ratio: CGFloat { canvas.size.width / bounds.width } - - private var disablesUserInteraction: Bool = false - - required init(tool: Tool, canvas: Canvas, history: History) { - self.tool = tool - self.canvas = canvas - self.history = history - self.renderView = MTKView(frame: .zero) - super.init(frame: .zero) - } - - required init(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateDrawableSize(with size: CGSize) { - renderView.drawableSize = size.multiply(by: 2.5) - } - - func touchBegan(on point: CGPoint) { - guard !disablesUserInteraction else { return } - guard let pen = tool.selectedPen else { return } - let stroke = canvas.beginTouch(at: point.muliply(by: ratio), pen: pen) - renderView.draw() - history.addUndo(.stroke(stroke)) - history.resetRedo() - } - - func touchMoved(to point: CGPoint) { - guard !disablesUserInteraction else { return } - canvas.moveTouch(to: point.muliply(by: ratio)) - renderView.draw() - } - - func touchEnded(to point: CGPoint) { - guard !disablesUserInteraction else { return } - canvas.endTouch(at: point.muliply(by: ratio)) - renderView.draw() - } - - func disableUserInteraction() { - disablesUserInteraction = true - } - - func enableUserInteraction() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in - self?.disablesUserInteraction = false - } - } -} diff --git a/Memola/Canvas/View/Bridge/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift similarity index 86% rename from Memola/Canvas/View/Bridge/CanvasViewController.swift rename to Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 174ac95..4f98fc2 100644 --- a/Memola/Canvas/View/Bridge/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -84,6 +84,8 @@ extension CanvasViewController { scrollView.showsHorizontalScrollIndicator = true scrollView.delegate = self scrollView.backgroundColor = .clear +// scrollView.pinchGestureRecognizer?.cancelsTouchesInView = true +// scrollView.pinchGestureRecognizer?.delaysTouchesEnded = true scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) @@ -97,6 +99,8 @@ extension CanvasViewController { scrollView.addSubview(drawingView) drawingView.backgroundColor = .clear drawingView.isUserInteractionEnabled = false + drawingView.isMultipleTouchEnabled = true + drawingView.isExclusiveTouch = true } func resizeDocumentView(to newSize: CGSize? = nil) { @@ -187,37 +191,41 @@ extension CanvasViewController: MTKViewDelegate { extension CanvasViewController { func configureGestures() { - let drawingPanGesture = UIPanGestureRecognizer(target: self, action: #selector(recognizePanGesture)) - drawingPanGesture.maximumNumberOfTouches = 1 - drawingPanGesture.minimumNumberOfTouches = 1 - drawingView.addGestureRecognizer(drawingPanGesture) +// let drawingPanGesture = PanGestureRecognizer(target: self, action: #selector(recognizePanGesture)) +// drawingPanGesture.maximumNumberOfTouches = 1 +// drawingPanGesture.minimumNumberOfTouches = 1 +// drawingView.addGestureRecognizer(drawingPanGesture) - let drawingTapGesture = UITapGestureRecognizer(target: self, action: #selector(recognizeTapGesture)) - drawingTapGesture.numberOfTapsRequired = 1 - drawingView.addGestureRecognizer(drawingTapGesture) +// let drawingTapGesture = UITapGestureRecognizer(target: self, action: #selector(recognizeTapGesture)) +// drawingTapGesture.numberOfTapsRequired = 1 +// drawingView.addGestureRecognizer(drawingTapGesture) } - @objc func recognizePanGesture(_ gesture: UIPanGestureRecognizer) { - let point = gesture.location(in: drawingView) - switch gesture.state { - case .began: - drawingView.touchBegan(on: point) - case .changed: - drawingView.touchMoved(to: point) - case .ended: - drawingView.touchEnded(to: point) - case .cancelled: - drawingView.touchEnded(to: point) - default: - break - } - } +// @objc func recognizePanGesture(_ gesture: PanGestureRecognizer) { +// let point = gesture.location(in: drawingView) +// switch gesture.state { +// case .began: +// if let initialTouch = gesture.initialTouch { +// drawingView.touchBegan(on: initialTouch.location(in: drawingView)) +// } else { +// drawingView.touchBegan(on: point) +// } +// case .changed: +// drawingView.touchMoved(to: point) +// case .ended: +// drawingView.touchEnded(to: point) +// case .cancelled: +// drawingView.touchEnded(to: point) +// default: +// break +// } +// } - @objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) { - let point = gesture.location(in: drawingView) - drawingView.touchBegan(on: point) - drawingView.touchEnded(to: point) - } +// @objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) { +// let point = gesture.location(in: drawingView) +// drawingView.touchBegan(on: point) +// drawingView.touchEnded(to: point) +// } } extension CanvasViewController: UIScrollViewDelegate { diff --git a/Memola/Canvas/View/Bridge/Views/DrawingView.swift b/Memola/Canvas/View/Bridge/Views/DrawingView.swift new file mode 100644 index 0000000..a34705d --- /dev/null +++ b/Memola/Canvas/View/Bridge/Views/DrawingView.swift @@ -0,0 +1,144 @@ +// +// DrawingView.swift +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +import UIKit +import MetalKit +import Foundation + +class DrawingView: UIView { + let tool: Tool + let canvas: Canvas + let history: History + let renderView: MTKView + + var ratio: CGFloat { canvas.size.width / bounds.width } + + var beganTouches: Set = [] + + private var disablesUserInteraction: Bool = false + + required init(tool: Tool, canvas: Canvas, history: History) { + self.tool = tool + self.canvas = canvas + self.history = history + self.renderView = MTKView(frame: .zero) + super.init(frame: .zero) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateDrawableSize(with size: CGSize) { + renderView.drawableSize = size.multiply(by: 2.5) + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + print("Touch Began - \(touches.count) & \(event?.allTouches?.count ?? -1)") + super.touchesBegan(touches, with: event) + for touch in touches { + beganTouches.insert(touch) + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + print("Touch Moved - \(beganTouches.count) & \(event?.allTouches?.count ?? -1)") + super.touchesMoved(touches, with: event) + validateTouch() + guard let touch = touches.first else { return } + let point = touch.location(in: self) + touchMoved(to: point) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + print("Touch Ended - \(beganTouches.count)") + super.touchesEnded(touches, with: event) + validateTouch() + guard let touch = touches.first else { return } + let point = touch.location(in: self) + touchEnded(at: point) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + print("Touch Cancelled - \(beganTouches.count)") + super.touchesCancelled(touches, with: event) + touchCancelled() + } + +// func didCreateNewStroke() -> Bool { +// switch beganTouches.count { +// case 0: +// return true +// case 1: +// if canvas.graphicContext.currentStroke == nil { +// guard let touch = beganTouches.first else { return false } +// let point = touch.location(in: self) +// touchBegan(at: point) +// beganTouches.removeAll() +// return true +// } else { +// touchCancelled() +// return false +// } +// default: +// return false +// } +// } + + func validateTouch() { + if beganTouches.count == 1 { + if canvas.graphicContext.currentStroke == nil { + guard let touch = beganTouches.first else { return } + let point = touch.location(in: self) + touchBegan(at: point) + beganTouches.removeAll() + } else { + touchCancelled() + } + } + } + + func touchBegan(at point: CGPoint) { + guard !disablesUserInteraction else { return } + guard let pen = tool.selectedPen else { return } + let stroke = canvas.beginTouch(at: point.muliply(by: ratio), pen: pen) + history.addUndo(.stroke(stroke)) + history.resetRedo() + } + + func touchMoved(to point: CGPoint) { + guard !disablesUserInteraction else { return } + canvas.moveTouch(to: point.muliply(by: ratio)) + renderView.draw() + } + + func touchEnded(at point: CGPoint) { + guard !disablesUserInteraction else { return } + canvas.endTouch(at: point.muliply(by: ratio)) + renderView.draw() + beganTouches.removeAll() + } + + func touchCancelled() { + if canvas.graphicContext.currentStroke != nil { + canvas.cancelTouch() + renderView.draw() + history.restoreUndo() + } + beganTouches.removeAll() + } + + func disableUserInteraction() { + disablesUserInteraction = true + } + + func enableUserInteraction() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in + self?.disablesUserInteraction = false + } + } +} From 0202678d8d7e06220c0a9a6f76022282f5847051 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 6 May 2024 21:02:23 +0700 Subject: [PATCH 2/2] feat: resolve pinch gesture conflict with responder touches --- Memola/Canvas/Core/Canvas.swift | 7 +++ Memola/Canvas/Geometries/Stroke/Stroke.swift | 2 + .../ViewController/CanvasViewController.swift | 45 +------------- .../View/Bridge/Views/DrawingView.swift | 61 ++++--------------- 4 files changed, 23 insertions(+), 92 deletions(-) diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 9c81794..f8d2381 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -32,6 +32,13 @@ final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicCo @Published var state: State = .initial lazy var didUpdate = PassthroughSubject() + var hasValidStroke: Bool { + if let currentStroke = graphicContext.currentStroke { + return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80 + } + return false + } + init(size: CGSize = .init(width: 4_000, height: 4_000)) { self.size = size } diff --git a/Memola/Canvas/Geometries/Stroke/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Stroke.swift index 6272536..31cd2e3 100644 --- a/Memola/Canvas/Geometries/Stroke/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Stroke.swift @@ -22,6 +22,8 @@ class Stroke: Codable { var vertexBuffer: MTLBuffer? var vertexCount: Int = 0 + let createdAt: Date = Date() + var texture: MTLTexture? var isEmpty: Bool { diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 4f98fc2..5a3907a 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -40,7 +40,6 @@ class CanvasViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() configureViews() - configureGestures() configureListeners() loadMemo() @@ -84,8 +83,6 @@ extension CanvasViewController { scrollView.showsHorizontalScrollIndicator = true scrollView.delegate = self scrollView.backgroundColor = .clear -// scrollView.pinchGestureRecognizer?.cancelsTouchesInView = true -// scrollView.pinchGestureRecognizer?.delaysTouchesEnded = true scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) @@ -99,8 +96,6 @@ extension CanvasViewController { scrollView.addSubview(drawingView) drawingView.backgroundColor = .clear drawingView.isUserInteractionEnabled = false - drawingView.isMultipleTouchEnabled = true - drawingView.isExclusiveTouch = true } func resizeDocumentView(to newSize: CGSize? = nil) { @@ -189,45 +184,6 @@ extension CanvasViewController: MTKViewDelegate { } } -extension CanvasViewController { - func configureGestures() { -// let drawingPanGesture = PanGestureRecognizer(target: self, action: #selector(recognizePanGesture)) -// drawingPanGesture.maximumNumberOfTouches = 1 -// drawingPanGesture.minimumNumberOfTouches = 1 -// drawingView.addGestureRecognizer(drawingPanGesture) - -// let drawingTapGesture = UITapGestureRecognizer(target: self, action: #selector(recognizeTapGesture)) -// drawingTapGesture.numberOfTapsRequired = 1 -// drawingView.addGestureRecognizer(drawingTapGesture) - } - -// @objc func recognizePanGesture(_ gesture: PanGestureRecognizer) { -// let point = gesture.location(in: drawingView) -// switch gesture.state { -// case .began: -// if let initialTouch = gesture.initialTouch { -// drawingView.touchBegan(on: initialTouch.location(in: drawingView)) -// } else { -// drawingView.touchBegan(on: point) -// } -// case .changed: -// drawingView.touchMoved(to: point) -// case .ended: -// drawingView.touchEnded(to: point) -// case .cancelled: -// drawingView.touchEnded(to: point) -// default: -// break -// } -// } - -// @objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) { -// let point = gesture.location(in: drawingView) -// drawingView.touchBegan(on: point) -// drawingView.touchEnded(to: point) -// } -} - extension CanvasViewController: UIScrollViewDelegate { func viewForZooming(in scrollView: UIScrollView) -> UIView? { drawingView @@ -277,6 +233,7 @@ extension CanvasViewController: UIScrollViewDelegate { extension CanvasViewController { func magnificationStarted() { guard !renderer.updatesViewPort else { return } + drawingView.touchCancelled() canvas.updateClipBounds(scrollView, on: drawingView) drawingView.disableUserInteraction() renderer.updatesViewPort = true diff --git a/Memola/Canvas/View/Bridge/Views/DrawingView.swift b/Memola/Canvas/View/Bridge/Views/DrawingView.swift index a34705d..20c263d 100644 --- a/Memola/Canvas/View/Bridge/Views/DrawingView.swift +++ b/Memola/Canvas/View/Bridge/Views/DrawingView.swift @@ -17,8 +17,6 @@ class DrawingView: UIView { var ratio: CGFloat { canvas.size.width / bounds.width } - var beganTouches: Set = [] - private var disablesUserInteraction: Bool = false required init(tool: Tool, canvas: Canvas, history: History) { @@ -37,69 +35,36 @@ class DrawingView: UIView { renderView.drawableSize = size.multiply(by: 2.5) } + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + !canvas.hasValidStroke + } + override func touchesBegan(_ touches: Set, with event: UIEvent?) { - print("Touch Began - \(touches.count) & \(event?.allTouches?.count ?? -1)") super.touchesBegan(touches, with: event) - for touch in touches { - beganTouches.insert(touch) - } + guard let touch = touches.first else { return } + let point = touch.location(in: self) + touchBegan(at: point) } override func touchesMoved(_ touches: Set, with event: UIEvent?) { - print("Touch Moved - \(beganTouches.count) & \(event?.allTouches?.count ?? -1)") super.touchesMoved(touches, with: event) - validateTouch() guard let touch = touches.first else { return } let point = touch.location(in: self) touchMoved(to: point) } override func touchesEnded(_ touches: Set, with event: UIEvent?) { - print("Touch Ended - \(beganTouches.count)") super.touchesEnded(touches, with: event) - validateTouch() guard let touch = touches.first else { return } let point = touch.location(in: self) touchEnded(at: point) } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - print("Touch Cancelled - \(beganTouches.count)") super.touchesCancelled(touches, with: event) - touchCancelled() - } - -// func didCreateNewStroke() -> Bool { -// switch beganTouches.count { -// case 0: -// return true -// case 1: -// if canvas.graphicContext.currentStroke == nil { -// guard let touch = beganTouches.first else { return false } -// let point = touch.location(in: self) -// touchBegan(at: point) -// beganTouches.removeAll() -// return true -// } else { -// touchCancelled() -// return false -// } -// default: -// return false -// } -// } - - func validateTouch() { - if beganTouches.count == 1 { - if canvas.graphicContext.currentStroke == nil { - guard let touch = beganTouches.first else { return } - let point = touch.location(in: self) - touchBegan(at: point) - beganTouches.removeAll() - } else { - touchCancelled() - } - } + guard let touch = touches.first else { return } + let point = touch.location(in: self) + touchEnded(at: point) } func touchBegan(at point: CGPoint) { @@ -113,14 +78,15 @@ class DrawingView: UIView { func touchMoved(to point: CGPoint) { guard !disablesUserInteraction else { return } canvas.moveTouch(to: point.muliply(by: ratio)) - renderView.draw() + if canvas.hasValidStroke { + renderView.draw() + } } func touchEnded(at point: CGPoint) { guard !disablesUserInteraction else { return } canvas.endTouch(at: point.muliply(by: ratio)) renderView.draw() - beganTouches.removeAll() } func touchCancelled() { @@ -129,7 +95,6 @@ class DrawingView: UIView { renderView.draw() history.restoreUndo() } - beganTouches.removeAll() } func disableUserInteraction() {