mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-18 13:47:04 +02:00
Merge pull request #11 from dscyrescotti/canvas
Implement canvas rendering
This commit is contained in:
@@ -12,6 +12,56 @@
|
|||||||
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
||||||
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738792BE5EF0400A4542E /* MemosView.swift */; };
|
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738792BE5EF0400A4542E /* MemosView.swift */; };
|
||||||
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7387C2BE5EF4B00A4542E /* MemoView.swift */; };
|
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7387C2BE5EF4B00A4542E /* MemoView.swift */; };
|
||||||
|
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738822BE5FEFE00A4542E /* RenderPass.swift */; };
|
||||||
|
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738852BE5FF2500A4542E /* Canvas.swift */; };
|
||||||
|
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738872BE5FF4400A4542E /* Renderer.swift */; };
|
||||||
|
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738892BE6006A00A4542E /* PipelineStates.swift */; };
|
||||||
|
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7388B2BE6009600A4542E /* Textures.swift */; };
|
||||||
|
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA7388E2BE600DA00A4542E /* Grid.metal */; };
|
||||||
|
ECA738912BE600F500A4542E /* Cache.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738902BE600F500A4542E /* Cache.metal */; };
|
||||||
|
ECA738932BE6011100A4542E /* Stroke.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738922BE6011100A4542E /* Stroke.metal */; };
|
||||||
|
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738942BE6012D00A4542E /* ViewPort.metal */; };
|
||||||
|
ECA738972BE6014200A4542E /* Graphic.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECA738962BE6014200A4542E /* Graphic.metal */; };
|
||||||
|
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389B2BE601AF00A4542E /* GridVertex.swift */; };
|
||||||
|
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389D2BE601CB00A4542E /* QuadVertex.swift */; };
|
||||||
|
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */; };
|
||||||
|
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A22BE6020A00A4542E /* CGFloat++.swift */; };
|
||||||
|
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A52BE6023F00A4542E /* GridUniforms.swift */; };
|
||||||
|
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A72BE6025900A4542E /* GraphicUniforms.swift */; };
|
||||||
|
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A92BE6026D00A4542E /* Uniforms.swift */; };
|
||||||
|
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738AC2BE60CC600A4542E /* DrawingView.swift */; };
|
||||||
|
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */; };
|
||||||
|
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B22BE60D9E00A4542E /* CanvasView.swift */; };
|
||||||
|
ECA738B62BE60DCD00A4542E /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B52BE60DCD00A4542E /* History.swift */; };
|
||||||
|
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */; };
|
||||||
|
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738B92BE60DEF00A4542E /* HistoryAction.swift */; };
|
||||||
|
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738BB2BE60E0300A4542E /* Tool.swift */; };
|
||||||
|
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738BE2BE60E3400A4542E /* Pen.swift */; };
|
||||||
|
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C02BE60E5300A4542E /* PenStyle.swift */; };
|
||||||
|
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */; };
|
||||||
|
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */; };
|
||||||
|
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738C82BE60EF700A4542E /* GraphicContext.swift */; };
|
||||||
|
ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */; };
|
||||||
|
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738CC2BE60F2F00A4542E /* GridContext.swift */; };
|
||||||
|
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D12BE60F7B00A4542E /* Stroke.swift */; };
|
||||||
|
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */; };
|
||||||
|
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */; };
|
||||||
|
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */; };
|
||||||
|
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */; };
|
||||||
|
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */; };
|
||||||
|
ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */; };
|
||||||
|
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */; };
|
||||||
|
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E32BE6110800A4542E /* Drawable.swift */; };
|
||||||
|
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E52BE611FD00A4542E /* CGRect++.swift */; };
|
||||||
|
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E72BE6120F00A4542E /* Color++.swift */; };
|
||||||
|
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E92BE6122E00A4542E /* CGPoint++.swift */; };
|
||||||
|
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */; };
|
||||||
|
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */; };
|
||||||
|
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738EF2BE6127700A4542E /* CGSize++.swift */; };
|
||||||
|
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; };
|
||||||
|
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
|
||||||
|
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
||||||
|
ECA738F82BE612EB00A4542E /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* Quad.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@@ -21,6 +71,56 @@
|
|||||||
EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
ECA738792BE5EF0400A4542E /* MemosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosView.swift; sourceTree = "<group>"; };
|
ECA738792BE5EF0400A4542E /* MemosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosView.swift; sourceTree = "<group>"; };
|
||||||
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoView.swift; sourceTree = "<group>"; };
|
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoView.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738822BE5FEFE00A4542E /* RenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderPass.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738852BE5FF2500A4542E /* Canvas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Canvas.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738872BE5FF4400A4542E /* Renderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738892BE6006A00A4542E /* PipelineStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipelineStates.swift; sourceTree = "<group>"; };
|
||||||
|
ECA7388B2BE6009600A4542E /* Textures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Textures.swift; sourceTree = "<group>"; };
|
||||||
|
ECA7388E2BE600DA00A4542E /* Grid.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Grid.metal; sourceTree = "<group>"; };
|
||||||
|
ECA738902BE600F500A4542E /* Cache.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Cache.metal; sourceTree = "<group>"; };
|
||||||
|
ECA738922BE6011100A4542E /* Stroke.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Stroke.metal; sourceTree = "<group>"; };
|
||||||
|
ECA738942BE6012D00A4542E /* ViewPort.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ViewPort.metal; sourceTree = "<group>"; };
|
||||||
|
ECA738962BE6014200A4542E /* Graphic.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Graphic.metal; sourceTree = "<group>"; };
|
||||||
|
ECA7389B2BE601AF00A4542E /* GridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridVertex.swift; sourceTree = "<group>"; };
|
||||||
|
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadVertex.swift; sourceTree = "<group>"; };
|
||||||
|
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortVertex.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738A22BE6020A00A4542E /* CGFloat++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738A52BE6023F00A4542E /* GridUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridUniforms.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738A72BE6025900A4542E /* GraphicUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicUniforms.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738A92BE6026D00A4542E /* Uniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uniforms.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738AC2BE60CC600A4542E /* DrawingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawingView.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasViewController.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738B22BE60D9E00A4542E /* CanvasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasView.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738B52BE60DCD00A4542E /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEvent.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738B92BE60DEF00A4542E /* HistoryAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryAction.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738BB2BE60E0300A4542E /* Tool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tool.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738BE2BE60E3400A4542E /* Pen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pen.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738C02BE60E5300A4542E /* PenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenStyle.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerPenStyle.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserPenStyle.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738C82BE60EF700A4542E /* GraphicContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContext.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortContext.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738CC2BE60F2F00A4542E /* GridContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridContext.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738D12BE60F7B00A4542E /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeGenerator.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidPointStrokeGenerator.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheRenderPass.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeRenderPass.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortRenderPass.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserRenderPass.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicRenderPass.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738E32BE6110800A4542E /* Drawable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawable.swift; sourceTree = "<group>"; };
|
||||||
|
ECA738E52BE611FD00A4542E /* CGRect++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738E72BE6120F00A4542E /* Color++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738E92BE6122E00A4542E /* CGPoint++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGAffineTransform++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "simd_float4x4++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738EF2BE6127700A4542E /* CGSize++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECA738F72BE612EB00A4542E /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -54,9 +154,11 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ECA738762BE5EE4E00A4542E /* App */,
|
ECA738762BE5EE4E00A4542E /* App */,
|
||||||
|
ECA7387E2BE5FE4200A4542E /* Canvas */,
|
||||||
|
ECA738A12BE601F700A4542E /* Extensions */,
|
||||||
ECA738772BE5EEE800A4542E /* Features */,
|
ECA738772BE5EEE800A4542E /* Features */,
|
||||||
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */,
|
|
||||||
EC7F6BF12BE5E6E400A34A7B /* Preview Content */,
|
EC7F6BF12BE5E6E400A34A7B /* Preview Content */,
|
||||||
|
ECA738802BE5FE6000A4542E /* Resources */,
|
||||||
);
|
);
|
||||||
path = Memola;
|
path = Memola;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -102,6 +204,222 @@
|
|||||||
path = Memo;
|
path = Memo;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738F92BE6130000A4542E /* Geometries */,
|
||||||
|
ECA738812BE5FEEE00A4542E /* Abstracts */,
|
||||||
|
ECA738992BE6018900A4542E /* Buffers */,
|
||||||
|
ECA738C72BE60EE200A4542E /* Contexts */,
|
||||||
|
ECA738842BE5FF1B00A4542E /* Core */,
|
||||||
|
ECA738B42BE60DC200A4542E /* History */,
|
||||||
|
ECA738D82BE60FE200A4542E /* RenderPasses */,
|
||||||
|
ECA7388D2BE600BB00A4542E /* Shaders */,
|
||||||
|
ECA738B12BE60D8800A4542E /* Tool */,
|
||||||
|
ECA738AB2BE60CB500A4542E /* View */,
|
||||||
|
);
|
||||||
|
path = Canvas;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738802BE5FE6000A4542E /* Resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */,
|
||||||
|
);
|
||||||
|
path = Resources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738812BE5FEEE00A4542E /* Abstracts */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738822BE5FEFE00A4542E /* RenderPass.swift */,
|
||||||
|
ECA738E32BE6110800A4542E /* Drawable.swift */,
|
||||||
|
);
|
||||||
|
path = Abstracts;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738842BE5FF1B00A4542E /* Core */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738852BE5FF2500A4542E /* Canvas.swift */,
|
||||||
|
ECA738892BE6006A00A4542E /* PipelineStates.swift */,
|
||||||
|
ECA738872BE5FF4400A4542E /* Renderer.swift */,
|
||||||
|
ECA7388B2BE6009600A4542E /* Textures.swift */,
|
||||||
|
);
|
||||||
|
path = Core;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA7388D2BE600BB00A4542E /* Shaders */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA7388E2BE600DA00A4542E /* Grid.metal */,
|
||||||
|
ECA738902BE600F500A4542E /* Cache.metal */,
|
||||||
|
ECA738922BE6011100A4542E /* Stroke.metal */,
|
||||||
|
ECA738942BE6012D00A4542E /* ViewPort.metal */,
|
||||||
|
ECA738962BE6014200A4542E /* Graphic.metal */,
|
||||||
|
);
|
||||||
|
path = Shaders;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738982BE6015700A4542E /* Primitives */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738F72BE612EB00A4542E /* Quad.swift */,
|
||||||
|
);
|
||||||
|
path = Primitives;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738992BE6018900A4542E /* Buffers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738A42BE6022F00A4542E /* Uniforms */,
|
||||||
|
ECA7389A2BE6019700A4542E /* Vertices */,
|
||||||
|
);
|
||||||
|
path = Buffers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA7389A2BE6019700A4542E /* Vertices */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA7389B2BE601AF00A4542E /* GridVertex.swift */,
|
||||||
|
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */,
|
||||||
|
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */,
|
||||||
|
);
|
||||||
|
path = Vertices;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738A12BE601F700A4542E /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738F32BE612A000A4542E /* Array++.swift */,
|
||||||
|
ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */,
|
||||||
|
ECA738A22BE6020A00A4542E /* CGFloat++.swift */,
|
||||||
|
ECA738E92BE6122E00A4542E /* CGPoint++.swift */,
|
||||||
|
ECA738E52BE611FD00A4542E /* CGRect++.swift */,
|
||||||
|
ECA738EF2BE6127700A4542E /* CGSize++.swift */,
|
||||||
|
ECA738F12BE6128F00A4542E /* Collection++.swift */,
|
||||||
|
ECA738E72BE6120F00A4542E /* Color++.swift */,
|
||||||
|
ECA738F52BE612B700A4542E /* MTLDevice++.swift */,
|
||||||
|
ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738A42BE6022F00A4542E /* Uniforms */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738A52BE6023F00A4542E /* GridUniforms.swift */,
|
||||||
|
ECA738A72BE6025900A4542E /* GraphicUniforms.swift */,
|
||||||
|
ECA738A92BE6026D00A4542E /* Uniforms.swift */,
|
||||||
|
);
|
||||||
|
path = Uniforms;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738AB2BE60CB500A4542E /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738AE2BE60CEC00A4542E /* Bridge */,
|
||||||
|
ECA738B22BE60D9E00A4542E /* CanvasView.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738AE2BE60CEC00A4542E /* Bridge */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738AC2BE60CC600A4542E /* DrawingView.swift */,
|
||||||
|
ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Bridge;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738B12BE60D8800A4542E /* Tool */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738C22BE60E7200A4542E /* PenStyles */,
|
||||||
|
ECA738BD2BE60E2800A4542E /* Pen */,
|
||||||
|
ECA738BB2BE60E0300A4542E /* Tool.swift */,
|
||||||
|
);
|
||||||
|
path = Tool;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738B42BE60DC200A4542E /* History */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738B52BE60DCD00A4542E /* History.swift */,
|
||||||
|
ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */,
|
||||||
|
ECA738B92BE60DEF00A4542E /* HistoryAction.swift */,
|
||||||
|
);
|
||||||
|
path = History;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738BD2BE60E2800A4542E /* Pen */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738BE2BE60E3400A4542E /* Pen.swift */,
|
||||||
|
ECA738C02BE60E5300A4542E /* PenStyle.swift */,
|
||||||
|
);
|
||||||
|
path = Pen;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738C22BE60E7200A4542E /* PenStyles */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */,
|
||||||
|
ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */,
|
||||||
|
);
|
||||||
|
path = PenStyles;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738C72BE60EE200A4542E /* Contexts */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738C82BE60EF700A4542E /* GraphicContext.swift */,
|
||||||
|
ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */,
|
||||||
|
ECA738CC2BE60F2F00A4542E /* GridContext.swift */,
|
||||||
|
);
|
||||||
|
path = Contexts;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738CE2BE60F5000A4542E /* Stroke */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738D52BE60FA200A4542E /* Generators */,
|
||||||
|
ECA738D12BE60F7B00A4542E /* Stroke.swift */,
|
||||||
|
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */,
|
||||||
|
);
|
||||||
|
path = Stroke;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738D52BE60FA200A4542E /* Generators */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */,
|
||||||
|
);
|
||||||
|
path = Generators;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738D82BE60FE200A4542E /* RenderPasses */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */,
|
||||||
|
ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */,
|
||||||
|
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */,
|
||||||
|
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */,
|
||||||
|
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
|
||||||
|
);
|
||||||
|
path = RenderPasses;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECA738F92BE6130000A4542E /* Geometries */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECA738CE2BE60F5000A4542E /* Stroke */,
|
||||||
|
ECA738982BE6015700A4542E /* Primitives */,
|
||||||
|
);
|
||||||
|
path = Geometries;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -172,9 +490,59 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */,
|
||||||
|
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
|
||||||
|
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
|
||||||
|
ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */,
|
||||||
|
ECA738912BE600F500A4542E /* Cache.metal in Sources */,
|
||||||
|
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
|
||||||
|
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
|
||||||
|
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
|
||||||
|
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
|
||||||
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
|
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
|
||||||
|
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
|
||||||
|
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
||||||
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
|
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
|
||||||
|
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
|
||||||
|
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
|
||||||
|
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
|
||||||
|
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
|
||||||
|
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
||||||
|
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
|
||||||
|
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
||||||
|
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
||||||
|
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
|
||||||
|
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
|
||||||
|
ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */,
|
||||||
|
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */,
|
||||||
|
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */,
|
||||||
|
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
|
||||||
|
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
||||||
|
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
||||||
|
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
|
||||||
|
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
|
||||||
|
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
|
||||||
|
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
||||||
|
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||||
|
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||||
|
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
||||||
|
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
|
||||||
|
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
||||||
|
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
||||||
|
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */,
|
||||||
|
ECA738932BE6011100A4542E /* Stroke.metal in Sources */,
|
||||||
|
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
|
||||||
|
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
|
||||||
|
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */,
|
||||||
|
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */,
|
||||||
|
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
|
||||||
|
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
|
||||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||||
|
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||||
|
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||||
|
ECA738F82BE612EB00A4542E /* Quad.swift in Sources */,
|
||||||
|
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||||
|
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// Drawable.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol Drawable: AnyObject {
|
||||||
|
func prepare(device: MTLDevice)
|
||||||
|
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// RenderPass.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol RenderPass {
|
||||||
|
var label: String { get }
|
||||||
|
var descriptor: MTLRenderPassDescriptor? { get set }
|
||||||
|
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer)
|
||||||
|
func draw(on canvas: Canvas, with renderer: Renderer)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// GraphicUniforms.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GraphicUniforms {
|
||||||
|
var color: vector_float4
|
||||||
|
|
||||||
|
init(color: [CGFloat]) {
|
||||||
|
self.color = [color[0].float, color[1].float, color[2].float, color[3].float]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// GridUniforms.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GridUniforms {
|
||||||
|
var ratio: Float
|
||||||
|
var zoom: Float
|
||||||
|
var transform: simd_float4x4
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// Uniforms.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
var transform: simd_float4x4
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// GridVertex.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GridVertex {
|
||||||
|
var position: vector_float4
|
||||||
|
var pointSize: Float = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GridVertex {
|
||||||
|
init(x: CGFloat, y: CGFloat) {
|
||||||
|
self.position = [x.float, y.float, 0, 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// QuadVertex.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct QuadVertex: Codable {
|
||||||
|
var position: vector_float4
|
||||||
|
var textCoord: vector_float2
|
||||||
|
var color: vector_float4
|
||||||
|
var origin: vector_float2
|
||||||
|
var rotation: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
extension QuadVertex {
|
||||||
|
init(x: CGFloat, y: CGFloat, textCoord: CGPoint, color: [CGFloat], origin: CGPoint, rotation: CGFloat) {
|
||||||
|
self.position = [x.float, y.float, 0, 1]
|
||||||
|
self.textCoord = [textCoord.x.float, textCoord.y.float]
|
||||||
|
self.color = [color[0].float, color[1].float, color[2].float, color[3].float]
|
||||||
|
self.origin = [origin.x.float, origin.y.float]
|
||||||
|
self.rotation = rotation.float
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrigin() -> CGPoint {
|
||||||
|
CGPoint(x: CGFloat(origin[0]), y: CGFloat(origin[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// ViewPortVertex.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ViewPortVertex {
|
||||||
|
var position: vector_float4
|
||||||
|
var textCoord: vector_float2
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ViewPortVertex {
|
||||||
|
init(x: CGFloat, y: CGFloat, textCoord: CGPoint) {
|
||||||
|
self.position = [x.float, y.float, 0, 1]
|
||||||
|
self.textCoord = [textCoord.x.float, textCoord.y.float]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
//
|
||||||
|
// GraphicContext.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol GraphicContextDelegate: AnyObject {
|
||||||
|
var didUpdate: PassthroughSubject<Void, Never> { get set }
|
||||||
|
}
|
||||||
|
|
||||||
|
class GraphicContext: Codable {
|
||||||
|
var strokes: [Stroke] = []
|
||||||
|
var currentStroke: Stroke?
|
||||||
|
var previousStroke: Stroke?
|
||||||
|
var currentPoint: CGPoint?
|
||||||
|
|
||||||
|
var renderType: RenderType = .finished
|
||||||
|
|
||||||
|
var vertices: [ViewPortVertex] = []
|
||||||
|
var vertexCount: Int = 4
|
||||||
|
var vertexBuffer: MTLBuffer?
|
||||||
|
|
||||||
|
weak var delegate: GraphicContextDelegate?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
setViewPortVertices()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case strokes
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.strokes = try container.decode([Stroke].self, forKey: .strokes)
|
||||||
|
setViewPortVertices()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.strokes, forKey: .strokes)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
strokes.removeLast()
|
||||||
|
previousStroke = nil
|
||||||
|
delegate?.didUpdate.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
func redoGraphic(for event: HistoryEvent) {
|
||||||
|
switch event {
|
||||||
|
case .stroke(let stroke):
|
||||||
|
strokes.append(stroke)
|
||||||
|
previousStroke = nil
|
||||||
|
}
|
||||||
|
delegate?.didUpdate.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) -> Stroke {
|
||||||
|
let stroke = Stroke(
|
||||||
|
color: pen.color,
|
||||||
|
style: pen.style,
|
||||||
|
thickness: pen.thickness
|
||||||
|
)
|
||||||
|
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.style.stepRate else { return }
|
||||||
|
currentStroke.append(to: point)
|
||||||
|
self.currentPoint = point
|
||||||
|
}
|
||||||
|
|
||||||
|
func endStroke(at point: CGPoint) {
|
||||||
|
guard currentPoint != nil else { return }
|
||||||
|
currentStroke?.finish(at: point)
|
||||||
|
previousStroke = currentStroke
|
||||||
|
currentStroke = nil
|
||||||
|
self.currentPoint = nil
|
||||||
|
delegate?.didUpdate.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GraphicContext {
|
||||||
|
enum RenderType {
|
||||||
|
case inProgress
|
||||||
|
case newlyFinished
|
||||||
|
case finished
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// GridContext.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class GridContext {
|
||||||
|
var vertices: [GridVertex] = []
|
||||||
|
var vertexCount: Int = 0
|
||||||
|
var vertexBuffer: MTLBuffer?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
generateVertices()
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateVertices() {
|
||||||
|
let steps = stride(from: -10, through: 110, by: 0.25)
|
||||||
|
for y in steps {
|
||||||
|
for x in steps {
|
||||||
|
vertices.append(GridVertex(x: CGFloat(x), y: CGFloat(y)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vertexCount = vertices.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GridContext: Drawable {
|
||||||
|
func prepare(device: MTLDevice) {
|
||||||
|
guard vertexBuffer == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<GridVertex>.stride, options: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
guard vertexCount > .zero else { return }
|
||||||
|
prepare(device: device)
|
||||||
|
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||||
|
renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: vertexCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// ViewPortContext.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ViewPortContext {
|
||||||
|
var vertices: [ViewPortVertex] = []
|
||||||
|
let vertexCount: Int = 4
|
||||||
|
var vertexBuffer: MTLBuffer?
|
||||||
|
|
||||||
|
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 setViewPortUpdateVertices(from bounds: CGRect) {
|
||||||
|
vertexBuffer = nil
|
||||||
|
vertices = [
|
||||||
|
ViewPortVertex(x: bounds.minX, y: bounds.minY, textCoord: CGPoint(x: 0, y: 0)),
|
||||||
|
ViewPortVertex(x: bounds.minX, y: bounds.maxY, textCoord: CGPoint(x: 0, y: 1)),
|
||||||
|
ViewPortVertex(x: bounds.maxX, y: bounds.minY, textCoord: CGPoint(x: 1, y: 0)),
|
||||||
|
ViewPortVertex(x: bounds.maxX, y: bounds.maxY, textCoord: CGPoint(x: 1, y: 1)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ViewPortContext: Drawable {
|
||||||
|
func prepare(device: MTLDevice) {
|
||||||
|
guard vertexBuffer == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
//
|
||||||
|
// Canvas.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicContextDelegate {
|
||||||
|
let size: CGSize
|
||||||
|
let maximumZoomScale: CGFloat = 30
|
||||||
|
let minimumZoomScale: CGFloat = 3.1
|
||||||
|
|
||||||
|
var transform: simd_float4x4 = .init()
|
||||||
|
|
||||||
|
var uniformsBuffer: MTLBuffer?
|
||||||
|
|
||||||
|
let gridContext = GridContext()
|
||||||
|
var graphicContext = GraphicContext()
|
||||||
|
let viewPortContext = ViewPortContext()
|
||||||
|
|
||||||
|
var clipBounds: CGRect = .zero
|
||||||
|
var zoomScale: CGFloat = .zero
|
||||||
|
|
||||||
|
// weak var board: BoardObject?
|
||||||
|
var graphicLoader: (() throws -> GraphicContext)?
|
||||||
|
|
||||||
|
@Published var state: State = .initial
|
||||||
|
lazy var didUpdate = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
init(size: CGSize = .init(width: 8_000, height: 8_000)) {
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case size
|
||||||
|
case graphicContext
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.size = try container.decode(CGSize.self, forKey: .size)
|
||||||
|
self.graphicLoader = { try container.decode(GraphicContext.self, forKey: .graphicContext) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
extension Canvas {
|
||||||
|
func load() {
|
||||||
|
guard let graphicLoader else { return }
|
||||||
|
Task(priority: .high) { [unowned self, graphicLoader] in
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .loading
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let graphicContext = try graphicLoader()
|
||||||
|
graphicContext.delegate = self
|
||||||
|
await MainActor.run {
|
||||||
|
self.graphicContext = graphicContext
|
||||||
|
self.state = .loaded
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
NSLog("[SketchNote] - \(error.localizedDescription)")
|
||||||
|
await MainActor.run {
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(on managedObjectContext: NSManagedObjectContext) async {
|
||||||
|
// guard let board else { return }
|
||||||
|
// do {
|
||||||
|
// board.data = try JSONEncoder().encode(self)
|
||||||
|
// board.updatedAt = Date()
|
||||||
|
// try managedObjectContext.save()
|
||||||
|
// } catch {
|
||||||
|
// NSLog("[SketchNote] - \(error.localizedDescription)")
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
func listen(on managedObjectContext: NSManagedObjectContext) {
|
||||||
|
Task(priority: .background) { [unowned self] in
|
||||||
|
for await _ in didUpdate.values {
|
||||||
|
await save(on: managedObjectContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Dimension
|
||||||
|
extension Canvas {
|
||||||
|
func updateTransform(on drawingView: DrawingView) {
|
||||||
|
let bounds = CGRect(origin: .zero, size: size)
|
||||||
|
let renderView = drawingView.renderView
|
||||||
|
let targetRect = drawingView.convert(drawingView.bounds, to: renderView)
|
||||||
|
let transform1 = bounds.transform(to: targetRect)
|
||||||
|
let transform2 = renderView.bounds.transform(to: CGRect(x: -1.0, y: -1.0, width: 2.0, height: 2.0))
|
||||||
|
let transform3 = CGAffineTransform.identity.translatedBy(x: 0, y: 1).scaledBy(x: 1, y: -1).translatedBy(x: 0, y: 1)
|
||||||
|
self.transform = simd_float4x4(transform1 * transform2 * transform3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateClipBounds(_ scrollView: UIScrollView, on drawingView: DrawingView) {
|
||||||
|
let ratio = drawingView.ratio
|
||||||
|
let bounds = scrollView.convert(scrollView.bounds, to: drawingView)
|
||||||
|
clipBounds = CGRect(origin: bounds.origin.muliply(by: ratio), size: bounds.size.multiply(by: ratio))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Zoom Scale
|
||||||
|
extension Canvas {
|
||||||
|
func setZoomScale(_ zoomScale: CGFloat) {
|
||||||
|
self.zoomScale = zoomScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Graphic Context
|
||||||
|
extension Canvas {
|
||||||
|
func beginTouch(at point: CGPoint, pen: Pen) -> Stroke {
|
||||||
|
graphicContext.beginStroke(at: point, pen: pen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveTouch(to point: CGPoint) {
|
||||||
|
graphicContext.appendStroke(with: point)
|
||||||
|
}
|
||||||
|
|
||||||
|
func endTouch(at point: CGPoint) {
|
||||||
|
graphicContext.endStroke(at: point)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setGraphicRenderType(_ renderType: GraphicContext.RenderType) {
|
||||||
|
graphicContext.renderType = renderType
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNewlyAddedStroke() -> Stroke? {
|
||||||
|
graphicContext.strokes.last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Rendering
|
||||||
|
extension Canvas {
|
||||||
|
func renderGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
var uniforms = GridUniforms(
|
||||||
|
ratio: size.width.float / 100,
|
||||||
|
zoom: zoomScale.float,
|
||||||
|
transform: transform
|
||||||
|
)
|
||||||
|
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<GridUniforms>.size)
|
||||||
|
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||||
|
gridContext.draw(device: device, renderEncoder: renderEncoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderGraphic(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
graphicContext.draw(device: device, renderEncoder: renderEncoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderViewPort(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
viewPortContext.setViewPortVertices()
|
||||||
|
viewPortContext.draw(device: device, renderEncoder: renderEncoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderViewPortUpdate(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
var uniforms = Uniforms(transform: transform)
|
||||||
|
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
||||||
|
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||||
|
viewPortContext.setViewPortUpdateVertices(from: clipBounds)
|
||||||
|
viewPortContext.draw(device: device, renderEncoder: renderEncoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUniformsBuffer(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
var uniforms = Uniforms(transform: transform)
|
||||||
|
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
||||||
|
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - State
|
||||||
|
extension Canvas {
|
||||||
|
enum State {
|
||||||
|
case initial
|
||||||
|
case loading
|
||||||
|
case loaded
|
||||||
|
case closing
|
||||||
|
case closed
|
||||||
|
case failed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// PipelineStates.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PipelineStates {
|
||||||
|
static func createGridPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||||
|
let device = renderer.device
|
||||||
|
let library = renderer.library
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_grid")
|
||||||
|
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_grid")
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||||
|
pipelineDescriptor.label = "Grid Pipeline State"
|
||||||
|
|
||||||
|
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||||
|
attachment?.isBlendingEnabled = true
|
||||||
|
attachment?.rgbBlendOperation = .add
|
||||||
|
attachment?.sourceRGBBlendFactor = .one
|
||||||
|
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||||
|
attachment?.alphaBlendOperation = .add
|
||||||
|
attachment?.sourceAlphaBlendFactor = .sourceAlpha
|
||||||
|
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||||
|
|
||||||
|
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createGraphicPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||||
|
let device = renderer.device
|
||||||
|
let library = renderer.library
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_graphic")
|
||||||
|
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_graphic")
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||||
|
pipelineDescriptor.label = "Graphic Pipeline State"
|
||||||
|
|
||||||
|
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||||
|
attachment?.isBlendingEnabled = true
|
||||||
|
attachment?.rgbBlendOperation = .add
|
||||||
|
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||||
|
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||||
|
attachment?.alphaBlendOperation = .add
|
||||||
|
attachment?.sourceAlphaBlendFactor = .one
|
||||||
|
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||||
|
|
||||||
|
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createStrokePipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||||
|
let device = renderer.device
|
||||||
|
let library = renderer.library
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_stroke")
|
||||||
|
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_stroke")
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||||
|
pipelineDescriptor.label = "Stroke Pipeline State"
|
||||||
|
|
||||||
|
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||||
|
attachment?.isBlendingEnabled = true
|
||||||
|
attachment?.rgbBlendOperation = .add
|
||||||
|
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||||
|
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||||
|
attachment?.alphaBlendOperation = .add
|
||||||
|
attachment?.sourceAlphaBlendFactor = .one
|
||||||
|
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||||
|
|
||||||
|
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createEraserPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||||
|
let device = renderer.device
|
||||||
|
let library = renderer.library
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_stroke")
|
||||||
|
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_stroke")
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||||
|
pipelineDescriptor.label = "Eraser Pipeline State"
|
||||||
|
|
||||||
|
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||||
|
attachment?.isBlendingEnabled = true
|
||||||
|
attachment?.rgbBlendOperation = .add
|
||||||
|
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||||
|
attachment?.destinationRGBBlendFactor = .one
|
||||||
|
attachment?.alphaBlendOperation = .reverseSubtract
|
||||||
|
attachment?.sourceAlphaBlendFactor = .sourceAlpha
|
||||||
|
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||||
|
|
||||||
|
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createViewPortPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil, isUpdate: Bool = false) -> MTLRenderPipelineState? {
|
||||||
|
var label: String
|
||||||
|
var vertexName: String
|
||||||
|
if isUpdate {
|
||||||
|
label = "View Port Update Pipeline State"
|
||||||
|
vertexName = "vertex_viewport_update"
|
||||||
|
} else {
|
||||||
|
label = "View Port Pipeline State"
|
||||||
|
vertexName = "vertex_viewport"
|
||||||
|
}
|
||||||
|
let device = renderer.device
|
||||||
|
let library = renderer.library
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineDescriptor.vertexFunction = library.makeFunction(name: vertexName)
|
||||||
|
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_viewport")
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||||
|
pipelineDescriptor.label = label
|
||||||
|
|
||||||
|
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||||
|
attachment?.isBlendingEnabled = true
|
||||||
|
attachment?.rgbBlendOperation = .add
|
||||||
|
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||||
|
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||||
|
attachment?.alphaBlendOperation = .add
|
||||||
|
attachment?.sourceAlphaBlendFactor = .one
|
||||||
|
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||||
|
|
||||||
|
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createCachePipelineState(from renderer: Renderer) -> MTLComputePipelineState? {
|
||||||
|
let device = renderer.device
|
||||||
|
let library = renderer.library
|
||||||
|
guard let function = library.makeFunction(name: "copy_texture_viewport") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return try? device.makeComputePipelineState(function: function)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// Renderer.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class Renderer {
|
||||||
|
var device: MTLDevice
|
||||||
|
var library: MTLLibrary
|
||||||
|
var pixelFormat: MTLPixelFormat
|
||||||
|
var commandQueue: MTLCommandQueue
|
||||||
|
|
||||||
|
var redrawsGraphicRender: Bool = true
|
||||||
|
var updatesViewPort: Bool = false
|
||||||
|
|
||||||
|
var canvasView: MTKView
|
||||||
|
|
||||||
|
lazy var strokeRenderPass: StrokeRenderPass = {
|
||||||
|
StrokeRenderPass(renderer: self)
|
||||||
|
}()
|
||||||
|
lazy var eraserRenderPass: EraserRenderPass = {
|
||||||
|
EraserRenderPass(renderer: self)
|
||||||
|
}()
|
||||||
|
lazy var graphicRenderPass: GraphicRenderPass = {
|
||||||
|
GraphicRenderPass(renderer: self)
|
||||||
|
}()
|
||||||
|
lazy var cacheRenderPass: CacheRenderPass = {
|
||||||
|
CacheRenderPass(renderer: self)
|
||||||
|
}()
|
||||||
|
lazy var viewPortRenderPass: ViewPortRenderPass = {
|
||||||
|
ViewPortRenderPass(renderer: self)
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(canvasView: MTKView) {
|
||||||
|
guard let device = MTLCreateSystemDefaultDevice() else {
|
||||||
|
fatalError("[Error]: Unable to create system default device.")
|
||||||
|
}
|
||||||
|
guard let commandQueue = device.makeCommandQueue() else {
|
||||||
|
fatalError("[Error]: Unable to create command queue.")
|
||||||
|
}
|
||||||
|
guard let library = device.makeDefaultLibrary() else {
|
||||||
|
fatalError("[Error]: Unable to create default library.")
|
||||||
|
}
|
||||||
|
self.device = device
|
||||||
|
self.commandQueue = commandQueue
|
||||||
|
self.library = library
|
||||||
|
self.pixelFormat = canvasView.colorPixelFormat
|
||||||
|
self.canvasView = canvasView
|
||||||
|
canvasView.device = device
|
||||||
|
self.viewPortRenderPass.view = canvasView
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(on view: MTKView, to size: CGSize) {
|
||||||
|
if !updatesViewPort {
|
||||||
|
strokeRenderPass.resize(on: view, to: size, with: self)
|
||||||
|
graphicRenderPass.resize(on: view, to: size, with: self)
|
||||||
|
cacheRenderPass.resize(on: view, to: size, with: self)
|
||||||
|
}
|
||||||
|
viewPortRenderPass.resize(on: view, to: size, with: self)
|
||||||
|
redrawsGraphicRender = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw(in view: MTKView, on canvas: Canvas) {
|
||||||
|
if !updatesViewPort {
|
||||||
|
graphicRenderPass.strokeRenderPass = strokeRenderPass
|
||||||
|
graphicRenderPass.eraserRenderPass = eraserRenderPass
|
||||||
|
graphicRenderPass.draw(on: canvas, with: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture
|
||||||
|
cacheRenderPass.strokeRenderPass = strokeRenderPass
|
||||||
|
cacheRenderPass.eraserRenderPass = eraserRenderPass
|
||||||
|
cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture
|
||||||
|
cacheRenderPass.graphicPipelineState = graphicRenderPass.graphicPipelineState
|
||||||
|
cacheRenderPass.draw(on: canvas, with: self)
|
||||||
|
|
||||||
|
viewPortRenderPass.descriptor = view.currentRenderPassDescriptor
|
||||||
|
viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture
|
||||||
|
viewPortRenderPass.draw(on: canvas, with: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// Textures.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Textures {
|
||||||
|
static var penTextures: [String: MTLTexture] = [:]
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func createPenTexture(with textureName: String, on device: MTLDevice) -> MTLTexture? {
|
||||||
|
if let penTexture = penTextures[textureName] {
|
||||||
|
return penTexture
|
||||||
|
}
|
||||||
|
let textureLoader = MTKTextureLoader(device: device)
|
||||||
|
let penTexture = try? textureLoader.newTexture(name: textureName, scaleFactor: 1.0, bundle: .main, options: [.SRGB: false])
|
||||||
|
penTextures[textureName] = penTexture
|
||||||
|
return penTexture
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createGraphicTexture(
|
||||||
|
from renderer: Renderer,
|
||||||
|
size: CGSize,
|
||||||
|
pixelFormat: MTLPixelFormat? = nil
|
||||||
|
) -> MTLTexture? {
|
||||||
|
let width = Int(size.width)
|
||||||
|
let height = Int(size.height)
|
||||||
|
guard width > 0, height > 0 else { return nil }
|
||||||
|
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||||
|
pixelFormat: pixelFormat ?? renderer.pixelFormat,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
mipmapped: false
|
||||||
|
)
|
||||||
|
descriptor.storageMode = .shared
|
||||||
|
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
|
||||||
|
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
texture.label = "Graphic Texture"
|
||||||
|
return texture
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createCacheTexture(
|
||||||
|
from renderer: Renderer,
|
||||||
|
size: CGSize,
|
||||||
|
pixelFormat: MTLPixelFormat? = nil
|
||||||
|
) -> MTLTexture? {
|
||||||
|
let width = Int(size.width)
|
||||||
|
let height = Int(size.height)
|
||||||
|
guard width > 0, height > 0 else { return nil }
|
||||||
|
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||||
|
pixelFormat: pixelFormat ?? renderer.pixelFormat,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
mipmapped: false
|
||||||
|
)
|
||||||
|
descriptor.storageMode = .shared
|
||||||
|
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
|
||||||
|
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
texture.label = "Cache Texture"
|
||||||
|
return texture
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createStrokeTexture(
|
||||||
|
from renderer: Renderer,
|
||||||
|
size: CGSize,
|
||||||
|
pixelFormat: MTLPixelFormat? = nil
|
||||||
|
) -> MTLTexture? {
|
||||||
|
let width = Int(size.width)
|
||||||
|
let height = Int(size.height)
|
||||||
|
guard width > 0, height > 0 else { return nil }
|
||||||
|
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||||
|
pixelFormat: pixelFormat ?? renderer.pixelFormat,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
mipmapped: false
|
||||||
|
)
|
||||||
|
descriptor.storageMode = .shared
|
||||||
|
descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite]
|
||||||
|
guard let texture = renderer.device.makeTexture(descriptor: descriptor) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
texture.label = "Stroke Texture"
|
||||||
|
return texture
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
//
|
||||||
|
// Quad.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Quad {
|
||||||
|
var origin: CGPoint
|
||||||
|
var color: [CGFloat]
|
||||||
|
var size: CGFloat
|
||||||
|
var rotation: CGFloat
|
||||||
|
var vertices: [QuadVertex] = []
|
||||||
|
|
||||||
|
var vertexBuffer: MTLBuffer?
|
||||||
|
var vertexCount: Int = 0
|
||||||
|
|
||||||
|
init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: Shape = .rounded) {
|
||||||
|
self.origin = origin
|
||||||
|
self.size = size
|
||||||
|
self.color = color
|
||||||
|
self.rotation = rotation
|
||||||
|
generateVertices(shape)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateVertices(_ shape: Shape) {
|
||||||
|
switch shape {
|
||||||
|
case .rounded:
|
||||||
|
generateRoundedQuad()
|
||||||
|
case .squared:
|
||||||
|
generateSquaredQuad()
|
||||||
|
case let .calligraphic(vFactor, hFactor):
|
||||||
|
generateCalligraphicQuad(vFactor: vFactor, hFactor: hFactor)
|
||||||
|
case let .trapezoid(topFactor, bottomFactor, heightFactor):
|
||||||
|
generateTrapezoidQuad(topFactor: topFactor, bottomFactor: bottomFactor, heightFactor: heightFactor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRoundedQuad() {
|
||||||
|
let halfSize = size * 0.5
|
||||||
|
vertices = [
|
||||||
|
QuadVertex(x: origin.x - halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x - halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + halfSize, y: origin.y + halfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||||
|
]
|
||||||
|
vertexCount = vertices.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSquaredQuad() {
|
||||||
|
let vHalfSize = size * 0.5
|
||||||
|
let hHalfSize = size * 0.15
|
||||||
|
vertices = [
|
||||||
|
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||||
|
]
|
||||||
|
vertexCount = vertices.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) {
|
||||||
|
let vHalfSize = size * vFactor * 0.5
|
||||||
|
let hHalfSize = size * hFactor * 0.5
|
||||||
|
vertices = [
|
||||||
|
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||||
|
]
|
||||||
|
vertexCount = vertices.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) {
|
||||||
|
let vHalfSize = size * heightFactor * 0.5
|
||||||
|
let hTopHalfSize = size * topFactor * 0.5
|
||||||
|
let hBottomHalfSize = size * bottomFactor * 0.5
|
||||||
|
vertices = [
|
||||||
|
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||||
|
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||||
|
]
|
||||||
|
vertexCount = vertices.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Quad {
|
||||||
|
enum Shape {
|
||||||
|
case rounded
|
||||||
|
case squared
|
||||||
|
case calligraphic(CGFloat, CGFloat)
|
||||||
|
case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
//
|
||||||
|
// SolidPointStrokeGenerator.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||||
|
var configuration: Configuration
|
||||||
|
|
||||||
|
func begin(at point: CGPoint, on stroke: Stroke) {
|
||||||
|
stroke.keyPoints.append(point)
|
||||||
|
addPoint(point, on: stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(to point: CGPoint, on stroke: Stroke) {
|
||||||
|
guard stroke.keyPoints.count > 0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stroke.keyPoints.append(point)
|
||||||
|
switch stroke.keyPoints.count {
|
||||||
|
case 2:
|
||||||
|
let start = stroke.keyPoints[0]
|
||||||
|
let end = stroke.keyPoints[1]
|
||||||
|
let control = CGPoint.middle(p1: start, p2: end)
|
||||||
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
|
case 3:
|
||||||
|
discardPoints(upto: stroke.vertexIndex, on: stroke)
|
||||||
|
let index = stroke.keyPoints.count - 1
|
||||||
|
var start = stroke.keyPoints[index - 2]
|
||||||
|
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
|
var control = CGPoint.middle(p1: start, p2: end)
|
||||||
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
|
start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
|
control = stroke.keyPoints[index - 1]
|
||||||
|
end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||||
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
|
default:
|
||||||
|
smoothOutPath(on: stroke)
|
||||||
|
let index = stroke.keyPoints.count - 1
|
||||||
|
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
|
let control = stroke.keyPoints[index - 1]
|
||||||
|
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||||
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func finish(at point: CGPoint, on stroke: Stroke) {
|
||||||
|
switch stroke.keyPoints.count {
|
||||||
|
case 0...1:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
append(to: point, on: stroke)
|
||||||
|
let index = stroke.keyPoints.count - 1
|
||||||
|
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
|
let end = stroke.keyPoints[index]
|
||||||
|
let control = CGPoint.middle(p1: start, p2: end)
|
||||||
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func smoothOutPath(on stroke: Stroke) {
|
||||||
|
discardPoints(upto: stroke.vertexIndex, on: stroke)
|
||||||
|
adjustPreviousKeyPoint(on: stroke)
|
||||||
|
switch stroke.keyPoints.count {
|
||||||
|
case 4:
|
||||||
|
let index = stroke.keyPoints.count - 2
|
||||||
|
let start = stroke.keyPoints[index - 2]
|
||||||
|
let end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
|
let control = CGPoint.middle(p1: start, p2: end)
|
||||||
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
let index = stroke.keyPoints.count - 2
|
||||||
|
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
|
let control = stroke.keyPoints[index - 1]
|
||||||
|
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||||
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
|
}
|
||||||
|
stroke.vertexIndex = stroke.vertices.endIndex - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private func adjustPreviousKeyPoint(on stroke: Stroke) {
|
||||||
|
let index = stroke.keyPoints.count - 1
|
||||||
|
let prev = stroke.keyPoints[index - 1]
|
||||||
|
let current = stroke.keyPoints[index]
|
||||||
|
let averageX = (prev.x + current.x) / 2
|
||||||
|
let averageY = (prev.y + current.y) / 2
|
||||||
|
let point = CGPoint(x: averageX, y: averageY)
|
||||||
|
stroke.keyPoints[index] = point
|
||||||
|
stroke.keyPoints[index - 1] = point
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addPoint(_ point: CGPoint, on stroke: Stroke) {
|
||||||
|
let rotation: CGFloat
|
||||||
|
switch configuration.rotation {
|
||||||
|
case .fixed:
|
||||||
|
rotation = 0
|
||||||
|
case .random:
|
||||||
|
rotation = CGFloat.random(in: 0...360) * .pi / 180
|
||||||
|
}
|
||||||
|
let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation)
|
||||||
|
stroke.vertices.append(contentsOf: quad.vertices)
|
||||||
|
stroke.vertexCount = stroke.vertices.endIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
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 .fixed:
|
||||||
|
factor = 1 / (stroke.thickness * stroke.style.stepRate)
|
||||||
|
case .none:
|
||||||
|
factor = 1 / (stroke.thickness * 10 / 500)
|
||||||
|
}
|
||||||
|
let segements = max(Int(distance * factor), 1)
|
||||||
|
for i in 0..<segements {
|
||||||
|
let t = CGFloat(i) / CGFloat(segements)
|
||||||
|
let x = pow(1 - t, 2) * start.x + 2.0 * (1 - t) * t * control.x + t * t * end.x
|
||||||
|
let y = pow(1 - t, 2) * start.y + 2.0 * (1 - t) * t * control.y + t * t * end.y
|
||||||
|
let point = CGPoint(x: x, y: y)
|
||||||
|
addPoint(point, on: stroke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func discardPoints(upto index: Int, on stroke: Stroke) {
|
||||||
|
if index < 0 {
|
||||||
|
stroke.vertices.removeAll()
|
||||||
|
} else {
|
||||||
|
let count = stroke.vertices.endIndex
|
||||||
|
let dropCount = count - (max(0, index) + 1)
|
||||||
|
stroke.vertices.removeLast(dropCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SolidPointStrokeGenerator {
|
||||||
|
struct Configuration {
|
||||||
|
var rotation: Rotation = .fixed
|
||||||
|
var granularity: Granularity = .none
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Rotation {
|
||||||
|
case fixed
|
||||||
|
case random
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Granularity {
|
||||||
|
case fixed
|
||||||
|
case none
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// Stroke.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Stroke: Codable {
|
||||||
|
var color: [CGFloat]
|
||||||
|
var style: any PenStyle
|
||||||
|
var thickness: CGFloat
|
||||||
|
var angle: CGFloat = 0
|
||||||
|
|
||||||
|
var vertexIndex: Int = -1
|
||||||
|
var keyPoints: [CGPoint] = []
|
||||||
|
var thicknessFactor: CGFloat = 0.7
|
||||||
|
|
||||||
|
var vertices: [QuadVertex] = []
|
||||||
|
var vertexBuffer: MTLBuffer?
|
||||||
|
var vertexCount: Int = 0
|
||||||
|
var tailVertices: [QuadVertex] = []
|
||||||
|
|
||||||
|
var texture: MTLTexture?
|
||||||
|
// var strokeTexture: MTLTexture?
|
||||||
|
|
||||||
|
var isEmpty: Bool {
|
||||||
|
vertices.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEraserPenStyle: Bool {
|
||||||
|
style is EraserPenStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
init(color: [CGFloat], style: any PenStyle, thickness: CGFloat) {
|
||||||
|
self.color = color
|
||||||
|
self.style = style
|
||||||
|
self.thickness = thickness
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case color
|
||||||
|
case style
|
||||||
|
case thickness
|
||||||
|
case vertices
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
color = try container.decode([CGFloat].self, forKey: .color)
|
||||||
|
let style: String = try container.decode(String.self, forKey: .style)
|
||||||
|
thickness = try container.decode(CGFloat.self, forKey: .thickness)
|
||||||
|
vertices = try container.decode([QuadVertex].self, forKey: .vertices)
|
||||||
|
vertexCount = vertices.count
|
||||||
|
switch style {
|
||||||
|
case "marker":
|
||||||
|
self.style = .marker
|
||||||
|
case "eraser":
|
||||||
|
self.style = .eraser
|
||||||
|
default:
|
||||||
|
throw DecodingError.valueNotFound(PenStyle.self, .init(codingPath: [CodingKeys.style], debugDescription: "There is no pen style called `\(style)`."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(color, forKey: .color)
|
||||||
|
try container.encode(thickness, forKey: .thickness)
|
||||||
|
try container.encode(vertices, forKey: .vertices)
|
||||||
|
let styleName: String
|
||||||
|
switch style {
|
||||||
|
case is MarkerPenStyle:
|
||||||
|
styleName = "marker"
|
||||||
|
case is EraserPenStyle:
|
||||||
|
styleName = "eraser"
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
try container.encode(styleName, forKey: .style)
|
||||||
|
}
|
||||||
|
|
||||||
|
func begin(at point: CGPoint) {
|
||||||
|
style.generator.begin(at: point, on: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(to point: CGPoint) {
|
||||||
|
style.generator.append(to: point, on: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func finish(at point: CGPoint) {
|
||||||
|
style.generator.finish(at: point, on: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Stroke: Drawable {
|
||||||
|
func prepare(device: MTLDevice) {
|
||||||
|
if texture == nil {
|
||||||
|
texture = style.loadTexture(on: device)
|
||||||
|
}
|
||||||
|
vertexBuffer = device.makeBuffer(bytes: &vertices, length: MemoryLayout<QuadVertex>.stride * vertexCount, options: .cpuCacheModeWriteCombined)
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
guard !isEmpty else { return }
|
||||||
|
prepare(device: device)
|
||||||
|
renderEncoder.setFragmentTexture(texture, index: 0)
|
||||||
|
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||||
|
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// StrokeGenerator.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol StrokeGenerator {
|
||||||
|
associatedtype Configuration
|
||||||
|
|
||||||
|
var configuration: Configuration { get set }
|
||||||
|
|
||||||
|
func begin(at point: CGPoint, on stroke: Stroke)
|
||||||
|
func append(to point: CGPoint, on stroke: Stroke)
|
||||||
|
func finish(at point: CGPoint, on stroke: Stroke)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// History.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class History: ObservableObject {
|
||||||
|
@Published var undoStack: [HistoryEvent] = []
|
||||||
|
@Published var redoStack: [HistoryEvent] = []
|
||||||
|
|
||||||
|
let historyPublisher = PassthroughSubject<HistoryAction, Never>()
|
||||||
|
|
||||||
|
var undoDisabled: Bool {
|
||||||
|
undoStack.isEmpty
|
||||||
|
}
|
||||||
|
var redoDisabled: Bool {
|
||||||
|
redoStack.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func undo() -> Bool {
|
||||||
|
guard let event = undoStack.popLast() else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
addRedo(event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func redo() -> HistoryEvent? {
|
||||||
|
guard let event = redoStack.popLast() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
addUndo(event)
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUndo(_ event: HistoryEvent) {
|
||||||
|
undoStack.append(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRedo(_ event: HistoryEvent) {
|
||||||
|
redoStack.append(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetRedo() {
|
||||||
|
redoStack.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// HistoryAction.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum HistoryAction {
|
||||||
|
case undo
|
||||||
|
case redo
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// HistoryEvent.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum HistoryEvent {
|
||||||
|
case stroke(Stroke)
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
//
|
||||||
|
// CacheRenderPass.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class CacheRenderPass: RenderPass {
|
||||||
|
var label: String = "Cache Render Pass"
|
||||||
|
|
||||||
|
var descriptor: MTLRenderPassDescriptor?
|
||||||
|
|
||||||
|
var cachePipelineState: MTLComputePipelineState?
|
||||||
|
var graphicPipelineState: MTLRenderPipelineState?
|
||||||
|
|
||||||
|
weak var graphicTexture: MTLTexture?
|
||||||
|
var cacheTexture: MTLTexture?
|
||||||
|
|
||||||
|
weak var strokeRenderPass: StrokeRenderPass?
|
||||||
|
weak var eraserRenderPass: EraserRenderPass?
|
||||||
|
var clearsTexture: Bool = true
|
||||||
|
|
||||||
|
init(renderer: Renderer) {
|
||||||
|
descriptor = MTLRenderPassDescriptor()
|
||||||
|
cachePipelineState = PipelineStates.createCachePipelineState(from: renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
|
||||||
|
guard size != .zero else { return }
|
||||||
|
cacheTexture = Textures.createCacheTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let descriptor, let strokeRenderPass, let eraserRenderPass else { return }
|
||||||
|
|
||||||
|
copyTexture(on: canvas, with: renderer)
|
||||||
|
|
||||||
|
guard let graphicPipelineState else { return }
|
||||||
|
descriptor.colorAttachments[0].texture = cacheTexture
|
||||||
|
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||||
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
|
descriptor.colorAttachments[0].storeAction = .store
|
||||||
|
|
||||||
|
if let stroke = canvas.graphicContext.currentStroke {
|
||||||
|
if stroke.isEraserPenStyle {
|
||||||
|
eraserRenderPass.stroke = stroke
|
||||||
|
eraserRenderPass.descriptor = descriptor
|
||||||
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
} else {
|
||||||
|
canvas.setGraphicRenderType(.inProgress)
|
||||||
|
strokeRenderPass.stroke = stroke
|
||||||
|
strokeRenderPass.graphicDescriptor = descriptor
|
||||||
|
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||||
|
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
|
clearsTexture = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func copyTexture(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let graphicTexture, let cacheTexture else { return }
|
||||||
|
guard let cachePipelineState else { return }
|
||||||
|
guard let copyCommandBuffer = renderer.commandQueue.makeCommandBuffer() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let computeEncoder = copyCommandBuffer.makeComputeCommandEncoder() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
computeEncoder.label = label
|
||||||
|
|
||||||
|
computeEncoder.setComputePipelineState(cachePipelineState)
|
||||||
|
computeEncoder.setTexture(graphicTexture, index: 0)
|
||||||
|
computeEncoder.setTexture(cacheTexture, index: 1)
|
||||||
|
let threadgroupSize = MTLSize(width: 8, height: 8, depth: 1)
|
||||||
|
let threadgroupCount = MTLSize(
|
||||||
|
width: (graphicTexture.width + threadgroupSize.width - 1) / threadgroupSize.width,
|
||||||
|
height: (graphicTexture.height + threadgroupSize.height - 1) / threadgroupSize.height,
|
||||||
|
depth: 1
|
||||||
|
)
|
||||||
|
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
|
||||||
|
computeEncoder.endEncoding()
|
||||||
|
copyCommandBuffer.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// EraserRenderPass.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class EraserRenderPass: RenderPass {
|
||||||
|
var label: String = "Eraser Render Pass"
|
||||||
|
|
||||||
|
var descriptor: MTLRenderPassDescriptor?
|
||||||
|
|
||||||
|
var eraserPipelineState: MTLRenderPipelineState?
|
||||||
|
|
||||||
|
var stroke: Stroke?
|
||||||
|
weak var graphicTexture: MTLTexture?
|
||||||
|
|
||||||
|
init(renderer: Renderer) {
|
||||||
|
eraserPipelineState = PipelineStates.createEraserPipelineState(from: renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
||||||
|
|
||||||
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let descriptor else { return }
|
||||||
|
|
||||||
|
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||||
|
commandBuffer.label = "Eraser Command Buffer"
|
||||||
|
|
||||||
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
|
||||||
|
renderEncoder.label = label
|
||||||
|
|
||||||
|
guard let eraserPipelineState else { return }
|
||||||
|
renderEncoder.setRenderPipelineState(eraserPipelineState)
|
||||||
|
|
||||||
|
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
|
||||||
|
renderEncoder.endEncoding()
|
||||||
|
commandBuffer.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// GraphicRenderPass.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class GraphicRenderPass: RenderPass {
|
||||||
|
var label: String { "Graphic Render Pass" }
|
||||||
|
var descriptor: MTLRenderPassDescriptor?
|
||||||
|
var graphicTexture: MTLTexture?
|
||||||
|
|
||||||
|
var graphicPipelineState: MTLRenderPipelineState?
|
||||||
|
|
||||||
|
weak var strokeRenderPass: StrokeRenderPass?
|
||||||
|
weak var eraserRenderPass: EraserRenderPass?
|
||||||
|
|
||||||
|
var clearsTexture: Bool = true
|
||||||
|
|
||||||
|
init(renderer: Renderer) {
|
||||||
|
descriptor = MTLRenderPassDescriptor()
|
||||||
|
graphicPipelineState = PipelineStates.createGraphicPipelineState(from: renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
|
||||||
|
guard size != .zero else { return }
|
||||||
|
graphicTexture = Textures.createGraphicTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
|
||||||
|
clearsTexture = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let strokeRenderPass, let eraserRenderPass else { return }
|
||||||
|
guard let descriptor else { return }
|
||||||
|
|
||||||
|
guard let graphicPipelineState else { return }
|
||||||
|
let graphicContext = canvas.graphicContext
|
||||||
|
guard let graphicTexture else { return }
|
||||||
|
|
||||||
|
descriptor.colorAttachments[0].texture = graphicTexture
|
||||||
|
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||||
|
descriptor.colorAttachments[0].storeAction = .store
|
||||||
|
|
||||||
|
if renderer.redrawsGraphicRender {
|
||||||
|
canvas.setGraphicRenderType(.finished)
|
||||||
|
for stroke in graphicContext.strokes {
|
||||||
|
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
|
clearsTexture = false
|
||||||
|
if stroke.isEraserPenStyle {
|
||||||
|
eraserRenderPass.stroke = stroke
|
||||||
|
eraserRenderPass.descriptor = descriptor
|
||||||
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
} else {
|
||||||
|
canvas.setGraphicRenderType(.finished)
|
||||||
|
strokeRenderPass.stroke = stroke
|
||||||
|
strokeRenderPass.graphicDescriptor = descriptor
|
||||||
|
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||||
|
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderer.redrawsGraphicRender = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let stroke = graphicContext.previousStroke {
|
||||||
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
|
clearsTexture = false
|
||||||
|
if stroke.isEraserPenStyle {
|
||||||
|
eraserRenderPass.stroke = stroke
|
||||||
|
eraserRenderPass.descriptor = descriptor
|
||||||
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
} else {
|
||||||
|
canvas.setGraphicRenderType(.newlyFinished)
|
||||||
|
strokeRenderPass.stroke = stroke
|
||||||
|
strokeRenderPass.graphicDescriptor = descriptor
|
||||||
|
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||||
|
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
|
graphicContext.previousStroke = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// StrokeRenderPass.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class StrokeRenderPass: RenderPass {
|
||||||
|
var label: String = "Stroke Render Pass"
|
||||||
|
|
||||||
|
var descriptor: MTLRenderPassDescriptor?
|
||||||
|
weak var graphicDescriptor: MTLRenderPassDescriptor?
|
||||||
|
|
||||||
|
var strokePipelineState: MTLRenderPipelineState?
|
||||||
|
weak var graphicPipelineState: MTLRenderPipelineState?
|
||||||
|
|
||||||
|
var stroke: Stroke?
|
||||||
|
var strokeTexture: MTLTexture?
|
||||||
|
|
||||||
|
init(renderer: Renderer) {
|
||||||
|
descriptor = MTLRenderPassDescriptor()
|
||||||
|
strokePipelineState = PipelineStates.createStrokePipelineState(from: renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
|
||||||
|
guard size != .zero else { return }
|
||||||
|
strokeTexture = Textures.createStrokeTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let descriptor else { return }
|
||||||
|
|
||||||
|
guard let strokeTexture else { return }
|
||||||
|
descriptor.colorAttachments[0].texture = strokeTexture
|
||||||
|
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||||
|
descriptor.colorAttachments[0].loadAction = .clear
|
||||||
|
descriptor.colorAttachments[0].storeAction = .store
|
||||||
|
|
||||||
|
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||||
|
commandBuffer.label = "Stroke Command Buffer"
|
||||||
|
|
||||||
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
|
||||||
|
renderEncoder.label = label
|
||||||
|
|
||||||
|
guard let strokePipelineState else { return }
|
||||||
|
renderEncoder.setRenderPipelineState(strokePipelineState)
|
||||||
|
|
||||||
|
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
|
||||||
|
renderEncoder.endEncoding()
|
||||||
|
commandBuffer.commit()
|
||||||
|
|
||||||
|
drawStrokeTexture(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let stroke else { return }
|
||||||
|
guard let graphicDescriptor, let graphicPipelineState else { return }
|
||||||
|
|
||||||
|
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||||
|
commandBuffer.label = "Graphic Command Buffer"
|
||||||
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: graphicDescriptor) else { return }
|
||||||
|
renderEncoder.label = "Graphic Render Pass"
|
||||||
|
renderEncoder.setRenderPipelineState(graphicPipelineState)
|
||||||
|
|
||||||
|
renderEncoder.setFragmentTexture(strokeTexture, index: 0)
|
||||||
|
var uniforms = GraphicUniforms(color: stroke.color)
|
||||||
|
let uniformsBuffer = renderer.device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
||||||
|
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||||
|
canvas.renderGraphic(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
renderEncoder.endEncoding()
|
||||||
|
commandBuffer.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
//
|
||||||
|
// ViewPortRenderPass.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ViewPortRenderPass: RenderPass {
|
||||||
|
var label: String { "View Port Render Pass"}
|
||||||
|
var descriptor: MTLRenderPassDescriptor?
|
||||||
|
|
||||||
|
var gridPipelineState: MTLRenderPipelineState?
|
||||||
|
var viewPortPipelineState: MTLRenderPipelineState?
|
||||||
|
var viewPortUpdatePipelineState: MTLRenderPipelineState?
|
||||||
|
|
||||||
|
weak var cacheTexture: MTLTexture?
|
||||||
|
|
||||||
|
weak var view: MTKView?
|
||||||
|
|
||||||
|
init(renderer: Renderer) {
|
||||||
|
gridPipelineState = PipelineStates.createGridPipelineState(from: renderer)
|
||||||
|
viewPortPipelineState = PipelineStates.createViewPortPipelineState(from: renderer)
|
||||||
|
viewPortUpdatePipelineState = PipelineStates.createViewPortPipelineState(from: renderer, isUpdate: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
||||||
|
|
||||||
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let descriptor else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commandBuffer.label = "View Port Command Buffer"
|
||||||
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderEncoder.label = label
|
||||||
|
|
||||||
|
guard let gridPipelineState else { return }
|
||||||
|
renderEncoder.setRenderPipelineState(gridPipelineState)
|
||||||
|
canvas.renderGrid(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
|
||||||
|
if renderer.updatesViewPort {
|
||||||
|
guard let viewPortUpdatePipelineState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEncoder.setRenderPipelineState(viewPortUpdatePipelineState)
|
||||||
|
renderEncoder.setFragmentTexture(cacheTexture, index: 0)
|
||||||
|
canvas.renderViewPortUpdate(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
} else {
|
||||||
|
guard let viewPortPipelineState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEncoder.setRenderPipelineState(viewPortPipelineState)
|
||||||
|
renderEncoder.setFragmentTexture(cacheTexture, index: 0)
|
||||||
|
canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEncoder.endEncoding()
|
||||||
|
|
||||||
|
guard let drawable = view?.currentDrawable else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commandBuffer.present(drawable)
|
||||||
|
commandBuffer.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Cache.metal
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
kernel void copy_texture_viewport(
|
||||||
|
texture2d<float, access::read> inTexture [[texture(0)]],
|
||||||
|
texture2d<float, access::write> outTexture [[texture(1)]],
|
||||||
|
uint2 gid [[thread_position_in_grid]]
|
||||||
|
) {
|
||||||
|
float4 color = inTexture.read(gid);
|
||||||
|
outTexture.write(color, gid);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// Graphic.metal
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct VertexIn {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 textCoord;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexOut {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 textCoord;
|
||||||
|
float4 color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
float4 color;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex VertexOut vertex_graphic(
|
||||||
|
constant VertexIn *vertices [[buffer(0)]],
|
||||||
|
constant Uniforms &uniforms [[buffer(11)]],
|
||||||
|
uint vertexId [[vertex_id]]
|
||||||
|
) {
|
||||||
|
VertexIn in = vertices[vertexId];
|
||||||
|
VertexOut out;
|
||||||
|
out.position = in.position;
|
||||||
|
out.textCoord = in.textCoord;
|
||||||
|
out.color = uniforms.color;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment float4 fragment_graphic(
|
||||||
|
VertexOut out [[stage_in]],
|
||||||
|
texture2d<float> offscreenTexture [[texture(0)]]
|
||||||
|
) {
|
||||||
|
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||||
|
float4 color = float4(offscreenTexture.sample(textureSampler, out.textCoord));
|
||||||
|
return float4(out.color.rgb, color.a * out.color.a);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// Grid.metal
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float4 position [[position]];
|
||||||
|
float pointSize [[point_size]];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
float ratio;
|
||||||
|
float zoom;
|
||||||
|
float4x4 transform;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex Vertex vertex_grid(
|
||||||
|
constant Vertex *vertices [[buffer(0)]],
|
||||||
|
constant Uniforms &uniforms [[buffer(11)]],
|
||||||
|
uint vertexId [[vertex_id]]
|
||||||
|
) {
|
||||||
|
Vertex _vertex = vertices[vertexId];
|
||||||
|
float x = _vertex.position.x * uniforms.ratio;
|
||||||
|
float y = _vertex.position.y * uniforms.ratio;
|
||||||
|
float4 position = float4(x, y, 0, 1);
|
||||||
|
_vertex.position = uniforms.transform * position;
|
||||||
|
_vertex.pointSize = 10 * uniforms.zoom / 12;
|
||||||
|
return _vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment float4 fragment_grid(
|
||||||
|
Vertex _vertex [[stage_in]],
|
||||||
|
float2 pointCoord [[point_coord]]
|
||||||
|
) {
|
||||||
|
float dist = length(pointCoord - float2(0.5));
|
||||||
|
float4 color = float4(0.752, 0.752, 0.752, 1);
|
||||||
|
color.a = 1.0 - smoothstep(0.4, 0.5, dist);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// Stroke.metal
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct VertexIn {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 textCoord;
|
||||||
|
float4 color;
|
||||||
|
float2 origin;
|
||||||
|
float rotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexOut {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 textCoord;
|
||||||
|
float4 color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
float4x4 transform;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex VertexOut vertex_stroke(
|
||||||
|
constant VertexIn *vertices [[buffer(0)]],
|
||||||
|
constant Uniforms &uniforms [[buffer(11)]],
|
||||||
|
uint vertexId [[vertex_id]]
|
||||||
|
) {
|
||||||
|
VertexIn in = vertices[vertexId];
|
||||||
|
|
||||||
|
float2 rotatedPosition;
|
||||||
|
rotatedPosition.x = cos(in.rotation) * (in.position.x - in.origin.x) - sin(in.rotation) * (in.position.y - in.origin.y) + in.origin.x;
|
||||||
|
rotatedPosition.y = sin(in.rotation) * (in.position.x - in.origin.x) + cos(in.rotation) * (in.position.y - in.origin.y) + in.origin.y;
|
||||||
|
|
||||||
|
VertexOut out;
|
||||||
|
out.position = uniforms.transform * float4(rotatedPosition, 0, 1);
|
||||||
|
out.textCoord = in.textCoord;
|
||||||
|
out.color = in.color;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment float4 fragment_stroke(
|
||||||
|
VertexOut out [[stage_in]],
|
||||||
|
texture2d<float> texture [[texture(0)]]
|
||||||
|
) {
|
||||||
|
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||||
|
float4 color = float4(texture.sample(textureSampler, out.textCoord));
|
||||||
|
return float4(1, 1, 1, color.a);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// Viewport.metal
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 textCoord;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
float4x4 transform;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex Vertex vertex_viewport(
|
||||||
|
constant Vertex *vertices [[buffer(0)]],
|
||||||
|
uint vertexId [[vertex_id]]
|
||||||
|
) {
|
||||||
|
Vertex _vertex = vertices[vertexId];
|
||||||
|
return _vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
vertex Vertex vertex_viewport_update(
|
||||||
|
constant Vertex *vertices [[buffer(0)]],
|
||||||
|
constant Uniforms &uniforms [[buffer(11)]],
|
||||||
|
uint vertexId [[vertex_id]]
|
||||||
|
) {
|
||||||
|
Vertex _vertex = vertices[vertexId];
|
||||||
|
_vertex.position = uniforms.transform * _vertex.position;
|
||||||
|
return _vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment float4 fragment_viewport(
|
||||||
|
Vertex _vertex [[stage_in]],
|
||||||
|
texture2d<float> offscreenTexture [[texture(0)]]
|
||||||
|
) {
|
||||||
|
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||||
|
float4 sampledColor = float4(offscreenTexture.sample(textureSampler, _vertex.textCoord));
|
||||||
|
return sampledColor;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// Pen.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Pen: NSObject, ObservableObject, Identifiable {
|
||||||
|
@Published var style: PenStyle
|
||||||
|
@Published var color: [CGFloat]
|
||||||
|
@Published var thickness: CGFloat
|
||||||
|
|
||||||
|
init(style: any PenStyle, color: [CGFloat], thickness: CGFloat) {
|
||||||
|
self.style = style
|
||||||
|
self.color = color
|
||||||
|
self.thickness = thickness
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Pen {
|
||||||
|
convenience init(for style: any PenStyle) {
|
||||||
|
self.init(style: style, color: style.color, thickness: style.thinkness.min)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// PenStyle.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol PenStyle {
|
||||||
|
var icon: (base: String, tip: String?) { get }
|
||||||
|
var textureName: String { get }
|
||||||
|
var thinkness: (min: CGFloat, max: CGFloat) { get }
|
||||||
|
var color: [CGFloat] { get }
|
||||||
|
var stepRate: CGFloat { get }
|
||||||
|
var generator: any StrokeGenerator { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PenStyle {
|
||||||
|
@discardableResult
|
||||||
|
func loadTexture(on device: MTLDevice) -> MTLTexture? {
|
||||||
|
Textures.createPenTexture(with: textureName, on: device)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// EraserPenStyle.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct EraserPenStyle: PenStyle {
|
||||||
|
var icon: (base: String, tip: String?) = ("eraser", nil)
|
||||||
|
|
||||||
|
var textureName: String = "point-texture"
|
||||||
|
|
||||||
|
var thinkness: (min: CGFloat, max: CGFloat) = (15, 120)
|
||||||
|
|
||||||
|
var color: [CGFloat] = [1, 1, 1, 0]
|
||||||
|
|
||||||
|
var stepRate: CGFloat = 0.2
|
||||||
|
|
||||||
|
var generator: any StrokeGenerator {
|
||||||
|
SolidPointStrokeGenerator(configuration: .init())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PenStyle where Self == EraserPenStyle {
|
||||||
|
static var eraser: PenStyle {
|
||||||
|
EraserPenStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// MarkerPenStyle.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct MarkerPenStyle: PenStyle {
|
||||||
|
var icon: (base: String, tip: String?) = ("marker-base", "marker-tip")
|
||||||
|
|
||||||
|
var textureName: String = "point-texture"
|
||||||
|
|
||||||
|
var thinkness: (min: CGFloat, max: CGFloat) = (15, 120)
|
||||||
|
|
||||||
|
var color: [CGFloat] = [1, 0.38, 0.38, 1]
|
||||||
|
|
||||||
|
var stepRate: CGFloat = 0.2
|
||||||
|
|
||||||
|
var generator: any StrokeGenerator {
|
||||||
|
SolidPointStrokeGenerator(configuration: .init())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PenStyle where Self == MarkerPenStyle {
|
||||||
|
static var marker: PenStyle {
|
||||||
|
MarkerPenStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// Tool.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Tool: NSObject, ObservableObject {
|
||||||
|
@Published var pens: [Pen]
|
||||||
|
@Published var selectedPen: Pen?
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
pens = [
|
||||||
|
Pen(for: .marker),
|
||||||
|
Pen(for: .eraser)
|
||||||
|
]
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func changePen(_ pen: Pen) {
|
||||||
|
selectedPen = pen
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
//
|
||||||
|
// CanvasViewController.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class CanvasViewController: UIViewController {
|
||||||
|
let drawingView: DrawingView
|
||||||
|
let scrollView: UIScrollView = UIScrollView()
|
||||||
|
var renderView: MTKView {
|
||||||
|
drawingView.renderView
|
||||||
|
}
|
||||||
|
|
||||||
|
let tool: Tool
|
||||||
|
let canvas: Canvas
|
||||||
|
let history: History
|
||||||
|
let renderer: Renderer
|
||||||
|
|
||||||
|
var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
init(tool: Tool, canvas: Canvas, history: History) {
|
||||||
|
self.tool = tool
|
||||||
|
self.canvas = canvas
|
||||||
|
self.history = history
|
||||||
|
self.drawingView = DrawingView(tool: tool, canvas: canvas, history: history)
|
||||||
|
self.renderer = Renderer(canvasView: drawingView.renderView)
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
configureViews()
|
||||||
|
configureGestures()
|
||||||
|
configureListeners()
|
||||||
|
|
||||||
|
loadBoard()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
resizeDocumentView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
drawingView.disableUserInteraction()
|
||||||
|
drawingView.updateDrawableSize(with: view.frame.size)
|
||||||
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||||
|
renderView.draw()
|
||||||
|
drawingView.enableUserInteraction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController {
|
||||||
|
func configureViews() {
|
||||||
|
view.backgroundColor = .white
|
||||||
|
renderView.autoResizeDrawable = false
|
||||||
|
renderView.enableSetNeedsDisplay = true
|
||||||
|
renderView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
renderView.clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 1)
|
||||||
|
view.addSubview(renderView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
renderView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
renderView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
renderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
renderView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
scrollView.maximumZoomScale = canvas.maximumZoomScale
|
||||||
|
scrollView.minimumZoomScale = canvas.minimumZoomScale
|
||||||
|
scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
scrollView.isScrollEnabled = true
|
||||||
|
scrollView.showsVerticalScrollIndicator = true
|
||||||
|
scrollView.showsHorizontalScrollIndicator = true
|
||||||
|
scrollView.delegate = self
|
||||||
|
scrollView.backgroundColor = .clear
|
||||||
|
|
||||||
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(scrollView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
scrollView.addSubview(drawingView)
|
||||||
|
drawingView.backgroundColor = .clear
|
||||||
|
drawingView.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizeDocumentView(to newSize: CGSize? = nil) {
|
||||||
|
scrollView.layoutIfNeeded()
|
||||||
|
|
||||||
|
let size = canvas.size
|
||||||
|
let widthScale = (newSize?.width ?? view.frame.width) / size.width
|
||||||
|
let heightScale = (newSize?.height ?? view.frame.height) / size.height
|
||||||
|
let scale = max(widthScale, heightScale)
|
||||||
|
|
||||||
|
let width = size.width * scale
|
||||||
|
let height = size.height * scale
|
||||||
|
let newFrame = CGRect(x: 0, y: 0, width: width, height: height)
|
||||||
|
drawingView.frame = newFrame
|
||||||
|
|
||||||
|
scrollView.setZoomScale(canvas.minimumZoomScale, animated: true)
|
||||||
|
centerDocumentView(to: newSize)
|
||||||
|
|
||||||
|
let offsetX = (newFrame.width * canvas.minimumZoomScale - view.frame.width) / 2
|
||||||
|
let offsetY = (newFrame.height * canvas.minimumZoomScale - view.frame.height) / 2
|
||||||
|
|
||||||
|
let point = CGPoint(x: offsetX, y: offsetY)
|
||||||
|
scrollView.setContentOffset(point, animated: true)
|
||||||
|
|
||||||
|
drawingView.updateDrawableSize(with: view.frame.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func centerDocumentView(to newSize: CGSize? = nil) {
|
||||||
|
let documentViewSize = drawingView.frame.size
|
||||||
|
let scrollViewSize = newSize ?? view.frame.size
|
||||||
|
let verticalPadding = documentViewSize.height < scrollViewSize.height ? (scrollViewSize.height - documentViewSize.height) / 2 : 0
|
||||||
|
let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0
|
||||||
|
self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController {
|
||||||
|
func configureListeners() {
|
||||||
|
canvas.$state
|
||||||
|
.sink { [weak self] state in
|
||||||
|
self?.canvasStateChanged(state)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
tool.$selectedPen
|
||||||
|
.sink { [weak self] pen in
|
||||||
|
self?.penChanged(to: pen)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
history.historyPublisher
|
||||||
|
.sink { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .undo:
|
||||||
|
self?.historyUndid()
|
||||||
|
case .redo:
|
||||||
|
self?.historyRedid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController {
|
||||||
|
func loadBoard() {
|
||||||
|
canvas.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func canvasStateChanged(_ state: Canvas.State) {
|
||||||
|
guard state == .loaded else { return }
|
||||||
|
renderView.delegate = self
|
||||||
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||||
|
renderView.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController: MTKViewDelegate {
|
||||||
|
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { }
|
||||||
|
|
||||||
|
func draw(in view: MTKView) {
|
||||||
|
guard view.drawableSize != .zero else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
canvas.updateTransform(on: drawingView)
|
||||||
|
renderer.draw(in: view, on: canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController {
|
||||||
|
func configureGestures() {
|
||||||
|
let drawingPanGesture = UIPanGestureRecognizer(target: self, action: #selector(recognizePanGesture))
|
||||||
|
drawingPanGesture.maximumNumberOfTouches = 1
|
||||||
|
drawingPanGesture.minimumNumberOfTouches = 1
|
||||||
|
drawingView.addGestureRecognizer(drawingPanGesture)
|
||||||
|
|
||||||
|
let drawingTapGesture = UITapGestureRecognizer(target: self, action: #selector(recognizeTapGesture))
|
||||||
|
drawingTapGesture.numberOfTapsRequired = 1
|
||||||
|
drawingView.addGestureRecognizer(drawingTapGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func recognizePanGesture(_ gesture: UIPanGestureRecognizer) {
|
||||||
|
let point = gesture.location(in: drawingView)
|
||||||
|
switch gesture.state {
|
||||||
|
case .began:
|
||||||
|
drawingView.touchBegan(on: point)
|
||||||
|
case .changed:
|
||||||
|
drawingView.touchMoved(to: point)
|
||||||
|
case .ended:
|
||||||
|
drawingView.touchEnded(to: point)
|
||||||
|
case .cancelled:
|
||||||
|
drawingView.touchEnded(to: point)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
|
||||||
|
let point = gesture.location(in: drawingView)
|
||||||
|
drawingView.touchBegan(on: point)
|
||||||
|
drawingView.touchEnded(to: point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController: UIScrollViewDelegate {
|
||||||
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||||
|
drawingView
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
||||||
|
magnificationStarted()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||||
|
canvas.setZoomScale(scrollView.zoomScale)
|
||||||
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||||
|
renderView.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||||
|
centerDocumentView()
|
||||||
|
magnificationEnded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
draggingStarted()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||||
|
renderView.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
if !scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
|
||||||
|
scrollViewDidEndScrolling(scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
if !decelerate, scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
|
||||||
|
scrollViewDidEndScrolling(scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
|
||||||
|
draggingEnded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController {
|
||||||
|
func magnificationStarted() {
|
||||||
|
guard !renderer.updatesViewPort else { return }
|
||||||
|
canvas.updateClipBounds(scrollView, on: drawingView)
|
||||||
|
drawingView.disableUserInteraction()
|
||||||
|
renderer.updatesViewPort = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func magnificationEnded() {
|
||||||
|
renderer.updatesViewPort = false
|
||||||
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||||
|
renderView.draw()
|
||||||
|
drawingView.enableUserInteraction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func draggingStarted() {
|
||||||
|
guard !renderer.updatesViewPort else { return }
|
||||||
|
canvas.updateClipBounds(scrollView, on: drawingView)
|
||||||
|
drawingView.disableUserInteraction()
|
||||||
|
renderer.updatesViewPort = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func draggingEnded() {
|
||||||
|
renderer.updatesViewPort = false
|
||||||
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||||
|
renderView.draw()
|
||||||
|
drawingView.enableUserInteraction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController {
|
||||||
|
func penChanged(to pen: Pen?) {
|
||||||
|
if let pen, let device = drawingView.renderView.device {
|
||||||
|
pen.style.loadTexture(on: device)
|
||||||
|
}
|
||||||
|
let isPenSelected = pen != nil
|
||||||
|
scrollView.isScrollEnabled = !isPenSelected
|
||||||
|
drawingView.isUserInteractionEnabled = isPenSelected
|
||||||
|
isPenSelected ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController {
|
||||||
|
func historyUndid() {
|
||||||
|
guard history.undo() else { return }
|
||||||
|
drawingView.disableUserInteraction()
|
||||||
|
canvas.graphicContext.undoGraphic()
|
||||||
|
renderer.redrawsGraphicRender = true
|
||||||
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||||
|
renderView.draw()
|
||||||
|
drawingView.enableUserInteraction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func historyRedid() {
|
||||||
|
guard let event = history.redo() else { return }
|
||||||
|
drawingView.disableUserInteraction()
|
||||||
|
canvas.graphicContext.redoGraphic(for: event)
|
||||||
|
renderer.redrawsGraphicRender = true
|
||||||
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
||||||
|
renderView.draw()
|
||||||
|
drawingView.enableUserInteraction()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// DrawingView.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class DrawingView: UIView {
|
||||||
|
let tool: Tool
|
||||||
|
let canvas: Canvas
|
||||||
|
let history: History
|
||||||
|
let renderView: MTKView
|
||||||
|
|
||||||
|
var ratio: CGFloat { canvas.size.width / bounds.width }
|
||||||
|
|
||||||
|
private var disablesUserInteraction: Bool = false
|
||||||
|
|
||||||
|
required init(tool: Tool, canvas: Canvas, history: History) {
|
||||||
|
self.tool = tool
|
||||||
|
self.canvas = canvas
|
||||||
|
self.history = history
|
||||||
|
self.renderView = MTKView(frame: .zero)
|
||||||
|
super.init(frame: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDrawableSize(with size: CGSize) {
|
||||||
|
renderView.drawableSize = size.multiply(by: 2.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func touchBegan(on point: CGPoint) {
|
||||||
|
guard !disablesUserInteraction else { return }
|
||||||
|
guard let pen = tool.selectedPen else { return }
|
||||||
|
let stroke = canvas.beginTouch(at: point.muliply(by: ratio), pen: pen)
|
||||||
|
renderView.draw()
|
||||||
|
history.addUndo(.stroke(stroke))
|
||||||
|
history.resetRedo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func touchMoved(to point: CGPoint) {
|
||||||
|
guard !disablesUserInteraction else { return }
|
||||||
|
canvas.moveTouch(to: point.muliply(by: ratio))
|
||||||
|
renderView.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func touchEnded(to point: CGPoint) {
|
||||||
|
guard !disablesUserInteraction else { return }
|
||||||
|
canvas.endTouch(at: point.muliply(by: ratio))
|
||||||
|
renderView.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableUserInteraction() {
|
||||||
|
disablesUserInteraction = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableUserInteraction() {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
|
||||||
|
self?.disablesUserInteraction = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// CanvasView.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CanvasView: UIViewControllerRepresentable {
|
||||||
|
@EnvironmentObject var tool: Tool
|
||||||
|
@EnvironmentObject var canvas: Canvas
|
||||||
|
@EnvironmentObject var history: History
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> CanvasViewController {
|
||||||
|
CanvasViewController(tool: tool, canvas: canvas, history: history)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: CanvasViewController, context: Context) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// Array++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Array {
|
||||||
|
mutating func append(_ element: Element, capacity: Int) {
|
||||||
|
if count >= capacity {
|
||||||
|
remove(at: 0)
|
||||||
|
}
|
||||||
|
append(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// CGAffineTransform++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension CGAffineTransform {
|
||||||
|
static func * (lhs: CGAffineTransform, rhs: CGAffineTransform) -> CGAffineTransform {
|
||||||
|
return lhs.concatenating(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// CGFloat++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension CGFloat {
|
||||||
|
var float: Float {
|
||||||
|
Float(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// CGPoint++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension CGPoint {
|
||||||
|
func muliply(by factor: CGFloat) -> CGPoint {
|
||||||
|
CGPoint(x: x * factor, y: y * factor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func distance(to point: CGPoint) -> CGFloat {
|
||||||
|
let p = pow(x - point.x, 2) + pow(y - point.y, 2)
|
||||||
|
return sqrt(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func middle(p1: CGPoint, p2: CGPoint) -> CGPoint {
|
||||||
|
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func angle(to point: CGPoint) -> CGFloat {
|
||||||
|
let deltaX = point.x - x
|
||||||
|
let deltaY = point.y - y
|
||||||
|
return atan2(deltaY, deltaX)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// CGRect++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension CGRect {
|
||||||
|
func transform(to rect: CGRect) -> CGAffineTransform {
|
||||||
|
var t = CGAffineTransform.identity
|
||||||
|
t = t.translatedBy(x: -self.minX, y: -self.minY)
|
||||||
|
t = t.translatedBy(x: rect.minX, y: rect.minY)
|
||||||
|
t = t.scaledBy(x: 1 / self.width, y: 1 / self.height)
|
||||||
|
t = t.scaledBy(x: rect.width, y: rect.height)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// CGSize++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension CGSize {
|
||||||
|
func multiply(by scale: CGFloat) -> CGSize {
|
||||||
|
CGSize(width: width * scale, height: height * scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// Collection++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Collection {
|
||||||
|
subscript(safe index: Index) -> Element? {
|
||||||
|
return indices.contains(index) ? self[index] : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Collection where Index == Int {
|
||||||
|
subscript(fromEnd index: Index) -> Element? {
|
||||||
|
let i = count - (index + 1)
|
||||||
|
return self[safe: i]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// Color++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
var components: [CGFloat] {
|
||||||
|
let color = UIColor(self)
|
||||||
|
return color.components
|
||||||
|
}
|
||||||
|
|
||||||
|
static func rgba(from color: [CGFloat]) -> Color {
|
||||||
|
Color(red: color[0], green: color[1], blue: color[2]).opacity(color[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIColor {
|
||||||
|
var components: [CGFloat] {
|
||||||
|
let uiColor: UIColor = self
|
||||||
|
let ciColor: CIColor = .init(color: uiColor)
|
||||||
|
return [ciColor.red, ciColor.green, ciColor.blue, ciColor.alpha]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// MTLDevice++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
|
||||||
|
extension MTLDevice {
|
||||||
|
func maximumTextureDimension() -> Int {
|
||||||
|
supportsFamily(.apple3) ? 16384 : 8192
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// simd_float4x4++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension simd_float4x4 {
|
||||||
|
init(_ transform: CGAffineTransform) {
|
||||||
|
let t = CATransform3DMakeAffineTransform(transform)
|
||||||
|
self = simd_float4x4([
|
||||||
|
[Float(t.m11), Float(t.m12), Float(t.m13), Float(t.m14)],
|
||||||
|
[Float(t.m21), Float(t.m22), Float(t.m23), Float(t.m24)],
|
||||||
|
[Float(t.m31), Float(t.m32), Float(t.m33), Float(t.m34)],
|
||||||
|
[Float(t.m41), Float(t.m42), Float(t.m43), Float(t.m44)]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user