mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-17 21:27:09 +02:00
360 lines
14 KiB
Swift
360 lines
14 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 tree: RTree = RTree<Element>(maxEntries: 8)
|
|
var eraserStrokes: Set<EraserStroke> = []
|
|
var object: GraphicContextObject?
|
|
|
|
var currentElement: Element?
|
|
var previousElement: Element?
|
|
|
|
var currentPoint: CGPoint?
|
|
var renderType: RenderType = .finished
|
|
var vertices: [ViewPortVertex] = []
|
|
var vertexCount: Int = 4
|
|
var vertexBuffer: MTLBuffer?
|
|
|
|
var erasers: [URL: EraserStroke] = [:]
|
|
|
|
let barrierQueue = DispatchQueue(label: "com.memola.app.graphic-context", attributes: .concurrent)
|
|
|
|
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(for event: HistoryEvent) {
|
|
switch event {
|
|
case .stroke(let stroke):
|
|
switch stroke.style {
|
|
case .marker:
|
|
guard let penStroke = stroke.stroke(as: PenStroke.self) else { return }
|
|
tree.remove(penStroke.element, in: penStroke.strokeBox)
|
|
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
|
penStroke?.object?.element?.graphicContext = nil
|
|
try context.saveIfNeeded()
|
|
context.refreshAllObjects()
|
|
}
|
|
case .eraser:
|
|
guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { return }
|
|
eraserStrokes.remove(eraserStroke)
|
|
withPersistence(\.backgroundContext) { [weak eraserStroke] context in
|
|
guard let eraserStroke else { return }
|
|
for penStroke in eraserStroke.penStrokes.allObjects {
|
|
penStroke.eraserStrokes.remove(eraserStroke)
|
|
if let object = eraserStroke.object {
|
|
penStroke.object?.erasers.remove(object)
|
|
}
|
|
}
|
|
try context.saveIfNeeded()
|
|
context.refreshAllObjects()
|
|
}
|
|
}
|
|
previousElement = nil
|
|
case .photo(let photo):
|
|
tree.remove(photo.element, in: photo.photoBox)
|
|
withPersistence(\.backgroundContext) { [weak photo] context in
|
|
photo?.object?.element?.graphicContext = nil
|
|
try context.saveIfNeeded()
|
|
context.refreshAllObjects()
|
|
}
|
|
}
|
|
}
|
|
|
|
func redoGraphic(for event: HistoryEvent) {
|
|
switch event {
|
|
case .stroke(let stroke):
|
|
switch stroke.style {
|
|
case .marker:
|
|
guard let penStroke = stroke.stroke(as: PenStroke.self) else {
|
|
break
|
|
}
|
|
tree.insert(penStroke.element, in: penStroke.strokeBox)
|
|
withPersistence(\.backgroundContext) { [weak self, weak penStroke] context in
|
|
penStroke?.object?.element?.graphicContext = self?.object
|
|
try context.saveIfNeeded()
|
|
context.refreshAllObjects()
|
|
}
|
|
case .eraser:
|
|
guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else {
|
|
break
|
|
}
|
|
eraserStrokes.insert(eraserStroke)
|
|
withPersistence(\.backgroundContext) { [weak eraserStroke] context in
|
|
guard let eraserStroke else { return }
|
|
for penStroke in eraserStroke.penStrokes.allObjects {
|
|
penStroke.eraserStrokes.insert(eraserStroke)
|
|
if let object = eraserStroke.object {
|
|
penStroke.object?.erasers.add(object)
|
|
}
|
|
}
|
|
try context.saveIfNeeded()
|
|
context.refreshAllObjects()
|
|
}
|
|
}
|
|
previousElement = nil
|
|
case .photo(let photo):
|
|
tree.insert(photo.element, in: photo.photoBox)
|
|
withPersistence(\.backgroundContext) { [weak self, weak photo] context in
|
|
photo?.object?.element?.graphicContext = self?.object
|
|
try context.saveIfNeeded()
|
|
context.refreshAllObjects()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension GraphicContext {
|
|
func loadStrokes(_ bounds: CGRect) {
|
|
guard let object else { return }
|
|
let queue = OperationQueue()
|
|
queue.qualityOfService = .userInteractive
|
|
object.elements.forEach { element in
|
|
guard let element = element as? ElementObject else { return }
|
|
switch element.type {
|
|
case 0:
|
|
guard let stroke = element.stroke, stroke.style == 0 else { return }
|
|
let _stroke = PenStroke(object: stroke)
|
|
tree.insert(_stroke.element, in: _stroke.strokeBox)
|
|
if _stroke.isVisible(in: bounds) {
|
|
let id = stroke.objectID
|
|
queue.addOperation { [weak self] in
|
|
guard let self else { return }
|
|
withPersistenceSync(\.newBackgroundContext) { [weak _stroke] context in
|
|
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
|
_stroke?.loadQuads(from: stroke, with: self)
|
|
context.refreshAllObjects()
|
|
}
|
|
}
|
|
} else {
|
|
withPersistence(\.backgroundContext) { [weak self, weak _stroke] context in
|
|
guard let self else { return }
|
|
_stroke?.loadQuads(with: self)
|
|
context.refreshAllObjects()
|
|
}
|
|
}
|
|
case 1:
|
|
guard let photo = element.photo, photo.imageURL != nil else { return }
|
|
let _photo = Photo(object: photo)
|
|
tree.insert(_photo.element, in: _photo.photoBox)
|
|
default:
|
|
break
|
|
}
|
|
|
|
}
|
|
queue.waitUntilAllOperationsAreFinished()
|
|
}
|
|
|
|
func loadQuads(_ bounds: CGRect, on context: NSManagedObjectContext) {
|
|
for element in self.tree.search(box: bounds.box) {
|
|
guard let stroke = element.stroke(as: PenStroke.self), stroke.isEmpty else { continue }
|
|
stroke.loadQuads(with: self)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// MARK: - Stroke
|
|
extension GraphicContext {
|
|
func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke {
|
|
let stroke: any Stroke
|
|
switch pen.strokeStyle {
|
|
case .marker:
|
|
let penStroke = 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) { [weak graphicContext = object, weak _stroke = penStroke] context in
|
|
guard let _stroke else { return }
|
|
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.erasers = .init()
|
|
let element = ElementObject(\.backgroundContext)
|
|
element.createdAt = _stroke.createdAt
|
|
element.type = 0
|
|
element.graphicContext = graphicContext
|
|
stroke.element = element
|
|
element.stroke = stroke
|
|
graphicContext?.elements.add(element)
|
|
_stroke.object = stroke
|
|
try context.saveIfNeeded()
|
|
}
|
|
stroke = penStroke
|
|
case .eraser:
|
|
let eraserStroke = EraserStroke(
|
|
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
|
|
)
|
|
eraserStroke.graphicContext = self
|
|
withPersistence(\.backgroundContext) { [weak _stroke = eraserStroke] context in
|
|
guard let _stroke else { return }
|
|
let stroke = EraserObject(\.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.strokes = .init()
|
|
_stroke.object = stroke
|
|
try context.saveIfNeeded()
|
|
}
|
|
stroke = eraserStroke
|
|
}
|
|
currentElement = .stroke(stroke.anyStroke)
|
|
currentPoint = point
|
|
currentElement?.stroke()?.begin(at: point)
|
|
return stroke
|
|
}
|
|
|
|
func appendStroke(with point: CGPoint) {
|
|
guard let currentStroke = currentElement?.stroke() 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 = currentElement?.stroke() else { return }
|
|
currentStroke.finish(at: point)
|
|
if let penStroke = currentStroke.stroke(as: PenStroke.self) {
|
|
penStroke.saveQuads()
|
|
tree.insert(currentStroke.element, in: currentStroke.strokeBox)
|
|
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
|
guard let penStroke else { return }
|
|
penStroke.object?.bounds = penStroke.bounds
|
|
try context.saveIfNeeded()
|
|
context.refreshAllObjects()
|
|
}
|
|
} else if let eraserStroke = currentStroke.stroke(as: EraserStroke.self) {
|
|
eraserStroke.saveQuads()
|
|
eraserStrokes.insert(eraserStroke)
|
|
withPersistence(\.backgroundContext) { [weak eraserStroke] context in
|
|
guard let eraserStroke else { return }
|
|
eraserStroke.object?.bounds = eraserStroke.bounds
|
|
try context.saveIfNeeded()
|
|
context.refreshAllObjects()
|
|
}
|
|
}
|
|
previousElement = currentElement
|
|
self.currentElement = nil
|
|
self.currentPoint = nil
|
|
}
|
|
|
|
func cancelStroke() {
|
|
if let stroke = currentElement?.stroke() {
|
|
switch stroke.style {
|
|
case .marker:
|
|
guard let _stroke = stroke.stroke(as: PenStroke.self) else { break }
|
|
withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke] context in
|
|
guard let _stroke else { return }
|
|
if let element = _stroke.object?.element {
|
|
graphicContext?.elements.remove(element)
|
|
}
|
|
try context.saveIfNeeded()
|
|
}
|
|
case .eraser:
|
|
guard let eraserStroke = stroke.stroke(as: EraserStroke.self) else { break }
|
|
eraserStrokes.remove(eraserStroke)
|
|
withPersistence(\.backgroundContext) { [weak eraserStroke] context in
|
|
if let stroke = eraserStroke?.object {
|
|
context.delete(stroke)
|
|
}
|
|
try context.saveIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
currentElement = nil
|
|
currentPoint = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Photo
|
|
extension GraphicContext {
|
|
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) -> Photo {
|
|
let size = photoItem.dimension
|
|
let origin = point
|
|
let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2]
|
|
let photo = Photo(url: photoItem.id, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoItem.bookmark)
|
|
tree.insert(photo.element, in: photo.photoBox)
|
|
withPersistence(\.backgroundContext) { [weak _photo = photo, weak graphicContext = object] context in
|
|
guard let _photo else { return }
|
|
let photo = PhotoObject(\.backgroundContext)
|
|
photo.imageURL = _photo.url
|
|
photo.bounds = _photo.bounds
|
|
photo.width = _photo.size.width
|
|
photo.originY = _photo.origin.y
|
|
photo.originX = _photo.origin.x
|
|
photo.height = _photo.size.height
|
|
photo.createdAt = _photo.createdAt
|
|
photo.bookmark = _photo.bookmark
|
|
let element = ElementObject(\.backgroundContext)
|
|
element.createdAt = _photo.createdAt
|
|
element.type = 1
|
|
element.graphicContext = graphicContext
|
|
photo.element = element
|
|
element.photo = photo
|
|
graphicContext?.elements.add(element)
|
|
_photo.object = photo
|
|
try context.saveIfNeeded()
|
|
}
|
|
self.previousElement = .photo(photo)
|
|
return photo
|
|
}
|
|
}
|
|
|
|
extension GraphicContext {
|
|
enum RenderType {
|
|
case inProgress
|
|
case newlyFinished
|
|
case finished
|
|
}
|
|
}
|