mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-24 18:31:23 +01:00
340 lines
11 KiB
Swift
340 lines
11 KiB
Swift
//
|
|
// CanvasViewController.swift
|
|
// Memola
|
|
//
|
|
// Created by Dscyre Scotti on 5/4/24.
|
|
//
|
|
|
|
import Combine
|
|
import SwiftUI
|
|
import MetalKit
|
|
import Foundation
|
|
|
|
class CanvasViewController: UIViewController {
|
|
let drawingView: DrawingView
|
|
let scrollView: UIScrollView = UIScrollView()
|
|
var renderView: MTKView {
|
|
drawingView.renderView
|
|
}
|
|
|
|
let tool: Tool
|
|
let canvas: Canvas
|
|
let history: History
|
|
let renderer: Renderer
|
|
|
|
var cancellables: Set<AnyCancellable> = []
|
|
|
|
init(tool: Tool, canvas: Canvas, history: History) {
|
|
self.tool = tool
|
|
self.canvas = canvas
|
|
self.history = history
|
|
self.drawingView = DrawingView(tool: tool, canvas: canvas, history: history)
|
|
self.renderer = Renderer(canvasView: drawingView.renderView)
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
configureViews()
|
|
configureGestures()
|
|
configureListeners()
|
|
|
|
loadMemo()
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
resizeDocumentView()
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
drawingView.disableUserInteraction()
|
|
drawingView.updateDrawableSize(with: view.frame.size)
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func configureViews() {
|
|
view.backgroundColor = .white
|
|
renderView.autoResizeDrawable = false
|
|
renderView.enableSetNeedsDisplay = true
|
|
renderView.translatesAutoresizingMaskIntoConstraints = false
|
|
renderView.clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 1)
|
|
view.addSubview(renderView)
|
|
NSLayoutConstraint.activate([
|
|
renderView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
renderView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
renderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
renderView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
])
|
|
|
|
scrollView.maximumZoomScale = canvas.maximumZoomScale
|
|
scrollView.minimumZoomScale = canvas.minimumZoomScale
|
|
scrollView.contentInsetAdjustmentBehavior = .never
|
|
scrollView.isScrollEnabled = true
|
|
scrollView.showsVerticalScrollIndicator = true
|
|
scrollView.showsHorizontalScrollIndicator = true
|
|
scrollView.delegate = self
|
|
scrollView.backgroundColor = .clear
|
|
// scrollView.pinchGestureRecognizer?.cancelsTouchesInView = true
|
|
// scrollView.pinchGestureRecognizer?.delaysTouchesEnded = true
|
|
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(scrollView)
|
|
NSLayoutConstraint.activate([
|
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
])
|
|
|
|
scrollView.addSubview(drawingView)
|
|
drawingView.backgroundColor = .clear
|
|
drawingView.isUserInteractionEnabled = false
|
|
drawingView.isMultipleTouchEnabled = true
|
|
drawingView.isExclusiveTouch = true
|
|
}
|
|
|
|
func resizeDocumentView(to newSize: CGSize? = nil) {
|
|
scrollView.layoutIfNeeded()
|
|
|
|
let size = canvas.size
|
|
let widthScale = (newSize?.width ?? view.frame.width) / size.width
|
|
let heightScale = (newSize?.height ?? view.frame.height) / size.height
|
|
let scale = max(widthScale, heightScale)
|
|
|
|
let width = size.width * scale
|
|
let height = size.height * scale
|
|
let newFrame = CGRect(x: 0, y: 0, width: width, height: height)
|
|
drawingView.frame = newFrame
|
|
|
|
scrollView.setZoomScale(canvas.minimumZoomScale, animated: true)
|
|
centerDocumentView(to: newSize)
|
|
|
|
let offsetX = (newFrame.width * canvas.minimumZoomScale - view.frame.width) / 2
|
|
let offsetY = (newFrame.height * canvas.minimumZoomScale - view.frame.height) / 2
|
|
|
|
let point = CGPoint(x: offsetX, y: offsetY)
|
|
scrollView.setContentOffset(point, animated: true)
|
|
|
|
drawingView.updateDrawableSize(with: view.frame.size)
|
|
}
|
|
|
|
func centerDocumentView(to newSize: CGSize? = nil) {
|
|
let documentViewSize = drawingView.frame.size
|
|
let scrollViewSize = newSize ?? view.frame.size
|
|
let verticalPadding = documentViewSize.height < scrollViewSize.height ? (scrollViewSize.height - documentViewSize.height) / 2 : 0
|
|
let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0
|
|
self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func configureListeners() {
|
|
canvas.$state
|
|
.sink { [weak self] state in
|
|
self?.canvasStateChanged(state)
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
tool.$selectedPen
|
|
.sink { [weak self] pen in
|
|
self?.penChanged(to: pen)
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
history.historyPublisher
|
|
.sink { [weak self] action in
|
|
switch action {
|
|
case .undo:
|
|
self?.historyUndid()
|
|
case .redo:
|
|
self?.historyRedid()
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func loadMemo() {
|
|
canvas.load()
|
|
}
|
|
|
|
func canvasStateChanged(_ state: Canvas.State) {
|
|
guard state == .loaded else { return }
|
|
renderView.delegate = self
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController: MTKViewDelegate {
|
|
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { }
|
|
|
|
func draw(in view: MTKView) {
|
|
guard view.drawableSize != .zero else {
|
|
return
|
|
}
|
|
canvas.updateTransform(on: drawingView)
|
|
renderer.draw(in: view, on: canvas)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
|
magnificationStarted()
|
|
}
|
|
|
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
canvas.setZoomScale(scrollView.zoomScale)
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
}
|
|
|
|
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
|
centerDocumentView()
|
|
magnificationEnded()
|
|
}
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
draggingStarted()
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
if !scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
|
|
scrollViewDidEndScrolling(scrollView)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
if !decelerate, scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
|
|
scrollViewDidEndScrolling(scrollView)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
|
|
draggingEnded()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func magnificationStarted() {
|
|
guard !renderer.updatesViewPort else { return }
|
|
canvas.updateClipBounds(scrollView, on: drawingView)
|
|
drawingView.disableUserInteraction()
|
|
renderer.updatesViewPort = true
|
|
}
|
|
|
|
func magnificationEnded() {
|
|
renderer.updatesViewPort = false
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
|
|
func draggingStarted() {
|
|
guard !renderer.updatesViewPort else { return }
|
|
canvas.updateClipBounds(scrollView, on: drawingView)
|
|
drawingView.disableUserInteraction()
|
|
renderer.updatesViewPort = true
|
|
}
|
|
|
|
func draggingEnded() {
|
|
renderer.updatesViewPort = false
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func penChanged(to pen: Pen?) {
|
|
if let pen, let device = drawingView.renderView.device {
|
|
pen.style.loadTexture(on: device)
|
|
}
|
|
let isPenSelected = pen != nil
|
|
scrollView.isScrollEnabled = !isPenSelected
|
|
drawingView.isUserInteractionEnabled = isPenSelected
|
|
isPenSelected ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func historyUndid() {
|
|
guard history.undo() else { return }
|
|
drawingView.disableUserInteraction()
|
|
canvas.graphicContext.undoGraphic()
|
|
renderer.redrawsGraphicRender = true
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
|
|
func historyRedid() {
|
|
guard let event = history.redo() else { return }
|
|
drawingView.disableUserInteraction()
|
|
canvas.graphicContext.redoGraphic(for: event)
|
|
renderer.redrawsGraphicRender = true
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
}
|