mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-17 13:17:03 +02:00
Merge pull request #14 from dscyrescotti/feature/memo-canvas
Implement memo canvas view
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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]
|
||||||
|
|
||||||
+1
-1
@@ -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]
|
||||||
|
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user