refactor: replace stroke class with protocol

This commit is contained in:
dscyrescotti
2024-05-24 20:38:07 +07:00
parent 43d933a1dc
commit 1f9f8ef553
14 changed files with 209 additions and 146 deletions

View File

@@ -77,6 +77,9 @@
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; };
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; };
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; };
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; };
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; };
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15212BEF21F500455818 /* CanvasObject.swift */; };
@@ -159,6 +162,9 @@
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = "<group>"; };
ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = "<group>"; };
ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = "<group>"; };
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = "<group>"; };
ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = "<group>"; };
ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = "<group>"; };
@@ -622,6 +628,7 @@
isa = PBXGroup;
children = (
ECA738D12BE60F7B00A4542E /* PenStroke.swift */,
ECE883BC2C00AA170045C53D /* EraserStroke.swift */,
);
path = Strokes;
sourceTree = "<group>";
@@ -629,7 +636,9 @@
ECE883B92C009DCA0045C53D /* Core */ = {
isa = PBXGroup;
children = (
ECE883BE2C00AB440045C53D /* Stroke.swift */,
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */,
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */,
);
path = Core;
sourceTree = "<group>";
@@ -753,6 +762,7 @@
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */,
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
@@ -778,9 +788,11 @@
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */,
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */,
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,

View File

@@ -118,7 +118,7 @@ extension GraphicContext {
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.rawValue,
style: pen.strokeStyle,
createdAt: .now,
thickness: pen.thickness
)
@@ -126,7 +126,7 @@ extension GraphicContext {
let stroke = StrokeObject(\.backgroundContext)
stroke.bounds = _stroke.bounds
stroke.color = _stroke.color
stroke.style = _stroke.style
stroke.style = _stroke.style.rawValue
stroke.thickness = _stroke.thickness
stroke.createdAt = _stroke.createdAt
stroke.quads = []
@@ -143,7 +143,7 @@ extension GraphicContext {
func appendStroke(with point: CGPoint) {
guard let currentStroke else { return }
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.anyPenStyle.stepRate else {
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.stepRate else {
return
}
currentStroke.append(to: point)
@@ -153,9 +153,7 @@ extension GraphicContext {
func endStroke(at point: CGPoint) {
guard currentPoint != nil, let currentStroke else { return }
currentStroke.finish(at: point)
let batchIndex = currentStroke.batchIndex
let quads = Array(currentStroke.quads[batchIndex..<currentStroke.quads.count])
currentStroke.saveQuads(for: quads)
currentStroke.saveQuads(to: currentStroke.quads.endIndex)
withPersistence(\.backgroundContext) { context in
try context.saveIfNeeded()
if let stroke = currentStroke.object {

View File

@@ -0,0 +1,105 @@
//
// Stroke.swift
// Memola
//
// Created by Dscyre Scotti on 5/24/24.
//
import MetalKit
import Foundation
protocol Stroke: AnyObject, Drawable {
var bounds: [CGFloat] { get set }
var color: [CGFloat] { get set }
var style: StrokeStyle { get set }
var createdAt: Date { get set }
var thickness: CGFloat { get set }
var quads: [Quad] { get set }
var penStyle: any PenStyle { get set }
var batchIndex: Int { get set }
var quadIndex: Int { get set }
var keyPoints: [CGPoint] { get set }
var movingAverage: MovingAverage { get set }
var texture: MTLTexture? { get set }
var indexBuffer: MTLBuffer? { get set }
var vertexBuffer: MTLBuffer? { get set }
func begin(at point: CGPoint)
func append(to point: CGPoint)
func finish(at point: CGPoint)
func loadQuads()
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape)
func removeQuads(from index: Int)
func saveQuads(to index: Int)
}
extension Stroke {
var isEmpty: Bool { quads.isEmpty }
var strokeBounds: CGRect {
let x = bounds[0]
let y = bounds[1]
let width = bounds[2] - x
let height = bounds[3] - y
return CGRect(x: x, y: y, width: width, height: height)
}
func isVisible(in bounds: CGRect) -> Bool {
bounds.contains(strokeBounds) || bounds.intersects(strokeBounds)
}
func begin(at point: CGPoint) {
penStyle.generator.begin(at: point, on: self)
}
func append(to point: CGPoint) {
penStyle.generator.append(to: point, on: self)
}
func finish(at point: CGPoint) {
penStyle.generator.finish(at: point, on: self)
keyPoints.removeAll()
}
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) {
let quad = Quad(
origin: point,
size: thickness,
rotation: rotation,
shape: shape.rawValue,
color: color
)
quads.append(quad)
}
func removeQuads(from index: Int) {
let dropCount = quads.endIndex - max(1, index)
quads.removeLast(dropCount)
}
}
extension Stroke {
func prepare(device: MTLDevice) {
guard texture != nil else { return }
texture = penStyle.loadTexture(on: device)
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
guard !isEmpty, let indexBuffer else { return }
prepare(device: device)
renderEncoder.setFragmentTexture(texture, index: 0)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.drawIndexedPrimitives(
type: .triangle,
indexCount: quads.endIndex * 6,
indexType: .uint32,
indexBuffer: indexBuffer,
indexBufferOffset: 0
)
self.vertexBuffer = nil
self.indexBuffer = nil
}
}

View File

@@ -12,7 +12,7 @@ protocol StrokeGenerator {
var configuration: Configuration { get set }
func begin(at point: CGPoint, on stroke: PenStroke)
func append(to point: CGPoint, on stroke: PenStroke)
func finish(at point: CGPoint, on stroke: PenStroke)
func begin(at point: CGPoint, on stroke: Stroke)
func append(to point: CGPoint, on stroke: Stroke)
func finish(at point: CGPoint, on stroke: Stroke)
}

View File

@@ -0,0 +1,22 @@
//
// StrokeStyle.swift
// Memola
//
// Created by Dscyre Scotti on 5/24/24.
//
import Foundation
enum StrokeStyle: Int16 {
case marker
case eraser
var penStyle: any PenStyle {
switch self {
case .marker:
MarkerPenStyle.marker
case .eraser:
EraserPenStyle.eraser
}
}
}

View File

@@ -10,13 +10,13 @@ import Foundation
struct SolidPointStrokeGenerator: StrokeGenerator {
var configuration: Configuration
func begin(at point: CGPoint, on stroke: PenStroke) {
func begin(at point: CGPoint, on stroke: Stroke) {
let point = stroke.movingAverage.addPoint(point)
stroke.keyPoints.append(point)
addPoint(point, on: stroke)
}
func append(to point: CGPoint, on stroke: PenStroke) {
func append(to point: CGPoint, on stroke: Stroke) {
guard stroke.keyPoints.endIndex > 0 else {
return
}
@@ -49,7 +49,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
}
}
func finish(at point: CGPoint, on stroke: PenStroke) {
func finish(at point: CGPoint, on stroke: Stroke) {
switch stroke.keyPoints.endIndex {
case 0...1:
break
@@ -58,7 +58,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
}
}
private func smoothOutPath(on stroke: PenStroke) {
private func smoothOutPath(on stroke: Stroke) {
stroke.removeQuads(from: stroke.quadIndex + 1)
adjustKeyPoint(on: stroke)
switch stroke.keyPoints.endIndex {
@@ -79,7 +79,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
stroke.quadIndex = stroke.quads.endIndex - 1
}
private func adjustKeyPoint(on stroke: PenStroke) {
private func adjustKeyPoint(on stroke: Stroke) {
let index = stroke.keyPoints.endIndex - 1
let prev = stroke.keyPoints[index - 1]
let current = stroke.keyPoints[index]
@@ -89,7 +89,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
stroke.keyPoints[index] = point
}
private func addPoint(_ point: CGPoint, on stroke: PenStroke) {
private func addPoint(_ point: CGPoint, on stroke: Stroke) {
let rotation: CGFloat
switch configuration.rotation {
case .fixed:
@@ -100,14 +100,14 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
stroke.addQuad(at: point, rotation: rotation, shape: .rounded)
}
private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: PenStroke) {
private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) {
let distance = start.distance(to: end)
let factor: CGFloat
switch configuration.granularity {
case .automatic:
factor = min(5, 1 / (stroke.thickness * 1 / 50))
case .fixed:
factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate)
factor = 1 / (stroke.thickness * stroke.penStyle.stepRate)
case .none:
factor = 1 / (stroke.thickness * 10 / 500)
}

View File

@@ -0,0 +1,13 @@
//
// EraserStroke.swift
// Memola
//
// Created by Dscyre Scotti on 5/24/24.
//
import MetalKit
import Foundation
final class EraserStroke: Stroke, @unchecked Sendable {
}

View File

@@ -9,31 +9,30 @@ import MetalKit
import CoreData
import Foundation
final class PenStroke: @unchecked Sendable {
var object: StrokeObject?
final class PenStroke: Stroke, @unchecked Sendable {
var bounds: [CGFloat]
var color: [CGFloat]
var style: Int16
var style: StrokeStyle
var createdAt: Date
var thickness: CGFloat
var quads: [Quad]
var penStyle: Style
var penStyle: any PenStyle
init(object: StrokeObject) {
self.object = object
self.bounds = object.bounds
self.color = object.color
self.style = object.style
self.createdAt = object.createdAt
self.thickness = object.thickness
self.quads = []
self.penStyle = Style(rawValue: style) ?? .marker
}
var batchIndex: Int = 0
var quadIndex: Int = -1
var keyPoints: [CGPoint] = []
var movingAverage: MovingAverage = MovingAverage(windowSize: 3)
var texture: (any MTLTexture)?
var indexBuffer: (any MTLBuffer)?
var vertexBuffer: (any MTLBuffer)?
var object: StrokeObject?
init(
bounds: [CGFloat],
color: [CGFloat],
style: Int16,
style: StrokeStyle,
createdAt: Date,
thickness: CGFloat,
quads: [Quad] = []
@@ -44,53 +43,24 @@ final class PenStroke: @unchecked Sendable {
self.createdAt = createdAt
self.thickness = thickness
self.quads = quads
self.penStyle = Style(rawValue: style) ?? .marker
self.penStyle = style.penStyle
}
var batchIndex: Int = 0
var quadIndex: Int = -1
var keyPoints: [CGPoint] = []
let movingAverage = MovingAverage(windowSize: 3)
var texture: MTLTexture?
var indexBuffer: MTLBuffer?
var vertexBuffer: MTLBuffer?
var isEmpty: Bool { quads.isEmpty }
var strokeBounds: CGRect {
let x = bounds[0]
let y = bounds[1]
let width = bounds[2] - x
let height = bounds[3] - y
return CGRect(x: x, y: y, width: width, height: height)
convenience init(object: StrokeObject) {
let style = StrokeStyle(rawValue: object.style) ?? .marker
self.init(
bounds: object.bounds,
color: object.color,
style: style,
createdAt: object.createdAt,
thickness: object.thickness
)
self.object = object
}
func isVisible(in bounds: CGRect) -> Bool {
bounds.contains(strokeBounds) || bounds.intersects(strokeBounds)
}
func begin(at point: CGPoint) {
penStyle.anyPenStyle.generator.begin(at: point, on: self)
}
func append(to point: CGPoint) {
penStyle.anyPenStyle.generator.append(to: point, on: self)
}
func finish(at point: CGPoint) {
penStyle.anyPenStyle.generator.finish(at: point, on: self)
keyPoints.removeAll()
}
}
extension PenStroke {
func loadQuads() {
guard let object else { return }
quads = object.quads.compactMap { quad in
guard let quad = quad as? QuadObject else { return nil }
return Quad(object: quad)
}
loadQuads(from: object)
}
func loadQuads(from object: StrokeObject) {
@@ -100,30 +70,19 @@ extension PenStroke {
}
}
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) {
let quad = Quad(
origin: point,
size: thickness,
rotation: rotation,
shape: shape.rawValue,
color: color
)
quads.append(quad)
}
func removeQuads(from index: Int) {
let dropCount = quads.endIndex - max(1, index)
quads.removeLast(dropCount)
let quads = Array(quads[batchIndex..<index])
batchIndex = index
saveQuads(for: quads)
saveQuads(to: index)
}
func saveQuads(for quads: [Quad]) {
var topLeft: CGPoint = CGPoint(x: bounds[0], y: bounds[1])
var bottomRight: CGPoint = CGPoint(x: bounds[2], y: bounds[3])
func saveQuads(to index: Int) {
let quads = Array(quads[batchIndex..<index])
batchIndex = index
withPersistence(\.backgroundContext) { [weak self, object] context in
guard let self else { return }
var topLeft: CGPoint = CGPoint(x: bounds[0], y: bounds[1])
var bottomRight: CGPoint = CGPoint(x: bounds[2], y: bounds[3])
for _quad in quads {
let quad = QuadObject(\.backgroundContext)
quad.originX = _quad.originX.cgFloat
@@ -144,43 +103,3 @@ extension PenStroke {
}
}
}
extension PenStroke: Drawable {
func prepare(device: MTLDevice) {
if texture == nil {
texture = penStyle.anyPenStyle.loadTexture(on: device)
}
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
guard !isEmpty, let indexBuffer else { return }
prepare(device: device)
renderEncoder.setFragmentTexture(texture, index: 0)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.drawIndexedPrimitives(
type: .triangle,
indexCount: quads.endIndex * 6,
indexType: .uint32,
indexBuffer: indexBuffer,
indexBufferOffset: 0
)
self.vertexBuffer = nil
self.indexBuffer = nil
}
}
extension PenStroke {
enum Style: Int16 {
case marker
case eraser
var anyPenStyle: any PenStyle {
switch self {
case .marker:
return MarkerPenStyle.marker
case .eraser:
return EraserPenStyle.eraser
}
}
}
}

View File

@@ -46,7 +46,7 @@ class CacheRenderPass: RenderPass {
let graphicContext = canvas.graphicContext
if let stroke = graphicContext.currentStroke {
switch stroke.penStyle {
switch stroke.style {
case .eraser:
eraserRenderPass.stroke = stroke
eraserRenderPass.descriptor = descriptor

View File

@@ -52,7 +52,7 @@ class GraphicRenderPass: RenderPass {
guard stroke.isVisible(in: canvas.bounds) else { continue }
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
clearsTexture = false
switch stroke.penStyle {
switch stroke.style {
case .eraser:
eraserRenderPass.stroke = stroke
eraserRenderPass.descriptor = descriptor
@@ -71,7 +71,7 @@ class GraphicRenderPass: RenderPass {
if let stroke = graphicContext.previousStroke {
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
clearsTexture = false
switch stroke.penStyle {
switch stroke.style {
case .eraser:
eraserRenderPass.stroke = stroke
eraserRenderPass.descriptor = descriptor

View File

@@ -43,14 +43,14 @@ class Pen: NSObject, ObservableObject, Identifiable {
init(object: PenObject) {
self.object = object
self.id = object.objectID.uriRepresentation().absoluteString
self.style = (PenStroke.Style(rawValue: object.style) ?? .marker).anyPenStyle
self.style = (StrokeStyle(rawValue: object.style) ?? .marker).penStyle
self.rgba = object.color
self.thickness = object.thickness
self.isSelected = object.isSelected
super.init()
}
var strokeStyle: PenStroke.Style {
var strokeStyle: StrokeStyle {
style.strokeStyle
}
}

View File

@@ -16,6 +16,7 @@ protocol PenStyle {
var color: [CGFloat] { get }
var stepRate: CGFloat { get }
var generator: any StrokeGenerator { get }
var strokeStyle: StrokeStyle { get }
}
extension PenStyle {
@@ -23,15 +24,4 @@ extension PenStyle {
func loadTexture(on device: MTLDevice) -> MTLTexture? {
Textures.createPenTexture(with: textureName, on: device)
}
var strokeStyle: PenStroke.Style {
switch self {
case is MarkerPenStyle:
return .marker
case is EraserPenStyle:
return .eraser
default:
return .marker
}
}
}

View File

@@ -23,6 +23,8 @@ struct EraserPenStyle: PenStyle {
var generator: any StrokeGenerator {
SolidPointStrokeGenerator(configuration: .init())
}
var strokeStyle: StrokeStyle { .eraser }
}
extension PenStyle where Self == EraserPenStyle {

View File

@@ -23,6 +23,8 @@ struct MarkerPenStyle: PenStyle {
var generator: any StrokeGenerator {
SolidPointStrokeGenerator(configuration: .init())
}
var strokeStyle: StrokeStyle { .marker }
}
extension PenStyle where Self == MarkerPenStyle {