Merge pull request #14 from dscyrescotti/feature/memo-canvas

Implement memo canvas view
This commit is contained in:
Aye Chan
2024-05-04 22:39:55 +08:00
committed by GitHub
13 changed files with 179 additions and 26 deletions
+3 -1
View File
@@ -11,7 +11,9 @@ import SwiftUI
struct MemolaApp: App { struct MemolaApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MemosView() MemoView()
.environmentObject(Canvas())
.environment(\.managedObjectContext, Persistence.shared.viewContext)
} }
} }
} }
+13 -13
View File
@@ -26,7 +26,7 @@ final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicCo
var clipBounds: CGRect = .zero var clipBounds: CGRect = .zero
var zoomScale: CGFloat = .zero var zoomScale: CGFloat = .zero
// weak var board: BoardObject? weak var memo: Memo?
var graphicLoader: (() throws -> GraphicContext)? var graphicLoader: (() throws -> GraphicContext)?
@Published var state: State = .initial @Published var state: State = .initial
@@ -51,20 +51,20 @@ final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicCo
// MARK: - Actions // MARK: - Actions
extension Canvas { extension Canvas {
func load() { func load() {
guard let graphicLoader else { return } // guard let graphicLoader else { return }
Task(priority: .high) { [unowned self, graphicLoader] in Task(priority: .high) { [unowned self, graphicLoader] in
await MainActor.run { await MainActor.run {
self.state = .loading self.state = .loading
} }
do { do {
let graphicContext = try graphicLoader() // let graphicContext = try graphicLoader()
graphicContext.delegate = self graphicContext.delegate = self
await MainActor.run { await MainActor.run {
self.graphicContext = graphicContext // self.graphicContext = graphicContext
self.state = .loaded self.state = .loaded
} }
} catch { } catch {
NSLog("[SketchNote] - \(error.localizedDescription)") NSLog("[Memola] - \(error.localizedDescription)")
await MainActor.run { await MainActor.run {
self.state = .failed self.state = .failed
} }
@@ -73,14 +73,14 @@ extension Canvas {
} }
func save(on managedObjectContext: NSManagedObjectContext) async { func save(on managedObjectContext: NSManagedObjectContext) async {
// guard let board else { return } guard let memo else { return }
// do { do {
// board.data = try JSONEncoder().encode(self) memo.data = try JSONEncoder().encode(self)
// board.updatedAt = Date() memo.updatedAt = Date()
// try managedObjectContext.save() try managedObjectContext.save()
// } catch { } catch {
// NSLog("[SketchNote] - \(error.localizedDescription)") NSLog("[Memola] - \(error.localizedDescription)")
// } }
} }
func listen(on managedObjectContext: NSManagedObjectContext) { func listen(on managedObjectContext: NSManagedObjectContext) {
+4
View File
@@ -11,6 +11,10 @@ import Foundation
class Textures { class Textures {
static var penTextures: [String: MTLTexture] = [:] static var penTextures: [String: MTLTexture] = [:]
static func hasCreatedPenTexture(of textureName: String) -> Bool {
penTextures[textureName] != nil
}
@discardableResult @discardableResult
static func createPenTexture(with textureName: String, on device: MTLDevice) -> MTLTexture? { static func createPenTexture(with textureName: String, on device: MTLDevice) -> MTLTexture? {
if let penTexture = penTextures[textureName] { if let penTexture = penTextures[textureName] {
@@ -110,6 +110,8 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
let distance = start.distance(to: end) let distance = start.distance(to: end)
let factor: CGFloat let factor: CGFloat
switch configuration.granularity { switch configuration.granularity {
case .automatic:
factor = min(6, 1 / (stroke.thickness * 10 / 500))
case .fixed: case .fixed:
factor = 1 / (stroke.thickness * stroke.style.stepRate) factor = 1 / (stroke.thickness * stroke.style.stepRate)
case .none: case .none:
@@ -139,7 +141,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
extension SolidPointStrokeGenerator { extension SolidPointStrokeGenerator {
struct Configuration { struct Configuration {
var rotation: Rotation = .fixed var rotation: Rotation = .fixed
var granularity: Granularity = .none var granularity: Granularity = .automatic
} }
enum Rotation { enum Rotation {
@@ -148,6 +150,7 @@ extension SolidPointStrokeGenerator {
} }
enum Granularity { enum Granularity {
case automatic
case fixed case fixed
case none case none
} }
+1 -1
View File
@@ -24,7 +24,6 @@ class Stroke: Codable {
var tailVertices: [QuadVertex] = [] var tailVertices: [QuadVertex] = []
var texture: MTLTexture? var texture: MTLTexture?
// var strokeTexture: MTLTexture?
var isEmpty: Bool { var isEmpty: Bool {
vertices.isEmpty vertices.isEmpty
@@ -91,6 +90,7 @@ class Stroke: Codable {
func finish(at point: CGPoint) { func finish(at point: CGPoint) {
style.generator.finish(at: point, on: self) style.generator.finish(at: point, on: self)
NSLog("[Memola] - \(MemoryLayout<QuadVertex>.stride * vertexCount) bytes")
} }
} }
@@ -12,7 +12,7 @@ struct EraserPenStyle: PenStyle {
var textureName: String = "point-texture" var textureName: String = "point-texture"
var thinkness: (min: CGFloat, max: CGFloat) = (15, 120) var thinkness: (min: CGFloat, max: CGFloat) = (1, 120)
var color: [CGFloat] = [1, 1, 1, 0] var color: [CGFloat] = [1, 1, 1, 0]
@@ -12,7 +12,7 @@ struct MarkerPenStyle: PenStyle {
var textureName: String = "point-texture" var textureName: String = "point-texture"
var thinkness: (min: CGFloat, max: CGFloat) = (15, 120) var thinkness: (min: CGFloat, max: CGFloat) = (1, 120)
var color: [CGFloat] = [1, 0.38, 0.38, 1] var color: [CGFloat] = [1, 0.38, 0.38, 1]
+1
View File
@@ -18,6 +18,7 @@ class Tool: NSObject, ObservableObject {
Pen(for: .eraser) Pen(for: .eraser)
] ]
super.init() super.init()
selectedPen = pens.first
} }
func changePen(_ pen: Pen) { func changePen(_ pen: Pen) {
@@ -43,7 +43,7 @@ class CanvasViewController: UIViewController {
configureGestures() configureGestures()
configureListeners() configureListeners()
loadBoard() loadMemo()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@@ -161,7 +161,7 @@ extension CanvasViewController {
} }
extension CanvasViewController { extension CanvasViewController {
func loadBoard() { func loadMemo() {
canvas.load() canvas.load()
} }
+67 -6
View File
@@ -9,14 +9,75 @@ import SwiftUI
struct MemoView: View { struct MemoView: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.managedObjectContext) var managedObjectContext
@StateObject var tool = Tool()
@StateObject var history = History()
@EnvironmentObject var canvas: Canvas
var body: some View { var body: some View {
VStack { CanvasView()
Text("Memo View") .ignoresSafeArea()
Button { .overlay(alignment: .bottomTrailing) {
dismiss() PenToolView()
} label: { .padding()
Text("Close Memo")
} }
.overlay(alignment: .topTrailing) {
historyTool
.padding()
}
.overlay(alignment: .topLeading) {
Button {
dismiss()
} label: {
Image(systemName: "xmark")
.padding(15)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
.hoverEffect(.lift)
.padding()
}
.disabled(canvas.state == .loading)
.overlay {
if canvas.state == .loading {
ProgressView {
Text("Loading memo...")
}
.progressViewStyle(.circular)
.padding(20)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
.environmentObject(tool)
.environmentObject(canvas)
.environmentObject(history)
.task {
canvas.listen(on: managedObjectContext)
}
}
var historyTool: some View {
HStack {
Button {
history.historyPublisher.send(.undo)
} label: {
Image(systemName: "arrow.uturn.backward.circle")
}
.hoverEffect(.lift)
.disabled(history.undoDisabled)
Button {
history.historyPublisher.send(.redo)
} label: {
Image(systemName: "arrow.uturn.forward.circle")
}
.hoverEffect(.lift)
.disabled(history.redoDisabled)
} }
.padding(15)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
} }
} }
+82
View File
@@ -0,0 +1,82 @@
//
// PenToolView.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
struct PenToolView: View {
@EnvironmentObject var tool: Tool
var body: some View {
VStack {
if let pen = tool.selectedPen {
let thicknessBounds = pen.style.thinkness
let thickness = Binding {
max(pen.thickness, pen.style.thinkness.min)
} set: { newValue in
tool.selectedPen?.thickness = newValue
}
let color = Binding {
Color.rgba(from: pen.color)
} set: { newValue in
tool.selectedPen?.color = newValue.components
tool.objectWillChange.send()
}
HStack {
ColorPicker("", selection: color)
.frame(width: 40, height: 40)
Slider(value: thickness, in: thicknessBounds.min...thicknessBounds.max)
.frame(width: 180, height: 40)
}
}
HStack {
ForEach(tool.pens) { pen in
penView(pen)
.overlay(alignment: .bottom) {
if tool.selectedPen === pen {
Circle()
.frame(width: 5, height: 5)
.offset(y: 7.5)
.foregroundStyle(Color.rgba(from: pen.color))
}
}
}
}
.padding(15)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
@ViewBuilder
func penView(_ pen: Pen) -> some View {
Button {
if tool.selectedPen === pen {
tool.selectedPen = nil
} else {
tool.changePen(pen)
}
} label: {
ZStack {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.color))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: 30, height: 65)
.drawingGroup()
.hoverEffect(.lift)
}
.buttonStyle(.plain)
}
}