mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-07-13 00:02:43 +02:00
Merge pull request #22 from dscyrescotti/feature/memory
Optimize memory usage for huge number of strokes
This commit is contained in:
@@ -7,6 +7,12 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; };
|
||||||
|
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; };
|
||||||
|
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; };
|
||||||
|
EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; };
|
||||||
|
EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; };
|
||||||
|
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; };
|
||||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
|
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
|
||||||
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
|
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
|
||||||
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
||||||
@@ -61,14 +67,24 @@
|
|||||||
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; };
|
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; };
|
||||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
|
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
|
||||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
||||||
ECA738F82BE612EB00A4542E /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* Quad.swift */; };
|
|
||||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
|
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
|
||||||
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; };
|
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; };
|
||||||
ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; };
|
|
||||||
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; };
|
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; };
|
||||||
|
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; };
|
||||||
|
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; };
|
||||||
|
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15212BEF21F500455818 /* CanvasObject.swift */; };
|
||||||
|
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15232BEF223300455818 /* GraphicContextObject.swift */; };
|
||||||
|
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15252BEF224900455818 /* StrokeObject.swift */; };
|
||||||
|
ECFA15282BEF225000455818 /* QuadObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15272BEF225000455818 /* QuadObject.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = "<group>"; };
|
||||||
|
EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = "<group>"; };
|
||||||
|
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = "<group>"; };
|
||||||
|
EC3565592BF060D900A4E0BF /* Quad.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Quad.metal; sourceTree = "<group>"; };
|
||||||
|
EC35655B2BF0712A00A4E0BF /* Float++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = "<group>"; };
|
||||||
|
EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
|
||||||
EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = "<group>"; };
|
EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = "<group>"; };
|
||||||
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
@@ -124,11 +140,15 @@
|
|||||||
ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = "<group>"; };
|
ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = "<group>"; };
|
||||||
ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; };
|
ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; };
|
||||||
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
|
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
|
||||||
ECA738F72BE612EB00A4542E /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
|
|
||||||
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
|
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
|
||||||
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
|
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
|
||||||
ECA739042BE61E3100A4542E /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = "<group>"; };
|
|
||||||
ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; };
|
ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; };
|
||||||
|
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = "<group>"; };
|
||||||
|
ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = "<group>"; };
|
||||||
|
ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = "<group>"; };
|
||||||
|
ECFA15232BEF223300455818 /* GraphicContextObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContextObject.swift; sourceTree = "<group>"; };
|
||||||
|
ECFA15252BEF224900455818 /* StrokeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeObject.swift; sourceTree = "<group>"; };
|
||||||
|
ECFA15272BEF225000455818 /* QuadObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadObject.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -284,6 +304,7 @@
|
|||||||
ECA738922BE6011100A4542E /* Stroke.metal */,
|
ECA738922BE6011100A4542E /* Stroke.metal */,
|
||||||
ECA738942BE6012D00A4542E /* ViewPort.metal */,
|
ECA738942BE6012D00A4542E /* ViewPort.metal */,
|
||||||
ECA738962BE6014200A4542E /* Graphic.metal */,
|
ECA738962BE6014200A4542E /* Graphic.metal */,
|
||||||
|
EC3565592BF060D900A4E0BF /* Quad.metal */,
|
||||||
);
|
);
|
||||||
path = Shaders;
|
path = Shaders;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -291,7 +312,8 @@
|
|||||||
ECA738982BE6015700A4542E /* Primitives */ = {
|
ECA738982BE6015700A4542E /* Primitives */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ECA738F72BE612EB00A4542E /* Quad.swift */,
|
EC4538882BEBCAE000A86FEC /* Quad.swift */,
|
||||||
|
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */,
|
||||||
);
|
);
|
||||||
path = Primitives;
|
path = Primitives;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -328,6 +350,10 @@
|
|||||||
ECA738E72BE6120F00A4542E /* Color++.swift */,
|
ECA738E72BE6120F00A4542E /* Color++.swift */,
|
||||||
ECA738F52BE612B700A4542E /* MTLDevice++.swift */,
|
ECA738F52BE612B700A4542E /* MTLDevice++.swift */,
|
||||||
ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */,
|
ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */,
|
||||||
|
EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */,
|
||||||
|
EC3565532BEFC6AD00A4E0BF /* View++.swift */,
|
||||||
|
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */,
|
||||||
|
EC35655B2BF0712A00A4E0BF /* Float++.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -449,7 +475,7 @@
|
|||||||
ECA738FA2BE61B1700A4542E /* Persistence */ = {
|
ECA738FA2BE61B1700A4542E /* Persistence */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ECA739032BE61E2600A4542E /* Entities */,
|
ECFA151E2BEF21BE00455818 /* Objects */,
|
||||||
ECA739022BE61DE700A4542E /* Core */,
|
ECA739022BE61DE700A4542E /* Core */,
|
||||||
);
|
);
|
||||||
path = Persistence;
|
path = Persistence;
|
||||||
@@ -479,14 +505,6 @@
|
|||||||
path = Core;
|
path = Core;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
ECA739032BE61E2600A4542E /* Entities */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
ECA739042BE61E3100A4542E /* Memo.swift */,
|
|
||||||
);
|
|
||||||
path = Entities;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
ECA739062BE61F7500A4542E /* Core */ = {
|
ECA739062BE61F7500A4542E /* Core */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -496,6 +514,18 @@
|
|||||||
path = Core;
|
path = Core;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
ECFA151E2BEF21BE00455818 /* Objects */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECFA151F2BEF21EF00455818 /* MemoObject.swift */,
|
||||||
|
ECFA15212BEF21F500455818 /* CanvasObject.swift */,
|
||||||
|
ECFA15232BEF223300455818 /* GraphicContextObject.swift */,
|
||||||
|
ECFA15252BEF224900455818 /* StrokeObject.swift */,
|
||||||
|
ECFA15272BEF225000455818 /* QuadObject.swift */,
|
||||||
|
);
|
||||||
|
path = Objects;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -570,23 +600,30 @@
|
|||||||
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
|
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
|
||||||
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
|
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
|
||||||
ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */,
|
ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */,
|
||||||
|
EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */,
|
||||||
ECA738912BE600F500A4542E /* Cache.metal in Sources */,
|
ECA738912BE600F500A4542E /* Cache.metal in Sources */,
|
||||||
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
|
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
|
||||||
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
|
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
|
||||||
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
|
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
|
||||||
|
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */,
|
||||||
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
|
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
|
||||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
|
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
|
||||||
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
|
ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */,
|
||||||
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
|
ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */,
|
||||||
|
EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */,
|
||||||
|
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */,
|
||||||
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
||||||
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
|
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
|
||||||
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
|
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
|
||||||
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
|
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
|
||||||
|
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
|
||||||
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
|
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
|
||||||
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
|
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
|
||||||
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
||||||
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
|
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
|
||||||
|
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */,
|
||||||
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
||||||
|
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
|
||||||
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
||||||
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
|
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
|
||||||
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
|
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
|
||||||
@@ -598,11 +635,14 @@
|
|||||||
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
||||||
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */,
|
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */,
|
||||||
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
||||||
|
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
|
||||||
|
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */,
|
||||||
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
|
ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */,
|
||||||
|
EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */,
|
||||||
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
|
ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */,
|
||||||
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
|
ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */,
|
||||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
||||||
ECA739052BE61E3100A4542E /* Memo.swift in Sources */,
|
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
||||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
||||||
@@ -610,6 +650,7 @@
|
|||||||
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
||||||
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
||||||
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */,
|
ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */,
|
||||||
|
ECFA15282BEF225000455818 /* QuadObject.swift in Sources */,
|
||||||
ECA738932BE6011100A4542E /* Stroke.metal in Sources */,
|
ECA738932BE6011100A4542E /* Stroke.metal in Sources */,
|
||||||
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
|
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
|
||||||
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
|
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
|
||||||
@@ -620,7 +661,6 @@
|
|||||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||||
ECA738F82BE612EB00A4542E /* Quad.swift in Sources */,
|
|
||||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1530"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EC7F6BE72BE5E6E300A34A7B"
|
||||||
|
BuildableName = "Memola.app"
|
||||||
|
BlueprintName = "Memola"
|
||||||
|
ReferencedContainer = "container:Memola.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EC7F6BE72BE5E6E300A34A7B"
|
||||||
|
BuildableName = "Memola.app"
|
||||||
|
BlueprintName = "Memola"
|
||||||
|
ReferencedContainer = "container:Memola.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EC7F6BE72BE5E6E300A34A7B"
|
||||||
|
BuildableName = "Memola.app"
|
||||||
|
BlueprintName = "Memola"
|
||||||
|
ReferencedContainer = "container:Memola.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -12,7 +12,7 @@ struct MemolaApp: App {
|
|||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MemosView()
|
MemosView()
|
||||||
.environment(\.managedObjectContext, Persistence.shared.viewContext)
|
.persistence(\.viewContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,45 +7,25 @@
|
|||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import MetalKit
|
import MetalKit
|
||||||
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol GraphicContextDelegate: AnyObject {
|
final class GraphicContext: @unchecked Sendable {
|
||||||
var didUpdate: PassthroughSubject<Void, Never> { get set }
|
|
||||||
}
|
|
||||||
|
|
||||||
class GraphicContext: Codable {
|
|
||||||
var strokes: [Stroke] = []
|
var strokes: [Stroke] = []
|
||||||
|
var object: GraphicContextObject?
|
||||||
|
|
||||||
var currentStroke: Stroke?
|
var currentStroke: Stroke?
|
||||||
var previousStroke: Stroke?
|
var previousStroke: Stroke?
|
||||||
var currentPoint: CGPoint?
|
var currentPoint: CGPoint?
|
||||||
|
|
||||||
var renderType: RenderType = .finished
|
var renderType: RenderType = .finished
|
||||||
|
|
||||||
var vertices: [ViewPortVertex] = []
|
var vertices: [ViewPortVertex] = []
|
||||||
var vertexCount: Int = 4
|
var vertexCount: Int = 4
|
||||||
var vertexBuffer: MTLBuffer?
|
var vertexBuffer: MTLBuffer?
|
||||||
|
|
||||||
weak var delegate: GraphicContextDelegate?
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
setViewPortVertices()
|
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() {
|
func setViewPortVertices() {
|
||||||
vertexBuffer = nil
|
vertexBuffer = nil
|
||||||
vertices = [
|
vertices = [
|
||||||
@@ -58,18 +38,39 @@ class GraphicContext: Codable {
|
|||||||
|
|
||||||
func undoGraphic() {
|
func undoGraphic() {
|
||||||
guard !strokes.isEmpty else { return }
|
guard !strokes.isEmpty else { return }
|
||||||
strokes.removeLast()
|
let stroke = strokes.removeLast()
|
||||||
|
withPersistence(\.backgroundContext) { [stroke] context in
|
||||||
|
stroke.object?.graphicContext = nil
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
previousStroke = nil
|
previousStroke = nil
|
||||||
delegate?.didUpdate.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func redoGraphic(for event: HistoryEvent) {
|
func redoGraphic(for event: HistoryEvent) {
|
||||||
switch event {
|
switch event {
|
||||||
case .stroke(let stroke):
|
case .stroke(let stroke):
|
||||||
strokes.append(stroke)
|
strokes.append(stroke)
|
||||||
|
withPersistence(\.backgroundContext) { [weak self, stroke] context in
|
||||||
|
stroke.object?.graphicContext = self?.object
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
previousStroke = nil
|
previousStroke = nil
|
||||||
}
|
}
|
||||||
delegate?.didUpdate.send()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GraphicContext {
|
||||||
|
func loadStrokes() {
|
||||||
|
guard let object else { return }
|
||||||
|
self.strokes = object.strokes.compactMap { stroke -> Stroke? in
|
||||||
|
guard let stroke = stroke as? StrokeObject else { return nil }
|
||||||
|
let _stroke = Stroke(object: stroke)
|
||||||
|
_stroke.loadQuads()
|
||||||
|
withPersistence(\.backgroundContext) { [stroke] context in
|
||||||
|
context.refresh(stroke, mergeChanges: false)
|
||||||
|
}
|
||||||
|
return _stroke
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +94,21 @@ extension GraphicContext {
|
|||||||
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
|
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
|
||||||
let stroke = Stroke(
|
let stroke = Stroke(
|
||||||
color: pen.color,
|
color: pen.color,
|
||||||
style: pen.style,
|
style: pen.strokeStyle.rawValue,
|
||||||
|
createdAt: .now,
|
||||||
thickness: pen.thickness
|
thickness: pen.thickness
|
||||||
)
|
)
|
||||||
|
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in
|
||||||
|
let stroke = StrokeObject(\.backgroundContext)
|
||||||
|
stroke.color = _stroke.color
|
||||||
|
stroke.style = _stroke.style
|
||||||
|
stroke.thickness = _stroke.thickness
|
||||||
|
stroke.createdAt = _stroke.createdAt
|
||||||
|
stroke.quads = []
|
||||||
|
stroke.graphicContext = graphicContext
|
||||||
|
graphicContext?.strokes.add(stroke)
|
||||||
|
_stroke.object = stroke
|
||||||
|
}
|
||||||
strokes.append(stroke)
|
strokes.append(stroke)
|
||||||
currentStroke = stroke
|
currentStroke = stroke
|
||||||
currentPoint = point
|
currentPoint = point
|
||||||
@@ -105,23 +118,38 @@ extension GraphicContext {
|
|||||||
|
|
||||||
func appendStroke(with point: CGPoint) {
|
func appendStroke(with point: CGPoint) {
|
||||||
guard let currentStroke else { return }
|
guard let currentStroke else { return }
|
||||||
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.style.stepRate else { return }
|
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.anyPenStyle.stepRate else { return }
|
||||||
currentStroke.append(to: point)
|
currentStroke.append(to: point)
|
||||||
self.currentPoint = point
|
self.currentPoint = point
|
||||||
}
|
}
|
||||||
|
|
||||||
func endStroke(at point: CGPoint) {
|
func endStroke(at point: CGPoint) {
|
||||||
guard currentPoint != nil else { return }
|
guard currentPoint != nil, let currentStroke else { return }
|
||||||
currentStroke?.finish(at: point)
|
currentStroke.finish(at: point)
|
||||||
|
let saveIndex = currentStroke.batchIndex
|
||||||
|
let quads = Array(currentStroke.quads[saveIndex..<currentStroke.quads.count])
|
||||||
|
withPersistence(\.backgroundContext) { [currentStroke, quads] context in
|
||||||
|
currentStroke.saveQuads(for: quads)
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
if let stroke = currentStroke.object {
|
||||||
|
context.refresh(stroke, mergeChanges: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
previousStroke = currentStroke
|
previousStroke = currentStroke
|
||||||
currentStroke = nil
|
self.currentStroke = nil
|
||||||
self.currentPoint = nil
|
self.currentPoint = nil
|
||||||
delegate?.didUpdate.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelStroke() {
|
func cancelStroke() {
|
||||||
if !strokes.isEmpty {
|
if !strokes.isEmpty {
|
||||||
strokes.removeLast()
|
let stroke = strokes.removeLast()
|
||||||
|
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in
|
||||||
|
if let stroke = _stroke.object {
|
||||||
|
graphicContext?.strokes.remove(stroke)
|
||||||
|
context.delete(stroke)
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
currentStroke = nil
|
currentStroke = nil
|
||||||
currentPoint = nil
|
currentPoint = nil
|
||||||
|
|||||||
@@ -10,27 +10,28 @@ import CoreData
|
|||||||
import MetalKit
|
import MetalKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicContextDelegate {
|
final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
|
||||||
let size: CGSize
|
let size: CGSize
|
||||||
let maximumZoomScale: CGFloat = 25
|
let canvasID: NSManagedObjectID
|
||||||
let minimumZoomScale: CGFloat = 3.1
|
|
||||||
|
|
||||||
var transform: simd_float4x4 = .init()
|
|
||||||
|
|
||||||
var uniformsBuffer: MTLBuffer?
|
|
||||||
|
|
||||||
let gridContext = GridContext()
|
let gridContext = GridContext()
|
||||||
var graphicContext = GraphicContext()
|
var graphicContext = GraphicContext()
|
||||||
let viewPortContext = ViewPortContext()
|
let viewPortContext = ViewPortContext()
|
||||||
|
|
||||||
|
let maximumZoomScale: CGFloat = 28
|
||||||
|
let minimumZoomScale: CGFloat = 3.1
|
||||||
|
|
||||||
|
var transform: simd_float4x4 = .init()
|
||||||
var clipBounds: CGRect = .zero
|
var clipBounds: CGRect = .zero
|
||||||
var zoomScale: CGFloat = .zero
|
var zoomScale: CGFloat = .zero
|
||||||
|
var uniformsBuffer: MTLBuffer?
|
||||||
|
|
||||||
weak var memo: Memo?
|
init(size: CGSize, canvasID: NSManagedObjectID) {
|
||||||
var graphicLoader: (() throws -> GraphicContext)?
|
self.size = size
|
||||||
|
self.canvasID = canvasID
|
||||||
|
}
|
||||||
|
|
||||||
@Published var state: State = .initial
|
@Published var state: State = .initial
|
||||||
lazy var didUpdate = PassthroughSubject<Void, Never>()
|
|
||||||
|
|
||||||
var hasValidStroke: Bool {
|
var hasValidStroke: Bool {
|
||||||
if let currentStroke = graphicContext.currentStroke {
|
if let currentStroke = graphicContext.currentStroke {
|
||||||
@@ -38,65 +39,27 @@ final class Canvas: NSObject, ObservableObject, Identifiable, Codable, GraphicCo
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
init(size: CGSize = .init(width: 4_000, height: 4_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
|
// MARK: - Actions
|
||||||
extension Canvas {
|
extension Canvas {
|
||||||
func load() {
|
func load() {
|
||||||
guard let graphicLoader else { return }
|
withPersistence(\.backgroundContext) { [weak self, canvasID] context in
|
||||||
Task(priority: .high) { [unowned self, graphicLoader] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
await MainActor.run {
|
self?.state = .loading
|
||||||
self.state = .loading
|
|
||||||
}
|
}
|
||||||
do {
|
guard let canvas = context.object(with: canvasID) as? CanvasObject else {
|
||||||
let graphicContext = try graphicLoader()
|
return
|
||||||
graphicContext.delegate = self
|
}
|
||||||
await MainActor.run {
|
let graphicContext = canvas.graphicContext
|
||||||
self.graphicContext = graphicContext
|
self?.graphicContext.object = graphicContext
|
||||||
self.state = .loaded
|
self?.graphicContext.loadStrokes()
|
||||||
}
|
context.refresh(canvas, mergeChanges: false)
|
||||||
} catch {
|
DispatchQueue.main.async { [weak self] in
|
||||||
NSLog("[Memola] - \(error.localizedDescription)")
|
self?.state = .loaded
|
||||||
await MainActor.run {
|
|
||||||
self.state = .failed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func save(on managedObjectContext: NSManagedObjectContext) async {
|
|
||||||
guard let memo else { return }
|
|
||||||
do {
|
|
||||||
memo.data = try JSONEncoder().encode(self)
|
|
||||||
memo.updatedAt = Date()
|
|
||||||
try managedObjectContext.save()
|
|
||||||
} catch {
|
|
||||||
NSLog("[Memola] - \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func listen(on managedObjectContext: NSManagedObjectContext) {
|
|
||||||
// Task(priority: .utility) { [unowned self] in
|
|
||||||
// for await _ in didUpdate.throttle(for: 500, scheduler: DispatchQueue.global(qos: .utility), latest: false).values {
|
|
||||||
// await save(on: managedObjectContext)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Dimension
|
// MARK: - Dimension
|
||||||
|
|||||||
@@ -131,4 +131,13 @@ struct PipelineStates {
|
|||||||
}
|
}
|
||||||
return try? device.makeComputePipelineState(function: function)
|
return try? device.makeComputePipelineState(function: function)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func createQuadPipelineState(from renderer: Renderer) -> MTLComputePipelineState? {
|
||||||
|
let device = renderer.device
|
||||||
|
let library = renderer.library
|
||||||
|
guard let function = library.makeFunction(name: "generate_stroke_vertices") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return try? device.makeComputePipelineState(function: function)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,105 +2,55 @@
|
|||||||
// Quad.swift
|
// Quad.swift
|
||||||
// Memola
|
// Memola
|
||||||
//
|
//
|
||||||
// Created by Dscyre Scotti on 5/4/24.
|
// Created by Dscyre Scotti on 5/8/24.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
import MetalKit
|
import MetalKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Quad {
|
struct Quad {
|
||||||
var origin: CGPoint
|
var originX: Float
|
||||||
var color: [CGFloat]
|
var originY: Float
|
||||||
var size: CGFloat
|
var size: Float
|
||||||
var rotation: CGFloat
|
var rotation: Float
|
||||||
var vertices: [QuadVertex] = []
|
var shape: Int16
|
||||||
|
var color: vector_float4
|
||||||
|
|
||||||
var vertexBuffer: MTLBuffer?
|
init(object: QuadObject) {
|
||||||
var vertexCount: Int = 0
|
self.originX = object.originX.float
|
||||||
|
self.originY = object.originY.float
|
||||||
init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: Shape = .rounded) {
|
self.size = object.size.float
|
||||||
self.origin = origin
|
self.rotation = object.rotation.float
|
||||||
self.size = size
|
self.shape = object.shape
|
||||||
self.color = color
|
self.color = [
|
||||||
self.rotation = rotation
|
object.color[0].float,
|
||||||
generateVertices(shape)
|
object.color[1].float,
|
||||||
}
|
object.color[2].float,
|
||||||
|
object.color[3].float
|
||||||
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() {
|
init(origin: CGPoint, size: CGFloat, rotation: CGFloat, shape: Int16, color: [CGFloat]) {
|
||||||
let vHalfSize = size * 0.5
|
self.originX = origin.x.float
|
||||||
let hHalfSize = size * 0.15
|
self.originY = origin.y.float
|
||||||
vertices = [
|
self.size = size.float
|
||||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
self.rotation = rotation.float
|
||||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
self.shape = shape
|
||||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
self.color = [color[0].float, color[1].float, color[2].float, color[3].float]
|
||||||
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 {
|
extension Quad {
|
||||||
enum Shape {
|
var origin: CGPoint {
|
||||||
case rounded
|
get { CGPoint(x: originX.cgFloat, y: originY.cgFloat) }
|
||||||
case squared
|
set {
|
||||||
case calligraphic(CGFloat, CGFloat)
|
originX = newValue.x.float
|
||||||
case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat)
|
originY = newValue.y.float
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColor() -> [CGFloat] {
|
||||||
|
[color.x.cgFloat, color.y.cgFloat, color.z.cgFloat, color.w.cgFloat]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// QuadShape.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/10/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum QuadShape: Int16 {
|
||||||
|
case rounded
|
||||||
|
case squared
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
let control = CGPoint.middle(p1: start, p2: end)
|
let control = CGPoint.middle(p1: start, p2: end)
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
case 3:
|
case 3:
|
||||||
discardPoints(upto: stroke.vertexIndex, on: stroke)
|
stroke.removeQuads(from: stroke.quadIndex + 1)
|
||||||
let index = stroke.keyPoints.count - 1
|
let index = stroke.keyPoints.count - 1
|
||||||
var start = stroke.keyPoints[index - 2]
|
var start = stroke.keyPoints[index - 2]
|
||||||
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
@@ -62,7 +62,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func smoothOutPath(on stroke: Stroke) {
|
private func smoothOutPath(on stroke: Stroke) {
|
||||||
discardPoints(upto: stroke.vertexIndex, on: stroke)
|
stroke.removeQuads(from: stroke.quadIndex + 1)
|
||||||
adjustPreviousKeyPoint(on: stroke)
|
adjustPreviousKeyPoint(on: stroke)
|
||||||
switch stroke.keyPoints.count {
|
switch stroke.keyPoints.count {
|
||||||
case 4:
|
case 4:
|
||||||
@@ -79,7 +79,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
}
|
}
|
||||||
stroke.vertexIndex = stroke.vertices.endIndex - 1
|
stroke.quadIndex = stroke.quads.count - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private func adjustPreviousKeyPoint(on stroke: Stroke) {
|
private func adjustPreviousKeyPoint(on stroke: Stroke) {
|
||||||
@@ -105,9 +105,8 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
case .random:
|
case .random:
|
||||||
rotation = CGFloat.random(in: 0...360) * .pi / 180
|
rotation = CGFloat.random(in: 0...360) * .pi / 180
|
||||||
}
|
}
|
||||||
let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation)
|
let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded)
|
||||||
stroke.vertices.append(contentsOf: quad.vertices)
|
stroke.quads.append(quad)
|
||||||
stroke.vertexCount = stroke.vertices.endIndex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) {
|
private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) {
|
||||||
@@ -115,9 +114,9 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
let factor: CGFloat
|
let factor: CGFloat
|
||||||
switch configuration.granularity {
|
switch configuration.granularity {
|
||||||
case .automatic:
|
case .automatic:
|
||||||
factor = min(6, 1 / (stroke.thickness * 10 / 500))
|
factor = min(5, 1 / (stroke.thickness * 1 / 50))
|
||||||
case .fixed:
|
case .fixed:
|
||||||
factor = 1 / (stroke.thickness * stroke.style.stepRate)
|
factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate)
|
||||||
case .none:
|
case .none:
|
||||||
factor = 1 / (stroke.thickness * 10 / 500)
|
factor = 1 / (stroke.thickness * 10 / 500)
|
||||||
}
|
}
|
||||||
@@ -130,29 +129,6 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
addPoint(point, on: stroke)
|
addPoint(point, on: stroke)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#warning("TODO: remove later")
|
|
||||||
private func addLine(from start: CGPoint, to end: CGPoint, on stroke: Stroke) {
|
|
||||||
let distance = end.distance(to: start)
|
|
||||||
let segments = max(distance / stroke.style.stepRate, 2)
|
|
||||||
for i in 0..<Int(segments) {
|
|
||||||
let i = CGFloat(i)
|
|
||||||
let x = start.x + (end.x - start.x) * (i / segments)
|
|
||||||
let y = start.y + (end.y - start.y) * (i / segments)
|
|
||||||
let point = CGPoint(x: x, y: y)
|
|
||||||
addPoint(point, on: stroke)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func discardPoints(upto index: Int, on stroke: Stroke) {
|
|
||||||
if index < 0 {
|
|
||||||
stroke.vertices.removeAll()
|
|
||||||
} else {
|
|
||||||
let count = stroke.vertices.endIndex
|
|
||||||
let dropCount = count - (max(0, index) + 1)
|
|
||||||
stroke.vertices.removeLast(dropCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SolidPointStrokeGenerator {
|
extension SolidPointStrokeGenerator {
|
||||||
|
|||||||
@@ -6,100 +6,125 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import MetalKit
|
import MetalKit
|
||||||
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Stroke: Codable {
|
final class Stroke: @unchecked Sendable {
|
||||||
|
var object: StrokeObject?
|
||||||
var color: [CGFloat]
|
var color: [CGFloat]
|
||||||
var style: any PenStyle
|
var style: Int16
|
||||||
|
var createdAt: Date
|
||||||
var thickness: CGFloat
|
var thickness: CGFloat
|
||||||
var angle: CGFloat = 0
|
var quads: [Quad]
|
||||||
|
|
||||||
var vertexIndex: Int = -1
|
init(object: StrokeObject) {
|
||||||
|
self.object = object
|
||||||
|
self.color = object.color
|
||||||
|
self.style = object.style
|
||||||
|
self.createdAt = object.createdAt
|
||||||
|
self.thickness = object.thickness
|
||||||
|
self.quads = []
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
color: [CGFloat],
|
||||||
|
style: Int16,
|
||||||
|
createdAt: Date,
|
||||||
|
thickness: CGFloat,
|
||||||
|
quads: [Quad] = []
|
||||||
|
) {
|
||||||
|
self.color = color
|
||||||
|
self.style = style
|
||||||
|
self.createdAt = createdAt
|
||||||
|
self.thickness = thickness
|
||||||
|
self.quads = quads
|
||||||
|
}
|
||||||
|
|
||||||
|
var angle: CGFloat = 0
|
||||||
|
var penStyle: Style {
|
||||||
|
Style(rawValue: style) ?? .marker
|
||||||
|
}
|
||||||
|
|
||||||
|
var batchIndex: Int = 0
|
||||||
|
var quadIndex: Int = -1
|
||||||
var keyPoints: [CGPoint] = []
|
var keyPoints: [CGPoint] = []
|
||||||
var thicknessFactor: CGFloat = 0.7
|
var thicknessFactor: CGFloat = 0.7
|
||||||
|
|
||||||
var vertices: [QuadVertex] = []
|
|
||||||
var vertexBuffer: MTLBuffer?
|
var vertexBuffer: MTLBuffer?
|
||||||
var vertexCount: Int = 0
|
|
||||||
|
|
||||||
let createdAt: Date = Date()
|
|
||||||
|
|
||||||
var texture: MTLTexture?
|
var texture: MTLTexture?
|
||||||
|
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
vertices.isEmpty
|
quads.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
var isEraserPenStyle: Bool {
|
var isEraserPenStyle: Bool {
|
||||||
style is EraserPenStyle
|
penStyle == .eraser
|
||||||
}
|
|
||||||
|
|
||||||
init(color: [CGFloat], style: any PenStyle, thickness: CGFloat) {
|
|
||||||
self.color = color
|
|
||||||
self.style = style
|
|
||||||
self.thickness = thickness
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CodingKeys: CodingKey {
|
|
||||||
case color
|
|
||||||
case style
|
|
||||||
case thickness
|
|
||||||
case vertices
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
color = try container.decode([CGFloat].self, forKey: .color)
|
|
||||||
let style: String = try container.decode(String.self, forKey: .style)
|
|
||||||
thickness = try container.decode(CGFloat.self, forKey: .thickness)
|
|
||||||
vertices = try container.decode([QuadVertex].self, forKey: .vertices)
|
|
||||||
vertexCount = vertices.count
|
|
||||||
switch style {
|
|
||||||
case "marker":
|
|
||||||
self.style = .marker
|
|
||||||
case "eraser":
|
|
||||||
self.style = .eraser
|
|
||||||
default:
|
|
||||||
throw DecodingError.valueNotFound(PenStyle.self, .init(codingPath: [CodingKeys.style], debugDescription: "There is no pen style called `\(style)`."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(color, forKey: .color)
|
|
||||||
try container.encode(thickness, forKey: .thickness)
|
|
||||||
try container.encode(vertices, forKey: .vertices)
|
|
||||||
let styleName: String
|
|
||||||
switch style {
|
|
||||||
case is MarkerPenStyle:
|
|
||||||
styleName = "marker"
|
|
||||||
case is EraserPenStyle:
|
|
||||||
styleName = "eraser"
|
|
||||||
default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
try container.encode(styleName, forKey: .style)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func begin(at point: CGPoint) {
|
func begin(at point: CGPoint) {
|
||||||
style.generator.begin(at: point, on: self)
|
penStyle.anyPenStyle.generator.begin(at: point, on: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func append(to point: CGPoint) {
|
func append(to point: CGPoint) {
|
||||||
style.generator.append(to: point, on: self)
|
penStyle.anyPenStyle.generator.append(to: point, on: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func finish(at point: CGPoint) {
|
func finish(at point: CGPoint) {
|
||||||
style.generator.finish(at: point, on: self)
|
penStyle.anyPenStyle.generator.finish(at: point, on: self)
|
||||||
|
keyPoints.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Stroke {
|
||||||
|
func loadQuads() {
|
||||||
|
guard let object else { return }
|
||||||
|
quads = object.quads.compactMap { quad in
|
||||||
|
guard let quad = quad as? QuadObject else { return nil }
|
||||||
|
return Quad(object: quad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad {
|
||||||
|
let quad = Quad(
|
||||||
|
origin: point,
|
||||||
|
size: thickness,
|
||||||
|
rotation: rotation,
|
||||||
|
shape: shape.rawValue,
|
||||||
|
color: color
|
||||||
|
)
|
||||||
|
quads.append(quad)
|
||||||
|
return quad
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeQuads(from index: Int) {
|
||||||
|
let dropCount = quads.endIndex - max(1, index)
|
||||||
|
quads.removeLast(dropCount)
|
||||||
|
let quads = Array(quads[batchIndex..<index])
|
||||||
|
batchIndex = index
|
||||||
|
withPersistence(\.backgroundContext) { [weak self, quads] context in
|
||||||
|
self?.saveQuads(for: quads)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveQuads(for quads: [Quad]) {
|
||||||
|
for _quad in quads {
|
||||||
|
let quad = QuadObject(\.backgroundContext)
|
||||||
|
quad.originX = _quad.originX.cgFloat
|
||||||
|
quad.originY = _quad.originY.cgFloat
|
||||||
|
quad.size = _quad.size.cgFloat
|
||||||
|
quad.rotation = _quad.rotation.cgFloat
|
||||||
|
quad.shape = _quad.shape
|
||||||
|
quad.color = _quad.getColor()
|
||||||
|
quad.stroke = object
|
||||||
|
object?.quads.add(quad)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Stroke: Drawable {
|
extension Stroke: Drawable {
|
||||||
func prepare(device: MTLDevice) {
|
func prepare(device: MTLDevice) {
|
||||||
if texture == nil {
|
if texture == nil {
|
||||||
texture = style.loadTexture(on: device)
|
texture = penStyle.anyPenStyle.loadTexture(on: device)
|
||||||
}
|
}
|
||||||
vertexBuffer = device.makeBuffer(bytes: &vertices, length: MemoryLayout<QuadVertex>.stride * vertexCount, options: .cpuCacheModeWriteCombined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
@@ -107,6 +132,23 @@ extension Stroke: Drawable {
|
|||||||
prepare(device: device)
|
prepare(device: device)
|
||||||
renderEncoder.setFragmentTexture(texture, index: 0)
|
renderEncoder.setFragmentTexture(texture, index: 0)
|
||||||
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount)
|
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quads.endIndex * 6)
|
||||||
|
vertexBuffer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Stroke {
|
||||||
|
enum Style: Int16 {
|
||||||
|
case marker
|
||||||
|
case eraser
|
||||||
|
|
||||||
|
var anyPenStyle: any PenStyle {
|
||||||
|
switch self {
|
||||||
|
case .marker:
|
||||||
|
return MarkerPenStyle.marker
|
||||||
|
case .eraser:
|
||||||
|
return EraserPenStyle.eraser
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,17 @@ class History: ObservableObject {
|
|||||||
|
|
||||||
func resetRedo() {
|
func resetRedo() {
|
||||||
redoCache = redoStack
|
redoCache = redoStack
|
||||||
|
for event in redoStack {
|
||||||
|
switch event {
|
||||||
|
case .stroke(let _stroke):
|
||||||
|
withPersistence(\.backgroundContext) { context in
|
||||||
|
if let stroke = _stroke.object {
|
||||||
|
context.delete(stroke)
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
redoStack.removeAll()
|
redoStack.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ class CacheRenderPass: RenderPass {
|
|||||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
descriptor.colorAttachments[0].storeAction = .store
|
descriptor.colorAttachments[0].storeAction = .store
|
||||||
|
|
||||||
if let stroke = canvas.graphicContext.currentStroke {
|
let graphicContext = canvas.graphicContext
|
||||||
|
if let stroke = graphicContext.currentStroke {
|
||||||
if stroke.isEraserPenStyle {
|
if stroke.isEraserPenStyle {
|
||||||
eraserRenderPass.stroke = stroke
|
eraserRenderPass.stroke = stroke
|
||||||
eraserRenderPass.descriptor = descriptor
|
eraserRenderPass.descriptor = descriptor
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ class GraphicRenderPass: RenderPass {
|
|||||||
guard let descriptor else { return }
|
guard let descriptor else { return }
|
||||||
|
|
||||||
guard let graphicPipelineState else { return }
|
guard let graphicPipelineState else { return }
|
||||||
let graphicContext = canvas.graphicContext
|
|
||||||
guard let graphicTexture else { return }
|
guard let graphicTexture else { return }
|
||||||
|
|
||||||
descriptor.colorAttachments[0].texture = graphicTexture
|
descriptor.colorAttachments[0].texture = graphicTexture
|
||||||
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||||
descriptor.colorAttachments[0].storeAction = .store
|
descriptor.colorAttachments[0].storeAction = .store
|
||||||
|
|
||||||
|
let graphicContext = canvas.graphicContext
|
||||||
if renderer.redrawsGraphicRender {
|
if renderer.redrawsGraphicRender {
|
||||||
canvas.setGraphicRenderType(.finished)
|
canvas.setGraphicRenderType(.finished)
|
||||||
for stroke in graphicContext.strokes {
|
for stroke in graphicContext.strokes {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class StrokeRenderPass: RenderPass {
|
|||||||
weak var graphicDescriptor: MTLRenderPassDescriptor?
|
weak var graphicDescriptor: MTLRenderPassDescriptor?
|
||||||
|
|
||||||
var strokePipelineState: MTLRenderPipelineState?
|
var strokePipelineState: MTLRenderPipelineState?
|
||||||
|
var quadPipelineState: MTLComputePipelineState?
|
||||||
weak var graphicPipelineState: MTLRenderPipelineState?
|
weak var graphicPipelineState: MTLRenderPipelineState?
|
||||||
|
|
||||||
var stroke: Stroke?
|
var stroke: Stroke?
|
||||||
@@ -23,6 +24,7 @@ class StrokeRenderPass: RenderPass {
|
|||||||
init(renderer: Renderer) {
|
init(renderer: Renderer) {
|
||||||
descriptor = MTLRenderPassDescriptor()
|
descriptor = MTLRenderPassDescriptor()
|
||||||
strokePipelineState = PipelineStates.createStrokePipelineState(from: renderer)
|
strokePipelineState = PipelineStates.createStrokePipelineState(from: renderer)
|
||||||
|
quadPipelineState = PipelineStates.createQuadPipelineState(from: renderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
|
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) {
|
||||||
@@ -33,6 +35,8 @@ class StrokeRenderPass: RenderPass {
|
|||||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
guard let descriptor else { return }
|
guard let descriptor else { return }
|
||||||
|
|
||||||
|
generateVertexBuffer(on: canvas, with: renderer)
|
||||||
|
|
||||||
guard let strokeTexture else { return }
|
guard let strokeTexture else { return }
|
||||||
descriptor.colorAttachments[0].texture = strokeTexture
|
descriptor.colorAttachments[0].texture = strokeTexture
|
||||||
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||||
@@ -50,14 +54,38 @@ class StrokeRenderPass: RenderPass {
|
|||||||
|
|
||||||
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
stroke?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
|
||||||
renderEncoder.endEncoding()
|
renderEncoder.endEncoding()
|
||||||
commandBuffer.commit()
|
commandBuffer.commit()
|
||||||
|
|
||||||
drawStrokeTexture(on: canvas, with: renderer)
|
drawStrokeTexture(on: canvas, with: renderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) {
|
private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let stroke, !stroke.quads.isEmpty, let quadPipelineState else { return }
|
||||||
|
guard let quadCommandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||||
|
guard let computeEncoder = quadCommandBuffer.makeComputeCommandEncoder() else { return }
|
||||||
|
|
||||||
|
computeEncoder.label = "Quad Render Pass"
|
||||||
|
|
||||||
|
let quadCount = stroke.quads.endIndex
|
||||||
|
var quads = stroke.quads
|
||||||
|
let quadBuffer = renderer.device.makeBuffer(bytes: &quads, length: MemoryLayout<Quad>.stride * quadCount, options: [])
|
||||||
|
let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout<QuadVertex>.stride * quadCount * 6, options: [])
|
||||||
|
|
||||||
|
computeEncoder.setComputePipelineState(quadPipelineState)
|
||||||
|
computeEncoder.setBuffer(quadBuffer, offset: 0, index: 0)
|
||||||
|
computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 1)
|
||||||
|
|
||||||
|
stroke.vertexBuffer = vertexBuffer
|
||||||
|
|
||||||
|
let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1)
|
||||||
|
let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1)
|
||||||
|
computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
|
||||||
|
computeEncoder.endEncoding()
|
||||||
|
quadCommandBuffer.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) {
|
||||||
guard let stroke else { return }
|
guard let stroke else { return }
|
||||||
guard let graphicDescriptor, let graphicPipelineState else { return }
|
guard let graphicDescriptor, let graphicPipelineState else { return }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// Quad.metal
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct Quad {
|
||||||
|
float originX;
|
||||||
|
float originY;
|
||||||
|
float size;
|
||||||
|
float rotation;
|
||||||
|
int shape;
|
||||||
|
float4 color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float4 position;
|
||||||
|
float2 textCoord;
|
||||||
|
float4 color;
|
||||||
|
float2 origin;
|
||||||
|
float rotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
Vertex createVertex(Quad quad, float2 factor, float2 textCoord) {
|
||||||
|
Vertex output;
|
||||||
|
float x = quad.originX + factor.x;
|
||||||
|
float y = quad.originY + factor.y;
|
||||||
|
output.position = float4(x, y, 0, 1);
|
||||||
|
output.textCoord = textCoord;
|
||||||
|
output.color = quad.color;
|
||||||
|
output.origin = float2(quad.originX, quad.originY);
|
||||||
|
output.rotation = quad.rotation;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
kernel void generate_stroke_vertices(
|
||||||
|
device Quad *quads [[buffer(0)]],
|
||||||
|
device Vertex *vertices [[buffer(1)]],
|
||||||
|
uint gid [[thread_position_in_grid]]
|
||||||
|
) {
|
||||||
|
uint index = gid * 6;
|
||||||
|
Quad quad = quads[gid];
|
||||||
|
float halfSize = quad.size * 0.5;
|
||||||
|
vertices[index] = createVertex(quad, float2(-halfSize, -halfSize), float2(0, 0));
|
||||||
|
vertices[index + 1] = createVertex(quad, float2(halfSize, -halfSize), float2(1, 0));
|
||||||
|
vertices[index + 2] = createVertex(quad, float2(-halfSize, halfSize), float2(0, 1));
|
||||||
|
vertices[index + 3] = createVertex(quad, float2(halfSize, -halfSize), float2(1, 0));
|
||||||
|
vertices[index + 4] = createVertex(quad, float2(-halfSize, halfSize), float2(0, 1));
|
||||||
|
vertices[index + 5] = createVertex(quad, float2(halfSize, halfSize), float2(1, 1));
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import SwiftUI
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Pen: NSObject, ObservableObject, Identifiable {
|
class Pen: NSObject, ObservableObject, Identifiable {
|
||||||
@Published var style: PenStyle
|
@Published var style: any PenStyle
|
||||||
@Published var color: [CGFloat]
|
@Published var color: [CGFloat]
|
||||||
@Published var thickness: CGFloat
|
@Published var thickness: CGFloat
|
||||||
|
|
||||||
@@ -18,6 +18,17 @@ class Pen: NSObject, ObservableObject, Identifiable {
|
|||||||
self.color = color
|
self.color = color
|
||||||
self.thickness = thickness
|
self.thickness = thickness
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var strokeStyle: Stroke.Style {
|
||||||
|
switch style {
|
||||||
|
case is MarkerPenStyle:
|
||||||
|
return .marker
|
||||||
|
case is EraserPenStyle:
|
||||||
|
return .eraser
|
||||||
|
default:
|
||||||
|
return .marker
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Pen {
|
extension Pen {
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ class CanvasViewController: UIViewController {
|
|||||||
renderView.draw()
|
renderView.draw()
|
||||||
drawingView.enableUserInteraction()
|
drawingView.enableUserInteraction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
history.resetRedo()
|
||||||
|
withPersistence(\.backgroundContext) { context in
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CanvasViewController {
|
extension CanvasViewController {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DrawingView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateDrawableSize(with size: CGSize) {
|
func updateDrawableSize(with size: CGSize) {
|
||||||
renderView.drawableSize = size.multiply(by: 2.5)
|
renderView.drawableSize = size.multiply(by: 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// Float++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Float {
|
||||||
|
var cgFloat: CGFloat {
|
||||||
|
CGFloat(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// NSManagedObject++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension NSManagedObject {
|
||||||
|
convenience init(_ keyPath: KeyPath<Persistence, NSManagedObjectContext>) {
|
||||||
|
self.init(context: Persistence.shared[keyPath: keyPath])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// NSManagedObjectContext++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
extension NSManagedObjectContext {
|
||||||
|
func saveIfNeeded() throws {
|
||||||
|
if hasChanges {
|
||||||
|
try save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// View++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func persistence(_ keyPath: KeyPath<Persistence, NSManagedObjectContext>) -> some View {
|
||||||
|
environment(\.managedObjectContext, Persistence.shared[keyPath: keyPath])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,15 +6,22 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
|
||||||
struct MemoView: View {
|
struct MemoView: View {
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@Environment(\.managedObjectContext) var managedObjectContext
|
@Environment(\.managedObjectContext) var managedObjectContext
|
||||||
|
|
||||||
@StateObject var tool = Tool()
|
@StateObject var tool = Tool()
|
||||||
|
@StateObject var canvas: Canvas
|
||||||
@StateObject var history = History()
|
@StateObject var history = History()
|
||||||
|
|
||||||
@EnvironmentObject var canvas: Canvas
|
let memo: MemoObject
|
||||||
|
|
||||||
|
init(memo: MemoObject) {
|
||||||
|
self.memo = memo
|
||||||
|
self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID))
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CanvasView()
|
CanvasView()
|
||||||
@@ -53,9 +60,6 @@ struct MemoView: View {
|
|||||||
.environmentObject(tool)
|
.environmentObject(tool)
|
||||||
.environmentObject(canvas)
|
.environmentObject(canvas)
|
||||||
.environmentObject(history)
|
.environmentObject(history)
|
||||||
.task {
|
|
||||||
canvas.listen(on: managedObjectContext)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var historyTool: some View {
|
var historyTool: some View {
|
||||||
@@ -91,15 +95,14 @@ struct MemoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeMemo() {
|
func closeMemo() {
|
||||||
Task(priority: .high) {
|
history.resetRedo()
|
||||||
await MainActor.run {
|
if managedObjectContext.hasChanges {
|
||||||
canvas.state = .closing
|
do {
|
||||||
}
|
try managedObjectContext.save()
|
||||||
await canvas.save(on: managedObjectContext)
|
} catch {
|
||||||
await MainActor.run {
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
canvas.state = .closed
|
|
||||||
dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import SwiftUI
|
|||||||
struct MemosView: View {
|
struct MemosView: View {
|
||||||
@Environment(\.managedObjectContext) var managedObjectContext
|
@Environment(\.managedObjectContext) var managedObjectContext
|
||||||
|
|
||||||
@FetchRequest(sortDescriptors: []) var memos: FetchedResults<Memo>
|
@FetchRequest(sortDescriptors: []) var memoObjects: FetchedResults<MemoObject>
|
||||||
|
|
||||||
@State var canvas: Canvas?
|
@State var memo: MemoObject?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@@ -29,16 +29,20 @@ struct MemosView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(item: $canvas) { canvas in
|
.fullScreenCover(item: $memo) { memo in
|
||||||
MemoView()
|
MemoView(memo: memo)
|
||||||
.environmentObject(canvas)
|
.onDisappear {
|
||||||
|
withPersistence(\.viewContext) { context in
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoGrid: some View {
|
var memoGrid: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) {
|
LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) {
|
||||||
ForEach(memos) { memo in
|
ForEach(memoObjects) { memo in
|
||||||
memoCard(memo)
|
memoCard(memo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,42 +50,44 @@ struct MemosView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func memoCard(_ memo: Memo) -> some View {
|
func memoCard(_ memoObject: MemoObject) -> some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.frame(height: 150)
|
.frame(height: 150)
|
||||||
Text(memo.title)
|
Text(memoObject.title)
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
openMemo(for: memo)
|
openMemo(for: memoObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMemo(title: String) {
|
func createMemo(title: String) {
|
||||||
do {
|
do {
|
||||||
let data = try JSONEncoder().encode(Canvas())
|
let memoObject = MemoObject(context: managedObjectContext)
|
||||||
let memo = Memo(context: managedObjectContext)
|
memoObject.title = title
|
||||||
memo.id = UUID()
|
memoObject.createdAt = .now
|
||||||
memo.title = title
|
memoObject.updatedAt = .now
|
||||||
memo.data = data
|
|
||||||
memo.createdAt = .now
|
let canvasObject = CanvasObject(context: managedObjectContext)
|
||||||
memo.updatedAt = .now
|
canvasObject.width = 4_000
|
||||||
|
canvasObject.height = 4_000
|
||||||
|
|
||||||
|
let graphicContextObject = GraphicContextObject(context: managedObjectContext)
|
||||||
|
graphicContextObject.strokes = []
|
||||||
|
|
||||||
|
memoObject.canvas = canvasObject
|
||||||
|
canvasObject.memo = memoObject
|
||||||
|
canvasObject.graphicContext = graphicContextObject
|
||||||
|
graphicContextObject.canvas = canvasObject
|
||||||
|
|
||||||
try managedObjectContext.save()
|
try managedObjectContext.save()
|
||||||
openMemo(for: memo)
|
openMemo(for: memoObject)
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("[SketchNote] - \(error.localizedDescription)")
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func openMemo(for memo: Memo) {
|
func openMemo(for memo: MemoObject) {
|
||||||
do {
|
self.memo = memo
|
||||||
let data = memo.data
|
|
||||||
let canvas = try JSONDecoder().decode(Canvas.self, from: data)
|
|
||||||
canvas.memo = memo
|
|
||||||
self.canvas = canvas
|
|
||||||
} catch {
|
|
||||||
NSLog("[SketchNote] - \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,23 @@
|
|||||||
import CoreData
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Persistence {
|
final class Persistence {
|
||||||
private let modelName = "MemolaModel"
|
private let modelName = "MemolaModel"
|
||||||
|
|
||||||
static let shared: Persistence = Persistence()
|
static let shared: Persistence = Persistence()
|
||||||
|
|
||||||
private init() { }
|
private init() { }
|
||||||
|
|
||||||
var viewContext: NSManagedObjectContext {
|
lazy var viewContext: NSManagedObjectContext = {
|
||||||
persistentContainer.viewContext
|
persistentContainer.viewContext
|
||||||
}
|
}()
|
||||||
|
|
||||||
|
lazy var backgroundContext: NSManagedObjectContext = {
|
||||||
|
let context = persistentContainer.newBackgroundContext()
|
||||||
|
context.undoManager = nil
|
||||||
|
context.automaticallyMergesChangesFromParent = true
|
||||||
|
return context
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var persistentContainer: NSPersistentContainer = {
|
lazy var persistentContainer: NSPersistentContainer = {
|
||||||
let persistentStore = NSPersistentStoreDescription()
|
let persistentStore = NSPersistentStoreDescription()
|
||||||
@@ -64,3 +71,15 @@ class Persistence {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Global Method
|
||||||
|
func withPersistence(_ keypath: KeyPath<Persistence, NSManagedObjectContext>, _ task: @escaping (NSManagedObjectContext) throws -> Void) {
|
||||||
|
let context = Persistence.shared[keyPath: keypath]
|
||||||
|
context.perform {
|
||||||
|
do {
|
||||||
|
try task(context)
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// CanvasObject.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
@objc(CanvasObject)
|
||||||
|
final class CanvasObject: NSManagedObject {
|
||||||
|
@NSManaged var width: CGFloat
|
||||||
|
@NSManaged var height: CGFloat
|
||||||
|
@NSManaged var memo: MemoObject?
|
||||||
|
@NSManaged var graphicContext: GraphicContextObject
|
||||||
|
|
||||||
|
var size: CGSize {
|
||||||
|
CGSize(width: width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// GraphicContextObject.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(GraphicContextObject)
|
||||||
|
final class GraphicContextObject: NSManagedObject {
|
||||||
|
@NSManaged var canvas: CanvasObject?
|
||||||
|
@NSManaged var strokes: NSMutableOrderedSet
|
||||||
|
}
|
||||||
+5
-7
@@ -1,20 +1,18 @@
|
|||||||
//
|
//
|
||||||
// Memo.swift
|
// MemoObject.swift
|
||||||
// Memola
|
// Memola
|
||||||
//
|
//
|
||||||
// Created by Dscyre Scotti on 5/4/24.
|
// Created by Dscyre Scotti on 5/11/24.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@objc(Memo)
|
@objc(MemoObject)
|
||||||
class Memo: NSManagedObject {
|
final class MemoObject: NSManagedObject, Identifiable {
|
||||||
@NSManaged var id: UUID
|
|
||||||
@NSManaged var title: String
|
@NSManaged var title: String
|
||||||
@NSManaged var data: Data
|
@NSManaged var data: Data
|
||||||
@NSManaged var createdAt: Date
|
@NSManaged var createdAt: Date
|
||||||
@NSManaged var updatedAt: Date
|
@NSManaged var updatedAt: Date
|
||||||
|
@NSManaged var canvas: CanvasObject
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Memo: Identifiable { }
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// QuadObject.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(QuadObject)
|
||||||
|
final class QuadObject: NSManagedObject {
|
||||||
|
@NSManaged var originX: CGFloat
|
||||||
|
@NSManaged var originY: CGFloat
|
||||||
|
@NSManaged var size: CGFloat
|
||||||
|
@NSManaged var rotation: CGFloat
|
||||||
|
@NSManaged var shape: Int16
|
||||||
|
@NSManaged var color: [CGFloat]
|
||||||
|
@NSManaged var stroke: StrokeObject?
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// StrokeObject.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/11/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(StrokeObject)
|
||||||
|
final class StrokeObject: NSManagedObject {
|
||||||
|
@NSManaged var color: [CGFloat]
|
||||||
|
@NSManaged var style: Int16
|
||||||
|
@NSManaged var createdAt: Date
|
||||||
|
@NSManaged var thickness: CGFloat
|
||||||
|
@NSManaged var quads: NSMutableOrderedSet
|
||||||
|
@NSManaged var graphicContext: GraphicContextObject?
|
||||||
|
}
|
||||||
@@ -1,10 +1,36 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23B74" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23B74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Memo" representedClassName="Memo" syncable="YES">
|
<entity name="CanvasObject" representedClassName="CanvasObject" syncable="YES">
|
||||||
|
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/>
|
||||||
|
<attribute name="width" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/>
|
||||||
|
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContextObject" inverseName="canvas" inverseEntity="GraphicContextObject"/>
|
||||||
|
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="MemoObject" inverseName="canvas" inverseEntity="MemoObject"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
|
||||||
|
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CanvasObject" inverseName="graphicContext" inverseEntity="CanvasObject"/>
|
||||||
|
<relationship name="strokes" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StrokeObject" inverseName="graphicContext" inverseEntity="StrokeObject"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="data" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
|
|
||||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
|
||||||
<attribute name="title" attributeType="String"/>
|
<attribute name="title" attributeType="String"/>
|
||||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
|
||||||
|
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
|
<attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="originY" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="rotation" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="shape" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="size" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="quads" inverseEntity="StrokeObject"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="StrokeObject" representedClassName="StrokeObject" syncable="YES">
|
||||||
|
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="strokes" inverseEntity="GraphicContextObject"/>
|
||||||
|
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="stroke" inverseEntity="QuadObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
</model>
|
</model>
|
||||||
Reference in New Issue
Block a user