diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 6d0a2c7..30ef618 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -12,6 +12,56 @@ EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; }; ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738792BE5EF0400A4542E /* MemosView.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 */ /* Begin PBXFileReference section */ @@ -21,6 +71,56 @@ EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; ECA738792BE5EF0400A4542E /* MemosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosView.swift; sourceTree = ""; }; ECA7387C2BE5EF4B00A4542E /* MemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoView.swift; sourceTree = ""; }; + ECA738822BE5FEFE00A4542E /* RenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderPass.swift; sourceTree = ""; }; + ECA738852BE5FF2500A4542E /* Canvas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Canvas.swift; sourceTree = ""; }; + ECA738872BE5FF4400A4542E /* Renderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = ""; }; + ECA738892BE6006A00A4542E /* PipelineStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipelineStates.swift; sourceTree = ""; }; + ECA7388B2BE6009600A4542E /* Textures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Textures.swift; sourceTree = ""; }; + ECA7388E2BE600DA00A4542E /* Grid.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Grid.metal; sourceTree = ""; }; + ECA738902BE600F500A4542E /* Cache.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Cache.metal; sourceTree = ""; }; + ECA738922BE6011100A4542E /* Stroke.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Stroke.metal; sourceTree = ""; }; + ECA738942BE6012D00A4542E /* ViewPort.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ViewPort.metal; sourceTree = ""; }; + ECA738962BE6014200A4542E /* Graphic.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Graphic.metal; sourceTree = ""; }; + ECA7389B2BE601AF00A4542E /* GridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridVertex.swift; sourceTree = ""; }; + ECA7389D2BE601CB00A4542E /* QuadVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadVertex.swift; sourceTree = ""; }; + ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortVertex.swift; sourceTree = ""; }; + ECA738A22BE6020A00A4542E /* CGFloat++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat++.swift"; sourceTree = ""; }; + ECA738A52BE6023F00A4542E /* GridUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridUniforms.swift; sourceTree = ""; }; + ECA738A72BE6025900A4542E /* GraphicUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicUniforms.swift; sourceTree = ""; }; + ECA738A92BE6026D00A4542E /* Uniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uniforms.swift; sourceTree = ""; }; + ECA738AC2BE60CC600A4542E /* DrawingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawingView.swift; sourceTree = ""; }; + ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasViewController.swift; sourceTree = ""; }; + ECA738B22BE60D9E00A4542E /* CanvasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasView.swift; sourceTree = ""; }; + ECA738B52BE60DCD00A4542E /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; + ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEvent.swift; sourceTree = ""; }; + ECA738B92BE60DEF00A4542E /* HistoryAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryAction.swift; sourceTree = ""; }; + ECA738BB2BE60E0300A4542E /* Tool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tool.swift; sourceTree = ""; }; + ECA738BE2BE60E3400A4542E /* Pen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pen.swift; sourceTree = ""; }; + ECA738C02BE60E5300A4542E /* PenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenStyle.swift; sourceTree = ""; }; + ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerPenStyle.swift; sourceTree = ""; }; + ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserPenStyle.swift; sourceTree = ""; }; + ECA738C82BE60EF700A4542E /* GraphicContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContext.swift; sourceTree = ""; }; + ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortContext.swift; sourceTree = ""; }; + ECA738CC2BE60F2F00A4542E /* GridContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridContext.swift; sourceTree = ""; }; + ECA738D12BE60F7B00A4542E /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = ""; }; + ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeGenerator.swift; sourceTree = ""; }; + ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidPointStrokeGenerator.swift; sourceTree = ""; }; + ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheRenderPass.swift; sourceTree = ""; }; + ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeRenderPass.swift; sourceTree = ""; }; + ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortRenderPass.swift; sourceTree = ""; }; + ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserRenderPass.swift; sourceTree = ""; }; + ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicRenderPass.swift; sourceTree = ""; }; + ECA738E32BE6110800A4542E /* Drawable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawable.swift; sourceTree = ""; }; + ECA738E52BE611FD00A4542E /* CGRect++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect++.swift"; sourceTree = ""; }; + ECA738E72BE6120F00A4542E /* Color++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color++.swift"; sourceTree = ""; }; + ECA738E92BE6122E00A4542E /* CGPoint++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint++.swift"; sourceTree = ""; }; + ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGAffineTransform++.swift"; sourceTree = ""; }; + ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "simd_float4x4++.swift"; sourceTree = ""; }; + ECA738EF2BE6127700A4542E /* CGSize++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize++.swift"; sourceTree = ""; }; + ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = ""; }; + ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = ""; }; + ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = ""; }; + ECA738F72BE612EB00A4542E /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,9 +154,11 @@ isa = PBXGroup; children = ( ECA738762BE5EE4E00A4542E /* App */, + ECA7387E2BE5FE4200A4542E /* Canvas */, + ECA738A12BE601F700A4542E /* Extensions */, ECA738772BE5EEE800A4542E /* Features */, - EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */, EC7F6BF12BE5E6E400A34A7B /* Preview Content */, + ECA738802BE5FE6000A4542E /* Resources */, ); path = Memola; sourceTree = ""; @@ -102,6 +204,222 @@ path = Memo; sourceTree = ""; }; + 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 = ""; + }; + ECA738802BE5FE6000A4542E /* Resources */ = { + isa = PBXGroup; + children = ( + EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + ECA738812BE5FEEE00A4542E /* Abstracts */ = { + isa = PBXGroup; + children = ( + ECA738822BE5FEFE00A4542E /* RenderPass.swift */, + ECA738E32BE6110800A4542E /* Drawable.swift */, + ); + path = Abstracts; + sourceTree = ""; + }; + ECA738842BE5FF1B00A4542E /* Core */ = { + isa = PBXGroup; + children = ( + ECA738852BE5FF2500A4542E /* Canvas.swift */, + ECA738892BE6006A00A4542E /* PipelineStates.swift */, + ECA738872BE5FF4400A4542E /* Renderer.swift */, + ECA7388B2BE6009600A4542E /* Textures.swift */, + ); + path = Core; + sourceTree = ""; + }; + ECA7388D2BE600BB00A4542E /* Shaders */ = { + isa = PBXGroup; + children = ( + ECA7388E2BE600DA00A4542E /* Grid.metal */, + ECA738902BE600F500A4542E /* Cache.metal */, + ECA738922BE6011100A4542E /* Stroke.metal */, + ECA738942BE6012D00A4542E /* ViewPort.metal */, + ECA738962BE6014200A4542E /* Graphic.metal */, + ); + path = Shaders; + sourceTree = ""; + }; + ECA738982BE6015700A4542E /* Primitives */ = { + isa = PBXGroup; + children = ( + ECA738F72BE612EB00A4542E /* Quad.swift */, + ); + path = Primitives; + sourceTree = ""; + }; + ECA738992BE6018900A4542E /* Buffers */ = { + isa = PBXGroup; + children = ( + ECA738A42BE6022F00A4542E /* Uniforms */, + ECA7389A2BE6019700A4542E /* Vertices */, + ); + path = Buffers; + sourceTree = ""; + }; + ECA7389A2BE6019700A4542E /* Vertices */ = { + isa = PBXGroup; + children = ( + ECA7389B2BE601AF00A4542E /* GridVertex.swift */, + ECA7389D2BE601CB00A4542E /* QuadVertex.swift */, + ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */, + ); + path = Vertices; + sourceTree = ""; + }; + 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 = ""; + }; + ECA738A42BE6022F00A4542E /* Uniforms */ = { + isa = PBXGroup; + children = ( + ECA738A52BE6023F00A4542E /* GridUniforms.swift */, + ECA738A72BE6025900A4542E /* GraphicUniforms.swift */, + ECA738A92BE6026D00A4542E /* Uniforms.swift */, + ); + path = Uniforms; + sourceTree = ""; + }; + ECA738AB2BE60CB500A4542E /* View */ = { + isa = PBXGroup; + children = ( + ECA738AE2BE60CEC00A4542E /* Bridge */, + ECA738B22BE60D9E00A4542E /* CanvasView.swift */, + ); + path = View; + sourceTree = ""; + }; + ECA738AE2BE60CEC00A4542E /* Bridge */ = { + isa = PBXGroup; + children = ( + ECA738AC2BE60CC600A4542E /* DrawingView.swift */, + ECA738AF2BE60D0B00A4542E /* CanvasViewController.swift */, + ); + path = Bridge; + sourceTree = ""; + }; + ECA738B12BE60D8800A4542E /* Tool */ = { + isa = PBXGroup; + children = ( + ECA738C22BE60E7200A4542E /* PenStyles */, + ECA738BD2BE60E2800A4542E /* Pen */, + ECA738BB2BE60E0300A4542E /* Tool.swift */, + ); + path = Tool; + sourceTree = ""; + }; + ECA738B42BE60DC200A4542E /* History */ = { + isa = PBXGroup; + children = ( + ECA738B52BE60DCD00A4542E /* History.swift */, + ECA738B72BE60DDC00A4542E /* HistoryEvent.swift */, + ECA738B92BE60DEF00A4542E /* HistoryAction.swift */, + ); + path = History; + sourceTree = ""; + }; + ECA738BD2BE60E2800A4542E /* Pen */ = { + isa = PBXGroup; + children = ( + ECA738BE2BE60E3400A4542E /* Pen.swift */, + ECA738C02BE60E5300A4542E /* PenStyle.swift */, + ); + path = Pen; + sourceTree = ""; + }; + ECA738C22BE60E7200A4542E /* PenStyles */ = { + isa = PBXGroup; + children = ( + ECA738C32BE60E8800A4542E /* MarkerPenStyle.swift */, + ECA738C52BE60E9D00A4542E /* EraserPenStyle.swift */, + ); + path = PenStyles; + sourceTree = ""; + }; + ECA738C72BE60EE200A4542E /* Contexts */ = { + isa = PBXGroup; + children = ( + ECA738C82BE60EF700A4542E /* GraphicContext.swift */, + ECA738CA2BE60F1900A4542E /* ViewPortContext.swift */, + ECA738CC2BE60F2F00A4542E /* GridContext.swift */, + ); + path = Contexts; + sourceTree = ""; + }; + ECA738CE2BE60F5000A4542E /* Stroke */ = { + isa = PBXGroup; + children = ( + ECA738D52BE60FA200A4542E /* Generators */, + ECA738D12BE60F7B00A4542E /* Stroke.swift */, + ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */, + ); + path = Stroke; + sourceTree = ""; + }; + ECA738D52BE60FA200A4542E /* Generators */ = { + isa = PBXGroup; + children = ( + ECA738D62BE60FC100A4542E /* SolidPointStrokeGenerator.swift */, + ); + path = Generators; + sourceTree = ""; + }; + ECA738D82BE60FE200A4542E /* RenderPasses */ = { + isa = PBXGroup; + children = ( + ECA738D92BE60FF100A4542E /* CacheRenderPass.swift */, + ECA738DB2BE6108D00A4542E /* StrokeRenderPass.swift */, + ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */, + ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */, + ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */, + ); + path = RenderPasses; + sourceTree = ""; + }; + ECA738F92BE6130000A4542E /* Geometries */ = { + isa = PBXGroup; + children = ( + ECA738CE2BE60F5000A4542E /* Stroke */, + ECA738982BE6015700A4542E /* Primitives */, + ); + path = Geometries; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -172,9 +490,59 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; 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 */, + ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */, + ECA738AA2BE6026D00A4542E /* Uniforms.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 */, + 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; }; diff --git a/Memola/Canvas/Abstracts/Drawable.swift b/Memola/Canvas/Abstracts/Drawable.swift new file mode 100644 index 0000000..5debb76 --- /dev/null +++ b/Memola/Canvas/Abstracts/Drawable.swift @@ -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) +} diff --git a/Memola/Canvas/Abstracts/RenderPass.swift b/Memola/Canvas/Abstracts/RenderPass.swift new file mode 100644 index 0000000..56eaf20 --- /dev/null +++ b/Memola/Canvas/Abstracts/RenderPass.swift @@ -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) +} diff --git a/Memola/Canvas/Buffers/Uniforms/GraphicUniforms.swift b/Memola/Canvas/Buffers/Uniforms/GraphicUniforms.swift new file mode 100644 index 0000000..1e67b08 --- /dev/null +++ b/Memola/Canvas/Buffers/Uniforms/GraphicUniforms.swift @@ -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] + } +} diff --git a/Memola/Canvas/Buffers/Uniforms/GridUniforms.swift b/Memola/Canvas/Buffers/Uniforms/GridUniforms.swift new file mode 100644 index 0000000..81fb7ec --- /dev/null +++ b/Memola/Canvas/Buffers/Uniforms/GridUniforms.swift @@ -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 +} diff --git a/Memola/Canvas/Buffers/Uniforms/Uniforms.swift b/Memola/Canvas/Buffers/Uniforms/Uniforms.swift new file mode 100644 index 0000000..40bf99b --- /dev/null +++ b/Memola/Canvas/Buffers/Uniforms/Uniforms.swift @@ -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 +} diff --git a/Memola/Canvas/Buffers/Vertices/GridVertex.swift b/Memola/Canvas/Buffers/Vertices/GridVertex.swift new file mode 100644 index 0000000..646b297 --- /dev/null +++ b/Memola/Canvas/Buffers/Vertices/GridVertex.swift @@ -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] + } +} diff --git a/Memola/Canvas/Buffers/Vertices/QuadVertex.swift b/Memola/Canvas/Buffers/Vertices/QuadVertex.swift new file mode 100644 index 0000000..4c95480 --- /dev/null +++ b/Memola/Canvas/Buffers/Vertices/QuadVertex.swift @@ -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])) + } +} + diff --git a/Memola/Canvas/Buffers/Vertices/ViewPortVertex.swift b/Memola/Canvas/Buffers/Vertices/ViewPortVertex.swift new file mode 100644 index 0000000..2a170ad --- /dev/null +++ b/Memola/Canvas/Buffers/Vertices/ViewPortVertex.swift @@ -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] + } +} diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift new file mode 100644 index 0000000..6c8a24d --- /dev/null +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -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 { 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.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 + } +} diff --git a/Memola/Canvas/Contexts/GridContext.swift b/Memola/Canvas/Contexts/GridContext.swift new file mode 100644 index 0000000..1535f6f --- /dev/null +++ b/Memola/Canvas/Contexts/GridContext.swift @@ -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.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) + } +} diff --git a/Memola/Canvas/Contexts/ViewPortContext.swift b/Memola/Canvas/Contexts/ViewPortContext.swift new file mode 100644 index 0000000..3a00db0 --- /dev/null +++ b/Memola/Canvas/Contexts/ViewPortContext.swift @@ -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.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) + } +} diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift new file mode 100644 index 0000000..77527cc --- /dev/null +++ b/Memola/Canvas/Core/Canvas.swift @@ -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() + + 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.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.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.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 + } +} diff --git a/Memola/Canvas/Core/PipelineStates.swift b/Memola/Canvas/Core/PipelineStates.swift new file mode 100644 index 0000000..7486acd --- /dev/null +++ b/Memola/Canvas/Core/PipelineStates.swift @@ -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) + } +} diff --git a/Memola/Canvas/Core/Renderer.swift b/Memola/Canvas/Core/Renderer.swift new file mode 100644 index 0000000..2853673 --- /dev/null +++ b/Memola/Canvas/Core/Renderer.swift @@ -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) + } +} diff --git a/Memola/Canvas/Core/Textures.swift b/Memola/Canvas/Core/Textures.swift new file mode 100644 index 0000000..addc399 --- /dev/null +++ b/Memola/Canvas/Core/Textures.swift @@ -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 + } +} diff --git a/Memola/Canvas/Geometries/Primitives/Quad.swift b/Memola/Canvas/Geometries/Primitives/Quad.swift new file mode 100644 index 0000000..fda3f2d --- /dev/null +++ b/Memola/Canvas/Geometries/Primitives/Quad.swift @@ -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) + } +} diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift new file mode 100644 index 0000000..5e18882 --- /dev/null +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -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...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) + } +} diff --git a/Memola/Canvas/Geometries/Stroke/StrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/StrokeGenerator.swift new file mode 100644 index 0000000..5c540f8 --- /dev/null +++ b/Memola/Canvas/Geometries/Stroke/StrokeGenerator.swift @@ -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) +} diff --git a/Memola/Canvas/History/History.swift b/Memola/Canvas/History/History.swift new file mode 100644 index 0000000..1cb2279 --- /dev/null +++ b/Memola/Canvas/History/History.swift @@ -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() + + 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() + } +} diff --git a/Memola/Canvas/History/HistoryAction.swift b/Memola/Canvas/History/HistoryAction.swift new file mode 100644 index 0000000..c6012a3 --- /dev/null +++ b/Memola/Canvas/History/HistoryAction.swift @@ -0,0 +1,13 @@ +// +// HistoryAction.swift +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +import Foundation + +enum HistoryAction { + case undo + case redo +} diff --git a/Memola/Canvas/History/HistoryEvent.swift b/Memola/Canvas/History/HistoryEvent.swift new file mode 100644 index 0000000..11ec7a3 --- /dev/null +++ b/Memola/Canvas/History/HistoryEvent.swift @@ -0,0 +1,12 @@ +// +// HistoryEvent.swift +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +import Foundation + +enum HistoryEvent { + case stroke(Stroke) +} diff --git a/Memola/Canvas/RenderPasses/CacheRenderPass.swift b/Memola/Canvas/RenderPasses/CacheRenderPass.swift new file mode 100644 index 0000000..5b3ba3e --- /dev/null +++ b/Memola/Canvas/RenderPasses/CacheRenderPass.swift @@ -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() + } +} diff --git a/Memola/Canvas/RenderPasses/EraserRenderPass.swift b/Memola/Canvas/RenderPasses/EraserRenderPass.swift new file mode 100644 index 0000000..ab4aab8 --- /dev/null +++ b/Memola/Canvas/RenderPasses/EraserRenderPass.swift @@ -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() + } +} diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift new file mode 100644 index 0000000..cdb4099 --- /dev/null +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -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 + } + } +} diff --git a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift new file mode 100644 index 0000000..d1c5de9 --- /dev/null +++ b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift @@ -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.size) + renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11) + canvas.renderGraphic(device: renderer.device, renderEncoder: renderEncoder) + renderEncoder.endEncoding() + commandBuffer.commit() + } +} diff --git a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift new file mode 100644 index 0000000..8589bd2 --- /dev/null +++ b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift @@ -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() + } +} diff --git a/Memola/Canvas/Shaders/Cache.metal b/Memola/Canvas/Shaders/Cache.metal new file mode 100644 index 0000000..6c20cec --- /dev/null +++ b/Memola/Canvas/Shaders/Cache.metal @@ -0,0 +1,18 @@ +// +// Cache.metal +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +#include +using namespace metal; + +kernel void copy_texture_viewport( + texture2d inTexture [[texture(0)]], + texture2d outTexture [[texture(1)]], + uint2 gid [[thread_position_in_grid]] +) { + float4 color = inTexture.read(gid); + outTexture.write(color, gid); +} diff --git a/Memola/Canvas/Shaders/Graphic.metal b/Memola/Canvas/Shaders/Graphic.metal new file mode 100644 index 0000000..c045ccc --- /dev/null +++ b/Memola/Canvas/Shaders/Graphic.metal @@ -0,0 +1,46 @@ +// +// Graphic.metal +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +#include +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 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); +} diff --git a/Memola/Canvas/Shaders/Grid.metal b/Memola/Canvas/Shaders/Grid.metal new file mode 100644 index 0000000..5e6b80d --- /dev/null +++ b/Memola/Canvas/Shaders/Grid.metal @@ -0,0 +1,44 @@ +// +// Grid.metal +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +#include +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; +} diff --git a/Memola/Canvas/Shaders/Stroke.metal b/Memola/Canvas/Shaders/Stroke.metal new file mode 100644 index 0000000..99ec764 --- /dev/null +++ b/Memola/Canvas/Shaders/Stroke.metal @@ -0,0 +1,55 @@ +// +// Stroke.metal +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +#include +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 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); +} diff --git a/Memola/Canvas/Shaders/ViewPort.metal b/Memola/Canvas/Shaders/ViewPort.metal new file mode 100644 index 0000000..c80aa73 --- /dev/null +++ b/Memola/Canvas/Shaders/ViewPort.metal @@ -0,0 +1,45 @@ +// +// Viewport.metal +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +#include +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 offscreenTexture [[texture(0)]] +) { + constexpr sampler textureSampler(mag_filter::linear, min_filter::linear); + float4 sampledColor = float4(offscreenTexture.sample(textureSampler, _vertex.textCoord)); + return sampledColor; +} diff --git a/Memola/Canvas/Tool/Pen/Pen.swift b/Memola/Canvas/Tool/Pen/Pen.swift new file mode 100644 index 0000000..8ef887a --- /dev/null +++ b/Memola/Canvas/Tool/Pen/Pen.swift @@ -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) + } +} diff --git a/Memola/Canvas/Tool/Pen/PenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyle.swift new file mode 100644 index 0000000..618806c --- /dev/null +++ b/Memola/Canvas/Tool/Pen/PenStyle.swift @@ -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) + } +} diff --git a/Memola/Canvas/Tool/PenStyles/EraserPenStyle.swift b/Memola/Canvas/Tool/PenStyles/EraserPenStyle.swift new file mode 100644 index 0000000..53e4698 --- /dev/null +++ b/Memola/Canvas/Tool/PenStyles/EraserPenStyle.swift @@ -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() + } +} diff --git a/Memola/Canvas/Tool/PenStyles/MarkerPenStyle.swift b/Memola/Canvas/Tool/PenStyles/MarkerPenStyle.swift new file mode 100644 index 0000000..ee049e9 --- /dev/null +++ b/Memola/Canvas/Tool/PenStyles/MarkerPenStyle.swift @@ -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() + } +} diff --git a/Memola/Canvas/Tool/Tool.swift b/Memola/Canvas/Tool/Tool.swift new file mode 100644 index 0000000..636dc75 --- /dev/null +++ b/Memola/Canvas/Tool/Tool.swift @@ -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 + } +} diff --git a/Memola/Canvas/View/Bridge/CanvasViewController.swift b/Memola/Canvas/View/Bridge/CanvasViewController.swift new file mode 100644 index 0000000..d52030e --- /dev/null +++ b/Memola/Canvas/View/Bridge/CanvasViewController.swift @@ -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 = [] + + 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() + } +} diff --git a/Memola/Canvas/View/Bridge/DrawingView.swift b/Memola/Canvas/View/Bridge/DrawingView.swift new file mode 100644 index 0000000..8791cf5 --- /dev/null +++ b/Memola/Canvas/View/Bridge/DrawingView.swift @@ -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 + } + } +} diff --git a/Memola/Canvas/View/CanvasView.swift b/Memola/Canvas/View/CanvasView.swift new file mode 100644 index 0000000..3842c6d --- /dev/null +++ b/Memola/Canvas/View/CanvasView.swift @@ -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) { } +} diff --git a/Memola/Extensions/Array++.swift b/Memola/Extensions/Array++.swift new file mode 100644 index 0000000..70cc415 --- /dev/null +++ b/Memola/Extensions/Array++.swift @@ -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) + } +} diff --git a/Memola/Extensions/CGAffineTransform++.swift b/Memola/Extensions/CGAffineTransform++.swift new file mode 100644 index 0000000..3bc2ce5 --- /dev/null +++ b/Memola/Extensions/CGAffineTransform++.swift @@ -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) + } +} diff --git a/Memola/Extensions/CGFloat++.swift b/Memola/Extensions/CGFloat++.swift new file mode 100644 index 0000000..17e3206 --- /dev/null +++ b/Memola/Extensions/CGFloat++.swift @@ -0,0 +1,14 @@ +// +// CGFloat++.swift +// Memola +// +// Created by Dscyre Scotti on 5/4/24. +// + +import Foundation + +extension CGFloat { + var float: Float { + Float(self) + } +} diff --git a/Memola/Extensions/CGPoint++.swift b/Memola/Extensions/CGPoint++.swift new file mode 100644 index 0000000..31acd69 --- /dev/null +++ b/Memola/Extensions/CGPoint++.swift @@ -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) + } +} diff --git a/Memola/Extensions/CGRect++.swift b/Memola/Extensions/CGRect++.swift new file mode 100644 index 0000000..a576e31 --- /dev/null +++ b/Memola/Extensions/CGRect++.swift @@ -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 + } +} diff --git a/Memola/Extensions/CGSize++.swift b/Memola/Extensions/CGSize++.swift new file mode 100644 index 0000000..2b934b2 --- /dev/null +++ b/Memola/Extensions/CGSize++.swift @@ -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) + } +} diff --git a/Memola/Extensions/Collection++.swift b/Memola/Extensions/Collection++.swift new file mode 100644 index 0000000..b301e21 --- /dev/null +++ b/Memola/Extensions/Collection++.swift @@ -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] + } +} diff --git a/Memola/Extensions/Color++.swift b/Memola/Extensions/Color++.swift new file mode 100644 index 0000000..b0e9aa6 --- /dev/null +++ b/Memola/Extensions/Color++.swift @@ -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] + } +} diff --git a/Memola/Extensions/MTLDevice++.swift b/Memola/Extensions/MTLDevice++.swift new file mode 100644 index 0000000..079b800 --- /dev/null +++ b/Memola/Extensions/MTLDevice++.swift @@ -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 + } +} diff --git a/Memola/Extensions/simd_float4x4++.swift b/Memola/Extensions/simd_float4x4++.swift new file mode 100644 index 0000000..b1f2200 --- /dev/null +++ b/Memola/Extensions/simd_float4x4++.swift @@ -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)] + ]) + } +} diff --git a/Memola/Assets.xcassets/AccentColor.colorset/Contents.json b/Memola/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Memola/Assets.xcassets/AccentColor.colorset/Contents.json rename to Memola/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Memola/Assets.xcassets/AppIcon.appiconset/Contents.json b/Memola/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Memola/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Memola/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Memola/Assets.xcassets/Contents.json b/Memola/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Memola/Assets.xcassets/Contents.json rename to Memola/Resources/Assets.xcassets/Contents.json