From 854c7cd732258aaacd1e3effb7e118acb7f3af36 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 5 May 2024 17:46:25 +0700 Subject: [PATCH] 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 + } + } +}