From 1f9f8ef55317d3c824c43f7405f70c40bc3da840 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Fri, 24 May 2024 20:38:07 +0700 Subject: [PATCH] refactor: replace stroke class with protocol --- Memola.xcodeproj/project.pbxproj | 12 ++ Memola/Canvas/Contexts/GraphicContext.swift | 10 +- .../Geometries/Stroke/Core/Stroke.swift | 105 +++++++++++++ .../Stroke/Core/StrokeGenerator.swift | 6 +- .../Geometries/Stroke/Core/StrokeStyle.swift | 22 +++ .../SolidPointStrokeGenerator.swift | 16 +- .../Stroke/Strokes/EraserStroke.swift | 13 ++ .../Geometries/Stroke/Strokes/PenStroke.swift | 145 ++++-------------- .../Canvas/RenderPasses/CacheRenderPass.swift | 2 +- .../RenderPasses/GraphicRenderPass.swift | 4 +- Memola/Canvas/Tool/Pen/Core/Pen.swift | 4 +- Memola/Canvas/Tool/Pen/Core/PenStyle.swift | 12 +- .../Tool/Pen/PenStyles/EraserPenStyle.swift | 2 + .../Tool/Pen/PenStyles/MarkerPenStyle.swift | 2 + 14 files changed, 209 insertions(+), 146 deletions(-) create mode 100644 Memola/Canvas/Geometries/Stroke/Core/Stroke.swift create mode 100644 Memola/Canvas/Geometries/Stroke/Core/StrokeStyle.swift create mode 100644 Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 5420651..43a30be 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -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 = ""; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = ""; }; + ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = ""; }; + ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = ""; }; + ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = ""; }; ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = ""; }; ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = ""; }; ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = ""; }; @@ -622,6 +628,7 @@ isa = PBXGroup; children = ( ECA738D12BE60F7B00A4542E /* PenStroke.swift */, + ECE883BC2C00AA170045C53D /* EraserStroke.swift */, ); path = Strokes; sourceTree = ""; @@ -629,7 +636,9 @@ ECE883B92C009DCA0045C53D /* Core */ = { isa = PBXGroup; children = ( + ECE883BE2C00AB440045C53D /* Stroke.swift */, ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */, + ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */, ); path = Core; sourceTree = ""; @@ -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 */, diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index b048432..774b8ec 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -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.. 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 + } +} diff --git a/Memola/Canvas/Geometries/Stroke/Core/StrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Core/StrokeGenerator.swift index 9486d84..5c540f8 100644 --- a/Memola/Canvas/Geometries/Stroke/Core/StrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Core/StrokeGenerator.swift @@ -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) } diff --git a/Memola/Canvas/Geometries/Stroke/Core/StrokeStyle.swift b/Memola/Canvas/Geometries/Stroke/Core/StrokeStyle.swift new file mode 100644 index 0000000..8615fc8 --- /dev/null +++ b/Memola/Canvas/Geometries/Stroke/Core/StrokeStyle.swift @@ -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 + } + } +} diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index eb13484..9f47bcf 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -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) } diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift new file mode 100644 index 0000000..40a5e57 --- /dev/null +++ b/Memola/Canvas/Geometries/Stroke/Strokes/EraserStroke.swift @@ -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 { + +} diff --git a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift index 5d42e80..43f8573 100644 --- a/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Strokes/PenStroke.swift @@ -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.. 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 - } - } } diff --git a/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift index c8cecc2..4b567e8 100644 --- a/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift +++ b/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift @@ -23,6 +23,8 @@ struct EraserPenStyle: PenStyle { var generator: any StrokeGenerator { SolidPointStrokeGenerator(configuration: .init()) } + + var strokeStyle: StrokeStyle { .eraser } } extension PenStyle where Self == EraserPenStyle { diff --git a/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift index cc7acd0..404d62c 100644 --- a/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift +++ b/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift @@ -23,6 +23,8 @@ struct MarkerPenStyle: PenStyle { var generator: any StrokeGenerator { SolidPointStrokeGenerator(configuration: .init()) } + + var strokeStyle: StrokeStyle { .marker } } extension PenStyle where Self == MarkerPenStyle {