mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-04-23 17:18:42 +02:00
193 lines
6.5 KiB
Swift
193 lines
6.5 KiB
Swift
//
|
|
// GraphicContext.swift
|
|
// Memola
|
|
//
|
|
// Created by Dscyre Scotti on 5/4/24.
|
|
//
|
|
|
|
import Combine
|
|
import MetalKit
|
|
import CoreData
|
|
import Foundation
|
|
|
|
final class GraphicContext: @unchecked Sendable {
|
|
var strokes: [any Stroke] = []
|
|
var object: GraphicContextObject?
|
|
|
|
var currentStroke: (any Stroke)?
|
|
var previousStroke: (any Stroke)?
|
|
|
|
var currentPoint: CGPoint?
|
|
var renderType: RenderType = .finished
|
|
var vertices: [ViewPortVertex] = []
|
|
var vertexCount: Int = 4
|
|
var vertexBuffer: MTLBuffer?
|
|
|
|
init() {
|
|
setViewPortVertices()
|
|
}
|
|
|
|
func setViewPortVertices() {
|
|
vertexBuffer = nil
|
|
vertices = [
|
|
ViewPortVertex(x: -1, y: -1, textCoord: CGPoint(x: 0, y: 1)),
|
|
ViewPortVertex(x: -1, y: 1, textCoord: CGPoint(x: 0, y: 0)),
|
|
ViewPortVertex(x: 1, y: -1, textCoord: CGPoint(x: 1, y: 1)),
|
|
ViewPortVertex(x: 1, y: 1, textCoord: CGPoint(x: 1, y: 0)),
|
|
]
|
|
}
|
|
|
|
func undoGraphic() {
|
|
guard !strokes.isEmpty else { return }
|
|
let stroke = strokes.removeLast()
|
|
withPersistence(\.backgroundContext) { [stroke] context in
|
|
stroke.stroke(as: PenStroke.self)?.object?.graphicContext = nil
|
|
try context.saveIfNeeded()
|
|
}
|
|
previousStroke = nil
|
|
}
|
|
|
|
func redoGraphic(for event: HistoryEvent) {
|
|
switch event {
|
|
case .stroke(let stroke):
|
|
strokes.append(stroke)
|
|
withPersistence(\.backgroundContext) { [weak self, stroke] context in
|
|
stroke.stroke(as: PenStroke.self)?.object?.graphicContext = self?.object
|
|
try context.saveIfNeeded()
|
|
}
|
|
previousStroke = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
extension GraphicContext {
|
|
func loadStrokes(_ bounds: CGRect) {
|
|
guard let object else { return }
|
|
let queue = OperationQueue()
|
|
queue.qualityOfService = .userInteractive
|
|
self.strokes = object.strokes.compactMap { stroke -> PenStroke? in
|
|
guard let stroke = stroke as? StrokeObject else { return nil }
|
|
let _stroke = PenStroke(object: stroke)
|
|
if _stroke.isVisible(in: bounds) {
|
|
let id = stroke.objectID
|
|
queue.addOperation {
|
|
withPersistenceSync(\.newBackgroundContext) { [_stroke] context in
|
|
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
|
_stroke.loadQuads(from: stroke)
|
|
}
|
|
withPersistence(\.backgroundContext) { [stroke] context in
|
|
context.refresh(stroke, mergeChanges: false)
|
|
}
|
|
}
|
|
} else {
|
|
withPersistence(\.backgroundContext) { [stroke] context in
|
|
_stroke.loadQuads()
|
|
context.refresh(stroke, mergeChanges: false)
|
|
}
|
|
}
|
|
return _stroke
|
|
}
|
|
queue.waitUntilAllOperationsAreFinished()
|
|
}
|
|
|
|
func loadQuads(_ bounds: CGRect) {
|
|
for stroke in self.strokes {
|
|
guard stroke.isVisible(in: bounds), stroke.isEmpty else { continue }
|
|
stroke.stroke(as: PenStroke.self)?.loadQuads()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension GraphicContext: Drawable {
|
|
func prepare(device: MTLDevice) {
|
|
guard vertexBuffer == nil else {
|
|
return
|
|
}
|
|
vertexCount = vertices.count
|
|
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<ViewPortVertex>.stride, options: [])
|
|
}
|
|
|
|
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
|
prepare(device: device)
|
|
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
|
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertices.count)
|
|
}
|
|
}
|
|
|
|
extension GraphicContext {
|
|
func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke {
|
|
let stroke = PenStroke(
|
|
bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
|
|
color: pen.rgba,
|
|
style: pen.strokeStyle,
|
|
createdAt: .now,
|
|
thickness: pen.thickness
|
|
)
|
|
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in
|
|
let stroke = StrokeObject(\.backgroundContext)
|
|
stroke.bounds = _stroke.bounds
|
|
stroke.color = _stroke.color
|
|
stroke.style = _stroke.style.rawValue
|
|
stroke.thickness = _stroke.thickness
|
|
stroke.createdAt = _stroke.createdAt
|
|
stroke.quads = []
|
|
stroke.graphicContext = graphicContext
|
|
graphicContext?.strokes.add(stroke)
|
|
_stroke.object = stroke
|
|
}
|
|
strokes.append(stroke)
|
|
currentStroke = stroke
|
|
currentPoint = point
|
|
currentStroke?.begin(at: point)
|
|
return stroke
|
|
}
|
|
|
|
func appendStroke(with point: CGPoint) {
|
|
guard let currentStroke else { return }
|
|
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.stepRate else {
|
|
return
|
|
}
|
|
currentStroke.append(to: point)
|
|
self.currentPoint = point
|
|
}
|
|
|
|
func endStroke(at point: CGPoint) {
|
|
guard currentPoint != nil, let currentStroke else { return }
|
|
currentStroke.finish(at: point)
|
|
withPersistence(\.backgroundContext) { [currentStroke] context in
|
|
guard let stroke = currentStroke.stroke(as: PenStroke.self) else { return }
|
|
stroke.object?.bounds = stroke.bounds
|
|
try context.saveIfNeeded()
|
|
if let object = stroke.object {
|
|
context.refresh(object, mergeChanges: false)
|
|
}
|
|
}
|
|
previousStroke = currentStroke
|
|
self.currentStroke = nil
|
|
self.currentPoint = nil
|
|
}
|
|
|
|
func cancelStroke() {
|
|
if !strokes.isEmpty {
|
|
let stroke = strokes.removeLast()
|
|
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in
|
|
if let stroke = _stroke.stroke(as: PenStroke.self)?.object {
|
|
graphicContext?.strokes.remove(stroke)
|
|
context.delete(stroke)
|
|
}
|
|
try context.saveIfNeeded()
|
|
}
|
|
}
|
|
currentStroke = nil
|
|
currentPoint = nil
|
|
}
|
|
}
|
|
|
|
extension GraphicContext {
|
|
enum RenderType {
|
|
case inProgress
|
|
case newlyFinished
|
|
case finished
|
|
}
|
|
}
|