Merge pull request #27 from dscyrescotti/feature/pen-tool
Redesign pen tool of memo canvas view
@@ -7,12 +7,18 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14202BF79C73009BFE5F /* ToolObject.swift */; };
|
||||
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; };
|
||||
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; };
|
||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; };
|
||||
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 */; };
|
||||
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; };
|
||||
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; };
|
||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
|
||||
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
|
||||
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
||||
@@ -68,23 +74,30 @@
|
||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
|
||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
|
||||
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; };
|
||||
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; };
|
||||
ECA739082BE623F300A4542E /* PenDockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDockView.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 */; };
|
||||
ECFC51272BF8885700D0D051 /* ColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFC51262BF8885700D0D051 /* ColorPicker.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
EC0D14202BF79C73009BFE5F /* ToolObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolObject.swift; sourceTree = "<group>"; };
|
||||
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
|
||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = "<group>"; };
|
||||
EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = "<group>"; };
|
||||
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = "<group>"; };
|
||||
EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -141,14 +154,14 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; };
|
||||
ECA739072BE623F300A4542E /* PenDockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDockView.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>"; };
|
||||
ECFC51262BF8885700D0D051 /* ColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -178,6 +191,73 @@
|
||||
path = ViewController;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC1B783A2BF9C68C005A34E2 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECFC51252BF8885000D0D051 /* ColorPicker */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC5050042BF65CBC00B4D86E /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738BB2BE60E0300A4542E /* Tool.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC5050052BF65CCD00B4D86E /* PenDock */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA739072BE623F300A4542E /* PenDockView.swift */,
|
||||
EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */,
|
||||
);
|
||||
path = PenDock;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC5050082BF65D0500B4D86E /* Memo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */,
|
||||
);
|
||||
path = Memo;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC5050092BF65D5700B4D86E /* Canvas */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738B22BE60D9E00A4542E /* CanvasView.swift */,
|
||||
);
|
||||
path = Canvas;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC50500A2BF6672000B4D86E /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC1B783A2BF9C68C005A34E2 /* Views */,
|
||||
EC50500B2BF6673300B4D86E /* ViewModifiers */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC50500B2BF6673300B4D86E /* ViewModifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */,
|
||||
EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */,
|
||||
);
|
||||
path = ViewModifiers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC5050102BF670EE00B4D86E /* Config */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC50500E2BF670EA00B4D86E /* Info.plist */,
|
||||
);
|
||||
path = Config;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC7F6BDF2BE5E6E300A34A7B = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -199,6 +279,8 @@
|
||||
children = (
|
||||
ECA738762BE5EE4E00A4542E /* App */,
|
||||
ECA7387E2BE5FE4200A4542E /* Canvas */,
|
||||
EC5050102BF670EE00B4D86E /* Config */,
|
||||
EC50500A2BF6672000B4D86E /* Components */,
|
||||
ECA738A12BE601F700A4542E /* Extensions */,
|
||||
ECA738772BE5EEE800A4542E /* Features */,
|
||||
ECA738FA2BE61B1700A4542E /* Persistence */,
|
||||
@@ -244,8 +326,8 @@
|
||||
ECA7387B2BE5EF3500A4542E /* Memo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */,
|
||||
ECA739072BE623F300A4542E /* PenToolView.swift */,
|
||||
EC5050082BF65D0500B4D86E /* Memo */,
|
||||
EC5050052BF65CCD00B4D86E /* PenDock */,
|
||||
);
|
||||
path = Memo;
|
||||
sourceTree = "<group>";
|
||||
@@ -371,8 +453,8 @@
|
||||
ECA738AB2BE60CB500A4542E /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC5050092BF65D5700B4D86E /* Canvas */,
|
||||
ECA738AE2BE60CEC00A4542E /* Bridge */,
|
||||
ECA738B22BE60D9E00A4542E /* CanvasView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
@@ -389,8 +471,8 @@
|
||||
ECA738B12BE60D8800A4542E /* Tool */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC5050042BF65CBC00B4D86E /* Core */,
|
||||
ECA738BD2BE60E2800A4542E /* Pen */,
|
||||
ECA738BB2BE60E0300A4542E /* Tool.swift */,
|
||||
);
|
||||
path = Tool;
|
||||
sourceTree = "<group>";
|
||||
@@ -492,7 +574,7 @@
|
||||
ECA738FE2BE61D5700A4542E /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */,
|
||||
EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -522,10 +604,20 @@
|
||||
ECFA15232BEF223300455818 /* GraphicContextObject.swift */,
|
||||
ECFA15252BEF224900455818 /* StrokeObject.swift */,
|
||||
ECFA15272BEF225000455818 /* QuadObject.swift */,
|
||||
EC0D14202BF79C73009BFE5F /* ToolObject.swift */,
|
||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
|
||||
);
|
||||
path = Objects;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECFC51252BF8885000D0D051 /* ColorPicker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECFC51262BF8885700D0D051 /* ColorPicker.swift */,
|
||||
);
|
||||
path = ColorPicker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -627,13 +719,15 @@
|
||||
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
||||
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
|
||||
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
|
||||
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */,
|
||||
ECA739082BE623F300A4542E /* PenDockView.swift in Sources */,
|
||||
ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */,
|
||||
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */,
|
||||
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */,
|
||||
ECFC51272BF8885700D0D051 /* ColorPicker.swift in Sources */,
|
||||
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
|
||||
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */,
|
||||
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
||||
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */,
|
||||
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */,
|
||||
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
||||
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
|
||||
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */,
|
||||
@@ -646,6 +740,8 @@
|
||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
||||
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
||||
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
|
||||
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
|
||||
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
||||
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
||||
@@ -655,11 +751,13 @@
|
||||
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
|
||||
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
|
||||
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */,
|
||||
EC5050072BF65CED00B4D86E /* PenDropDelegate.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 */,
|
||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||
@@ -798,7 +896,8 @@
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Memola/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 9TYSSFKV5U;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Memola/Config/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -830,7 +929,8 @@
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Memola/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 9TYSSFKV5U;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Memola/Config/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -876,13 +976,14 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */ = {
|
||||
EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */,
|
||||
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */;
|
||||
path = MemolaModel.xcdatamodeld;
|
||||
currentVersion = EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */;
|
||||
name = MemolaModel.xcdatamodeld;
|
||||
path = /Users/dscyrescotti/Documents/Projects/Memola/Memola/Resources/Models/MemolaModel.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ extension GraphicContext {
|
||||
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
|
||||
let stroke = Stroke(
|
||||
bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
|
||||
color: pen.color,
|
||||
color: pen.rgba,
|
||||
style: pen.strokeStyle.rawValue,
|
||||
createdAt: .now,
|
||||
thickness: pen.thickness
|
||||
|
||||
@@ -14,12 +14,14 @@ class EraserRenderPass: RenderPass {
|
||||
var descriptor: MTLRenderPassDescriptor?
|
||||
|
||||
var eraserPipelineState: MTLRenderPipelineState?
|
||||
var quadPipelineState: MTLComputePipelineState?
|
||||
|
||||
var stroke: Stroke?
|
||||
weak var graphicTexture: MTLTexture?
|
||||
|
||||
init(renderer: Renderer) {
|
||||
eraserPipelineState = PipelineStates.createEraserPipelineState(from: renderer)
|
||||
quadPipelineState = PipelineStates.createQuadPipelineState(from: renderer)
|
||||
}
|
||||
|
||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
||||
@@ -27,6 +29,8 @@ class EraserRenderPass: RenderPass {
|
||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||
guard let descriptor else { return }
|
||||
|
||||
generateVertexBuffer(on: canvas, with: renderer)
|
||||
|
||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||
commandBuffer.label = "Eraser Command Buffer"
|
||||
|
||||
@@ -42,4 +46,30 @@ class EraserRenderPass: RenderPass {
|
||||
renderEncoder.endEncoding()
|
||||
commandBuffer.commit()
|
||||
}
|
||||
|
||||
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()
|
||||
quadCommandBuffer.waitUntilCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
109
Memola/Canvas/Tool/Core/Tool.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// Tool.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public class Tool: NSObject, ObservableObject {
|
||||
let object: ToolObject
|
||||
|
||||
@Published var pens: [Pen] = []
|
||||
@Published var selectedPen: Pen?
|
||||
@Published var draggedPen: Pen?
|
||||
|
||||
let scrollPublisher = PassthroughSubject<String, Never>()
|
||||
var markers: [Pen] {
|
||||
pens.filter { $0.strokeStyle == .marker }
|
||||
}
|
||||
|
||||
init(object: ToolObject) {
|
||||
self.object = object
|
||||
}
|
||||
|
||||
func load() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
pens = object.pens.sortedArray(using: [NSSortDescriptor(key: "orderIndex", ascending: true)]).compactMap {
|
||||
guard let pen = $0 as? PenObject else { return nil }
|
||||
return Pen(object: pen)
|
||||
}
|
||||
if let selectedPen = pens.first(where: { $0.isSelected }) {
|
||||
selectPen(selectedPen)
|
||||
scrollPublisher.send(selectedPen.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func selectPen(_ pen: Pen) {
|
||||
if let selectedPen {
|
||||
unselectPen(selectedPen)
|
||||
}
|
||||
withAnimation {
|
||||
selectedPen = pen
|
||||
}
|
||||
selectedPen?.isSelected = true
|
||||
withPersistence(\.viewContext) { context in
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func unselectPen(_ pen: Pen) {
|
||||
pen.isSelected = false
|
||||
withAnimation {
|
||||
selectedPen = nil
|
||||
}
|
||||
withPersistence(\.viewContext) { context in
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func duplicatePen(_ pen: Pen, of originalPen: Pen) {
|
||||
guard let index = pens.firstIndex(where: { originalPen === $0 }) else { return }
|
||||
withAnimation {
|
||||
pens.insert(pen, at: index + 1)
|
||||
}
|
||||
selectPen(pen)
|
||||
withPersistence(\.viewContext) { [pens] context in
|
||||
for (index, pen) in pens.enumerated() {
|
||||
pen.object?.orderIndex = Int16(index)
|
||||
}
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func addPen(_ pen: Pen) {
|
||||
withAnimation {
|
||||
pens.append(pen)
|
||||
}
|
||||
selectPen(pen)
|
||||
if let _pen = pen.object {
|
||||
object.pens.add(_pen)
|
||||
}
|
||||
scrollPublisher.send(pen.id)
|
||||
withPersistence(\.viewContext) { context in
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func removePen(_ pen: Pen) {
|
||||
guard let index = pens.firstIndex(where: { $0 === pen }) else { return }
|
||||
let deletedPen = withAnimation {
|
||||
pens.remove(at: index)
|
||||
}
|
||||
unselectPen(deletedPen)
|
||||
if let _pen = deletedPen.object {
|
||||
_pen.tool = nil
|
||||
object.pens.remove(_pen)
|
||||
withPersistence(\.viewContext) { context in
|
||||
context.delete(_pen)
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,32 +7,50 @@
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class Pen: NSObject, ObservableObject, Identifiable {
|
||||
@Published var style: any PenStyle
|
||||
@Published var color: [CGFloat]
|
||||
@Published var thickness: CGFloat
|
||||
var object: PenObject?
|
||||
|
||||
init(style: any PenStyle, color: [CGFloat], thickness: CGFloat) {
|
||||
self.style = style
|
||||
self.color = color
|
||||
self.thickness = thickness
|
||||
let id: String
|
||||
@Published var style: any PenStyle {
|
||||
didSet {
|
||||
object?.style = strokeStyle.rawValue
|
||||
}
|
||||
}
|
||||
@Published var rgba: [CGFloat] {
|
||||
didSet {
|
||||
object?.color = rgba
|
||||
}
|
||||
}
|
||||
@Published var thickness: CGFloat {
|
||||
didSet {
|
||||
object?.thickness = thickness
|
||||
}
|
||||
}
|
||||
@Published var isSelected: Bool {
|
||||
didSet {
|
||||
object?.isSelected = isSelected
|
||||
}
|
||||
}
|
||||
var color: Color {
|
||||
get { Color.rgba(from: rgba) }
|
||||
set {
|
||||
rgba = newValue.components
|
||||
}
|
||||
}
|
||||
|
||||
init(object: PenObject) {
|
||||
self.object = object
|
||||
self.id = object.objectID.uriRepresentation().absoluteString
|
||||
self.style = (Stroke.Style(rawValue: object.style) ?? .marker).anyPenStyle
|
||||
self.rgba = object.color
|
||||
self.thickness = object.thickness
|
||||
self.isSelected = object.isSelected
|
||||
super.init()
|
||||
}
|
||||
|
||||
var strokeStyle: Stroke.Style {
|
||||
switch style {
|
||||
case is MarkerPenStyle:
|
||||
return .marker
|
||||
case is EraserPenStyle:
|
||||
return .eraser
|
||||
default:
|
||||
return .marker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Pen {
|
||||
convenience init(for style: any PenStyle) {
|
||||
self.init(style: style, color: style.color, thickness: style.thinkness.min)
|
||||
style.strokeStyle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import Foundation
|
||||
protocol PenStyle {
|
||||
var icon: (base: String, tip: String?) { get }
|
||||
var textureName: String { get }
|
||||
var thinkness: (min: CGFloat, max: CGFloat) { get }
|
||||
var thickness: (min: CGFloat, max: CGFloat) { get }
|
||||
var thicknessSteps: [CGFloat] { get }
|
||||
var color: [CGFloat] { get }
|
||||
var stepRate: CGFloat { get }
|
||||
var generator: any StrokeGenerator { get }
|
||||
@@ -22,4 +23,15 @@ extension PenStyle {
|
||||
func loadTexture(on device: MTLDevice) -> MTLTexture? {
|
||||
Textures.createPenTexture(with: textureName, on: device)
|
||||
}
|
||||
|
||||
var strokeStyle: Stroke.Style {
|
||||
switch self {
|
||||
case is MarkerPenStyle:
|
||||
return .marker
|
||||
case is EraserPenStyle:
|
||||
return .eraser
|
||||
default:
|
||||
return .marker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ struct EraserPenStyle: PenStyle {
|
||||
|
||||
var textureName: String = "point-texture"
|
||||
|
||||
var thinkness: (min: CGFloat, max: CGFloat) = (0.5, 120)
|
||||
var thickness: (min: CGFloat, max: CGFloat) = (0.5, 40)
|
||||
|
||||
var thicknessSteps: [CGFloat] = [0.5, 1, 2, 5, 7.5, 10, 15, 20, 25, 30, 35, 40]
|
||||
|
||||
var color: [CGFloat] = [1, 1, 1, 0]
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ struct MarkerPenStyle: PenStyle {
|
||||
|
||||
var textureName: String = "point-texture"
|
||||
|
||||
var thinkness: (min: CGFloat, max: CGFloat) = (0.5, 120)
|
||||
var thickness: (min: CGFloat, max: CGFloat) = (0.5, 40)
|
||||
|
||||
var thicknessSteps: [CGFloat] = [0.5, 1, 2, 5, 7.5, 10, 15, 20, 25, 30, 35, 40]
|
||||
|
||||
var color: [CGFloat] = [1, 0.38, 0.38, 1]
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// 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()
|
||||
selectedPen = pens.first
|
||||
}
|
||||
|
||||
func changePen(_ pen: Pen) {
|
||||
selectedPen = pen
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,7 @@ extension CanvasViewController {
|
||||
|
||||
extension CanvasViewController {
|
||||
func loadMemo() {
|
||||
tool.load()
|
||||
canvas.load()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// ContextMenuViewModifier.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/17/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
struct ContextMenuViewModifier<MenuContent: View, Preview: View>: ViewModifier {
|
||||
let condition: Bool
|
||||
let menuItems: () -> MenuContent
|
||||
let preview: () -> Preview
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
if condition {
|
||||
content.contextMenu(menuItems: menuItems, preview: preview)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func contextMenu<MenuContent: View, Preview: View>(if condition: Bool, @ViewBuilder menuItems: @escaping () -> MenuContent, @ViewBuilder preview: @escaping () -> Preview) -> some View {
|
||||
modifier(ContextMenuViewModifier(condition: condition, menuItems: menuItems, preview: preview))
|
||||
}
|
||||
}
|
||||
30
Memola/Components/ViewModifiers/OnDragViewModifier.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// OnDragViewModifier.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
struct OnDragViewModifier<Preview: View>: ViewModifier {
|
||||
let condition: Bool
|
||||
let data: () -> NSItemProvider
|
||||
let preview: () -> Preview
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
if condition {
|
||||
content.onDrag(data, preview: preview)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func onDrag<Preview: View>(if condition: Bool, data: @escaping () -> NSItemProvider, @ViewBuilder preview: @escaping () -> Preview) -> some View {
|
||||
modifier(OnDragViewModifier(condition: condition, data: data, preview: preview))
|
||||
}
|
||||
}
|
||||
196
Memola/Components/Views/ColorPicker/ColorPicker.swift
Normal file
@@ -0,0 +1,196 @@
|
||||
//
|
||||
// ColorPicker.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/18/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
struct ColorPicker: View {
|
||||
@State var hue: Double = 1
|
||||
@State var saturation: Double = 0
|
||||
@State var brightness: Double = 1
|
||||
@State var alpha: Double = 1
|
||||
|
||||
@Binding var color: Color
|
||||
|
||||
let size: CGFloat = 20
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
colorPicker
|
||||
.frame(width: 200, height: 200)
|
||||
HStack(spacing: 10) {
|
||||
hueSlider
|
||||
alphaSlider
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background {
|
||||
Rectangle()
|
||||
.fill(.regularMaterial)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
.onAppear {
|
||||
let hsba = color.hsba
|
||||
hue = hsba.hue
|
||||
saturation = hsba.saturation
|
||||
brightness = hsba.brightness
|
||||
alpha = hsba.alpha * 1.43 - 0.43
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var colorPicker: some View {
|
||||
GeometryReader { proxy in
|
||||
ZStack {
|
||||
Color(hue: hue, saturation: 1, brightness: 1)
|
||||
LinearGradient(
|
||||
colors: [.white, .clear],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
LinearGradient(
|
||||
colors: [.black, .clear],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
}
|
||||
.cornerRadius(5)
|
||||
.drawingGroup()
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.stroke(Color.gray, lineWidth: 0.2)
|
||||
}
|
||||
.overlay(alignment: .bottomLeading) {
|
||||
Color(hue: hue, saturation: saturation, brightness: brightness)
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(Circle())
|
||||
.padding(1)
|
||||
.overlay {
|
||||
Circle()
|
||||
.strokeBorder(.white, lineWidth: 2)
|
||||
}
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(Color.gray, lineWidth: 0.2)
|
||||
}
|
||||
.offset(x: -size + 5, y: size - 5)
|
||||
.offset(x: max(proxy.size.width * saturation, size - 10), y: min(proxy.size.height * -brightness, -size + 10))
|
||||
}
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
saturation = min(1, max(value.location.x / proxy.size.width, 0))
|
||||
brightness = 1 - min(1, max(value.location.y / proxy.size.height, 0))
|
||||
updateColor()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var hueSlider: some View {
|
||||
GeometryReader { proxy in
|
||||
ZStack(alignment: .leading) {
|
||||
LinearGradient(
|
||||
colors: (0...10).map { Color(hue: Double($0) * 0.1, saturation: 1, brightness: 1) },
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
Color(hue: hue, saturation: 1, brightness: 1)
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(Circle())
|
||||
.padding(1)
|
||||
.overlay {
|
||||
Circle()
|
||||
.strokeBorder(.white, lineWidth: 2)
|
||||
}
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(Color.gray, lineWidth: 0.2)
|
||||
}
|
||||
.offset(x: -size)
|
||||
.offset(x: max(size, proxy.size.width * hue - 2))
|
||||
}
|
||||
.frame(width: proxy.size.width, height: proxy.size.height)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
hue = min(1, max(value.location.x / proxy.size.width, 0))
|
||||
updateColor()
|
||||
}
|
||||
.onEnded { value in
|
||||
hue = min(1, max(value.location.x / proxy.size.width, 0))
|
||||
updateColor()
|
||||
}
|
||||
)
|
||||
.clipShape(Capsule())
|
||||
.overlay {
|
||||
Capsule()
|
||||
.stroke(Color.gray, lineWidth: 0.2)
|
||||
}
|
||||
.frame(height: proxy.size.height)
|
||||
}
|
||||
.frame(height: size)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var alphaSlider: some View {
|
||||
GeometryReader { proxy in
|
||||
let color = Color(hue: hue, saturation: saturation, brightness: brightness)
|
||||
ZStack(alignment: .leading) {
|
||||
LinearGradient(
|
||||
colors: (3...10).map { color.opacity(0.1 * CGFloat($0)) },
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.background {
|
||||
Image("transparent-grid-rect")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.background(.white)
|
||||
}
|
||||
color
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(Circle())
|
||||
.padding(1)
|
||||
.overlay {
|
||||
Circle()
|
||||
.strokeBorder(.white, lineWidth: 2)
|
||||
}
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(Color.gray, lineWidth: 0.2)
|
||||
}
|
||||
.offset(x: -size)
|
||||
.offset(x: max(size, proxy.size.width * alpha - 2))
|
||||
}
|
||||
.frame(width: proxy.size.width, height: proxy.size.height)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
alpha = min(1, max(value.location.x / proxy.size.width, 0))
|
||||
updateColor()
|
||||
}
|
||||
.onEnded { value in
|
||||
alpha = min(1, max(value.location.x / proxy.size.width, 0))
|
||||
updateColor()
|
||||
}
|
||||
)
|
||||
.clipShape(Capsule())
|
||||
.overlay {
|
||||
Capsule()
|
||||
.stroke(Color.gray, lineWidth: 0.2)
|
||||
}
|
||||
.frame(height: proxy.size.height)
|
||||
}
|
||||
.frame(height: size)
|
||||
}
|
||||
|
||||
func updateColor() {
|
||||
color = Color(hue: hue, saturation: saturation, brightness: brightness).opacity(0.7 * alpha + 0.3)
|
||||
}
|
||||
}
|
||||
51
Memola/Config/Info.plist
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>UISupportedInterfaceOrientations~iphone</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -18,6 +18,18 @@ extension Color {
|
||||
}
|
||||
}
|
||||
|
||||
extension Color {
|
||||
var hsba: (hue: Double, saturation: Double, brightness: Double, alpha: Double) {
|
||||
let uiColor = UIColor(self)
|
||||
var hue: CGFloat = 0
|
||||
var saturation: CGFloat = 0
|
||||
var brightness: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
uiColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
|
||||
return (hue, saturation, brightness, alpha)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
var components: [CGFloat] {
|
||||
let uiColor: UIColor = self
|
||||
|
||||
@@ -10,9 +10,8 @@ import CoreData
|
||||
|
||||
struct MemoView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.managedObjectContext) var managedObjectContext
|
||||
|
||||
@StateObject var tool = Tool()
|
||||
@StateObject var tool: Tool
|
||||
@StateObject var canvas: Canvas
|
||||
@StateObject var history = History()
|
||||
|
||||
@@ -20,25 +19,28 @@ struct MemoView: View {
|
||||
|
||||
init(memo: MemoObject) {
|
||||
self.memo = memo
|
||||
self._tool = StateObject(wrappedValue: Tool(object: memo.tool))
|
||||
self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
CanvasView()
|
||||
.ignoresSafeArea()
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
PenToolView()
|
||||
.padding()
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
historyTool
|
||||
.padding()
|
||||
}
|
||||
.overlay(alignment: .trailing) {
|
||||
PenDockView()
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
Button {
|
||||
closeMemo()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.contentShape(.circle)
|
||||
.padding(15)
|
||||
.background(.regularMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
@@ -68,6 +70,7 @@ struct MemoView: View {
|
||||
history.historyPublisher.send(.undo)
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.backward.circle")
|
||||
.contentShape(.circle)
|
||||
}
|
||||
.hoverEffect(.lift)
|
||||
.disabled(history.undoDisabled)
|
||||
@@ -75,6 +78,7 @@ struct MemoView: View {
|
||||
history.historyPublisher.send(.redo)
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.forward.circle")
|
||||
.contentShape(.circle)
|
||||
}
|
||||
.hoverEffect(.lift)
|
||||
.disabled(history.redoDisabled)
|
||||
@@ -95,13 +99,8 @@ struct MemoView: View {
|
||||
}
|
||||
|
||||
func closeMemo() {
|
||||
history.resetRedo()
|
||||
if managedObjectContext.hasChanges {
|
||||
do {
|
||||
try managedObjectContext.save()
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
}
|
||||
withPersistenceSync(\.viewContext) { context in
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
297
Memola/Features/Memo/PenDock/PenDockView.swift
Normal file
@@ -0,0 +1,297 @@
|
||||
//
|
||||
// PenDockView.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PenDockView: View {
|
||||
@EnvironmentObject var tool: Tool
|
||||
|
||||
let width: CGFloat = 90
|
||||
let height: CGFloat = 30
|
||||
let factor: CGFloat = 0.95
|
||||
|
||||
@State var refreshScrollId: UUID = UUID()
|
||||
@State var opensColorPicker: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .trailing) {
|
||||
if let pen = tool.selectedPen {
|
||||
VStack(spacing: 5) {
|
||||
penColorView(pen)
|
||||
penThicknessView(pen)
|
||||
}
|
||||
.padding(10)
|
||||
.frame(width: width * factor - 18)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(.regularMaterial)
|
||||
}
|
||||
.transition(.move(edge: .trailing).combined(with: .opacity))
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(width: width * factor - 18, height: 50)
|
||||
}
|
||||
penScrollView
|
||||
}
|
||||
.fixedSize()
|
||||
}
|
||||
|
||||
var penScrollView: some View {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(tool.pens) { pen in
|
||||
penView(pen)
|
||||
.id(pen.id)
|
||||
.scrollTransition { content, phase in
|
||||
content
|
||||
.scaleEffect(phase.isIdentity ? 1 : 0.04, anchor: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.leading, 40)
|
||||
.id(refreshScrollId)
|
||||
}
|
||||
.onReceive(tool.scrollPublisher) { id in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
withAnimation {
|
||||
proxy.scrollTo(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight:( (height * factor + 10) * 6) + 20)
|
||||
.fixedSize()
|
||||
.background(alignment: .trailing) {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(.regularMaterial)
|
||||
.frame(width: width * factor - 18)
|
||||
}
|
||||
.clipShape(.rect(cornerRadii: .init(bottomTrailing: 20, topTrailing: 20)))
|
||||
.overlay(alignment: .bottomLeading) {
|
||||
newPenButton
|
||||
.offset(x: 60, y: 10)
|
||||
}
|
||||
}
|
||||
|
||||
func penView(_ pen: Pen) -> some View {
|
||||
ZStack {
|
||||
penShadowView(pen)
|
||||
if let tip = pen.style.icon.tip {
|
||||
Image(tip)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundStyle(Color.rgba(from: pen.rgba))
|
||||
}
|
||||
Image(pen.style.icon.base)
|
||||
.resizable()
|
||||
}
|
||||
.frame(width: width * factor, height: height * factor)
|
||||
.padding(.vertical, 5)
|
||||
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
|
||||
.onTapGesture {
|
||||
if tool.selectedPen === pen {
|
||||
tool.unselectPen(pen)
|
||||
} else {
|
||||
tool.selectPen(pen)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 10)
|
||||
.contextMenu(if: pen.strokeStyle != .eraser) {
|
||||
ControlGroup {
|
||||
Button {
|
||||
tool.selectPen(pen)
|
||||
} label: {
|
||||
Label(
|
||||
title: { Text("Select") },
|
||||
icon: { Image(systemName: "pencil.tip.crop.circle") }
|
||||
)
|
||||
}
|
||||
Button {
|
||||
let originalPen = pen
|
||||
let pen = PenObject.createObject(\.viewContext, penStyle: originalPen.style)
|
||||
pen.color = originalPen.rgba
|
||||
pen.thickness = originalPen.thickness
|
||||
pen.isSelected = true
|
||||
pen.tool = tool.object
|
||||
let _pen = Pen(object: pen)
|
||||
tool.duplicatePen(_pen, of: originalPen)
|
||||
} label: {
|
||||
Label(
|
||||
title: { Text("Duplicate") },
|
||||
icon: { Image(systemName: "plus.square.on.square") }
|
||||
)
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
tool.removePen(pen)
|
||||
} label: {
|
||||
Label(
|
||||
title: { Text("Remove") },
|
||||
icon: { Image(systemName: "trash") }
|
||||
)
|
||||
}
|
||||
.disabled(tool.markers.count <= 1)
|
||||
}
|
||||
.controlGroupStyle(.menu)
|
||||
} preview: {
|
||||
penPreviewView(pen)
|
||||
.drawingGroup()
|
||||
.contentShape(.contextMenuPreview, .rect(cornerRadius: 10))
|
||||
}
|
||||
.onDrag(if: pen.strokeStyle != .eraser) {
|
||||
tool.draggedPen = pen
|
||||
return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider()
|
||||
} preview: {
|
||||
penPreviewView(pen)
|
||||
.contentShape(.dragPreview, .rect(cornerRadius: 10))
|
||||
}
|
||||
.onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool, action: { refreshScrollId = UUID() }))
|
||||
.offset(x: tool.selectedPen === pen ? 0 : 25)
|
||||
}
|
||||
|
||||
func penColorView(_ pen: Pen) -> some View {
|
||||
Button {
|
||||
opensColorPicker = true
|
||||
} label: {
|
||||
let hsba = pen.color.hsba
|
||||
let baseColor = Color(hue: hsba.hue, saturation: hsba.saturation, brightness: hsba.brightness)
|
||||
GeometryReader { proxy in
|
||||
HStack(spacing: 0) {
|
||||
baseColor
|
||||
.frame(width: proxy.size.width / 2)
|
||||
Image("transparent-grid-square")
|
||||
.resizable()
|
||||
.scaleEffect(3)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.opacity(1 - hsba.alpha)
|
||||
.frame(width: proxy.size.width / 2)
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
.background(baseColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.frame(height: 28)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.gray, lineWidth: 0.4)
|
||||
}
|
||||
.padding(0.2)
|
||||
.padding(.top, 4)
|
||||
.drawingGroup()
|
||||
}
|
||||
.hoverEffect(.lift)
|
||||
.popover(isPresented: $opensColorPicker) {
|
||||
let color = Binding(
|
||||
get: { pen.color },
|
||||
set: {
|
||||
pen.color = $0
|
||||
tool.objectWillChange.send()
|
||||
}
|
||||
)
|
||||
ColorPicker(color: color)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func penThicknessView(_ pen: Pen) -> some View {
|
||||
let minimum: CGFloat = pen.style.thickness.min
|
||||
let maximum: CGFloat = pen.style.thickness.max
|
||||
let start: CGFloat = 5
|
||||
let end: CGFloat = 15
|
||||
let selection = Binding(
|
||||
get: { pen.thickness },
|
||||
set: {
|
||||
pen.thickness = $0
|
||||
tool.objectWillChange.send()
|
||||
}
|
||||
)
|
||||
Picker("", selection: selection) {
|
||||
ForEach(pen.style.thicknessSteps, id: \.self) { step in
|
||||
let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (1 / step)
|
||||
if pen.thickness == step {
|
||||
Circle()
|
||||
.fill(.black)
|
||||
.frame(width: size, height: size)
|
||||
} else {
|
||||
Circle()
|
||||
.stroke(Color.black, lineWidth: 1)
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
.frame(width: width * factor - 18, height: 40)
|
||||
}
|
||||
|
||||
var newPenButton: some View {
|
||||
Button {
|
||||
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
|
||||
if let color = (tool.selectedPen ?? tool.pens.last)?.rgba {
|
||||
pen.color = color
|
||||
}
|
||||
pen.isSelected = true
|
||||
pen.tool = tool.object
|
||||
pen.orderIndex = Int16(tool.pens.count)
|
||||
let _pen = Pen(object: pen)
|
||||
tool.addPen(_pen)
|
||||
} label: {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title2)
|
||||
.padding(1)
|
||||
.contentShape(.circle)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(.white)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.green)
|
||||
.hoverEffect(.lift)
|
||||
}
|
||||
|
||||
func penPreviewView(_ pen: Pen) -> some View {
|
||||
ZStack {
|
||||
if let tip = pen.style.icon.tip {
|
||||
Image(tip)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundStyle(Color.rgba(from: pen.rgba))
|
||||
}
|
||||
Image(pen.style.icon.base)
|
||||
.resizable()
|
||||
}
|
||||
.frame(width: width * factor, height: height * factor)
|
||||
.padding(.vertical, 5)
|
||||
.padding(.leading, 10)
|
||||
}
|
||||
|
||||
func penShadowView(_ pen: Pen) -> some View {
|
||||
ZStack {
|
||||
Group {
|
||||
if let tip = pen.style.icon.tip {
|
||||
Image(tip)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
}
|
||||
Image(pen.style.icon.base)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
}
|
||||
.drawingGroup()
|
||||
.foregroundStyle(.black.opacity(0.2))
|
||||
.blur(radius: 3)
|
||||
if let tip = pen.style.icon.tip {
|
||||
Image(tip)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundStyle(Color(red: pen.rgba[0], green: pen.rgba[1], blue: pen.rgba[2]))
|
||||
.blur(radius: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Memola/Features/Memo/PenDock/PenDropDelegate.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// PenDropDelegate.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
struct PenDropDelegate: DropDelegate {
|
||||
let id: String
|
||||
@ObservedObject var tool: Tool
|
||||
let action: () -> Void
|
||||
|
||||
func performDrop(info: DropInfo) -> Bool {
|
||||
tool.draggedPen = nil
|
||||
action()
|
||||
return true
|
||||
}
|
||||
|
||||
func dropEntered(info: DropInfo) {
|
||||
guard let draggedPen = tool.draggedPen else { return }
|
||||
if draggedPen.id != id {
|
||||
let fromIndex = tool.pens.firstIndex(where: { $0.id == draggedPen.id })!
|
||||
let toIndex = tool.pens.firstIndex(where: { $0.id == id })!
|
||||
guard tool.pens[toIndex].strokeStyle != .eraser else { return }
|
||||
withAnimation {
|
||||
tool.pens.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
|
||||
tool.objectWillChange.send()
|
||||
}
|
||||
withPersistence(\.viewContext) { context in
|
||||
for (index, pen) in tool.pens.enumerated() {
|
||||
pen.object?.orderIndex = Int16(index)
|
||||
}
|
||||
try context.saveIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//
|
||||
// PenToolView.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PenToolView: View {
|
||||
@EnvironmentObject var tool: Tool
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let pen = tool.selectedPen {
|
||||
let thicknessBounds = pen.style.thinkness
|
||||
let thickness = Binding {
|
||||
max(pen.thickness, pen.style.thinkness.min)
|
||||
} set: { newValue in
|
||||
tool.selectedPen?.thickness = newValue
|
||||
}
|
||||
let color = Binding {
|
||||
Color.rgba(from: pen.color)
|
||||
} set: { newValue in
|
||||
tool.selectedPen?.color = newValue.components
|
||||
tool.objectWillChange.send()
|
||||
}
|
||||
HStack {
|
||||
ColorPicker("", selection: color)
|
||||
.frame(width: 40, height: 40)
|
||||
Slider(value: thickness, in: thicknessBounds.min...thicknessBounds.max)
|
||||
.frame(width: 180, height: 40)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
ForEach(tool.pens) { pen in
|
||||
penView(pen)
|
||||
.overlay(alignment: .bottom) {
|
||||
if tool.selectedPen === pen {
|
||||
Circle()
|
||||
.frame(width: 5, height: 5)
|
||||
.offset(y: 7.5)
|
||||
.foregroundStyle(Color.rgba(from: pen.color))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(15)
|
||||
.background(.regularMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func penView(_ pen: Pen) -> some View {
|
||||
Button {
|
||||
if tool.selectedPen === pen {
|
||||
tool.selectedPen = nil
|
||||
} else {
|
||||
tool.changePen(pen)
|
||||
}
|
||||
} label: {
|
||||
ZStack {
|
||||
if let tip = pen.style.icon.tip {
|
||||
Image(tip)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundStyle(Color.rgba(from: pen.color))
|
||||
}
|
||||
Image(pen.style.icon.base)
|
||||
.resizable()
|
||||
}
|
||||
.frame(width: 30, height: 65)
|
||||
.drawingGroup()
|
||||
.hoverEffect(.lift)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -62,28 +62,45 @@ struct MemosView: View {
|
||||
}
|
||||
|
||||
func createMemo(title: String) {
|
||||
do {
|
||||
let memoObject = MemoObject(context: managedObjectContext)
|
||||
memoObject.title = title
|
||||
memoObject.createdAt = .now
|
||||
memoObject.updatedAt = .now
|
||||
let memoObject = MemoObject(\.viewContext)
|
||||
memoObject.title = title
|
||||
memoObject.createdAt = .now
|
||||
memoObject.updatedAt = .now
|
||||
|
||||
let canvasObject = CanvasObject(context: managedObjectContext)
|
||||
canvasObject.width = 8_000
|
||||
canvasObject.height = 8_000
|
||||
let canvasObject = CanvasObject(context: managedObjectContext)
|
||||
canvasObject.width = 8_000
|
||||
canvasObject.height = 8_000
|
||||
|
||||
let graphicContextObject = GraphicContextObject(context: managedObjectContext)
|
||||
graphicContextObject.strokes = []
|
||||
let toolObject = ToolObject(\.viewContext)
|
||||
toolObject.pens = []
|
||||
|
||||
memoObject.canvas = canvasObject
|
||||
canvasObject.memo = memoObject
|
||||
canvasObject.graphicContext = graphicContextObject
|
||||
graphicContextObject.canvas = canvasObject
|
||||
let eraserPenObject = PenObject.createObject(\.viewContext, penStyle: .eraser)
|
||||
eraserPenObject.orderIndex = 0
|
||||
let markerPenObject = PenObject.createObject(\.viewContext, penStyle: .marker)
|
||||
markerPenObject.orderIndex = 1
|
||||
|
||||
try managedObjectContext.save()
|
||||
openMemo(for: memoObject)
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
let graphicContextObject = GraphicContextObject(\.viewContext)
|
||||
graphicContextObject.strokes = []
|
||||
|
||||
memoObject.canvas = canvasObject
|
||||
memoObject.tool = toolObject
|
||||
|
||||
canvasObject.memo = memoObject
|
||||
canvasObject.graphicContext = graphicContextObject
|
||||
|
||||
toolObject.memo = memoObject
|
||||
toolObject.pens = [eraserPenObject, markerPenObject]
|
||||
|
||||
eraserPenObject.tool = toolObject
|
||||
markerPenObject.tool = toolObject
|
||||
|
||||
graphicContextObject.canvas = canvasObject
|
||||
|
||||
withPersistenceSync(\.viewContext) { context in
|
||||
try context.save()
|
||||
DispatchQueue.main.async {
|
||||
openMemo(for: memoObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import Foundation
|
||||
|
||||
@objc(MemoObject)
|
||||
final class MemoObject: NSManagedObject, Identifiable {
|
||||
@NSManaged var title: String
|
||||
@NSManaged var data: Data
|
||||
@NSManaged var title: String
|
||||
@NSManaged var createdAt: Date
|
||||
@NSManaged var updatedAt: Date
|
||||
@NSManaged var tool: ToolObject
|
||||
@NSManaged var canvas: CanvasObject
|
||||
}
|
||||
|
||||
30
Memola/Persistence/Objects/PenObject.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// PenObject.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/17/24.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(PenObject)
|
||||
class PenObject: NSManagedObject {
|
||||
@NSManaged var color: [CGFloat]
|
||||
@NSManaged var style: Int16
|
||||
@NSManaged var thickness: CGFloat
|
||||
@NSManaged var isSelected: Bool
|
||||
@NSManaged var orderIndex: Int16
|
||||
@NSManaged var tool: ToolObject?
|
||||
}
|
||||
|
||||
extension PenObject {
|
||||
static func createObject(_ keyPath: KeyPath<Persistence, NSManagedObjectContext>, penStyle: any PenStyle) -> PenObject {
|
||||
let object = PenObject(context: Persistence.shared[keyPath: keyPath])
|
||||
object.color = penStyle.color
|
||||
object.style = penStyle.strokeStyle.rawValue
|
||||
object.isSelected = false
|
||||
object.thickness = penStyle.thickness.min
|
||||
return object
|
||||
}
|
||||
}
|
||||
15
Memola/Persistence/Objects/ToolObject.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// ToolObject.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/17/24.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(ToolObject)
|
||||
class ToolObject: NSManagedObject {
|
||||
@NSManaged var pens: NSMutableSet
|
||||
@NSManaged var memo: MemoObject?
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
23
Memola/Resources/Assets/Assets.xcassets/backgrounds/transparent-grid-rect.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "transparency_grid 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "transparency_grid 1@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "transparency_grid 1@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "transparency_grid 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "transparency_grid 1@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "transparency_grid 1@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 18 KiB |
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bullet-base.png",
|
||||
"filename" : "marker-base.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "bullet-base@2x.png",
|
||||
"filename" : "marker-base@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "bullet-base@3x.png",
|
||||
"filename" : "marker-base@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 20 KiB |
BIN
Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base.imageset/marker-base.png
vendored
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base.imageset/marker-base@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base.imageset/marker-base@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 25 KiB |
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bullet-tip.png",
|
||||
"filename" : "marker-tip.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "bullet-tip@2x.png",
|
||||
"filename" : "marker-tip@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "bullet-tip@3x.png",
|
||||
"filename" : "marker-tip@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 427 B |
|
Before Width: | Height: | Size: 859 B |
|
Before Width: | Height: | Size: 1.3 KiB |
BIN
Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip.imageset/marker-tip.png
vendored
Normal file
|
After Width: | Height: | Size: 384 B |
BIN
Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip.imageset/marker-tip@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 787 B |
BIN
Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip.imageset/marker-tip@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
@@ -15,6 +15,15 @@
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
|
||||
<relationship name="tool" maxCount="1" deletionRule="Cascade" destinationEntity="ToolObject" inverseName="memo" inverseEntity="ToolObject"/>
|
||||
</entity>
|
||||
<entity name="PenObject" representedClassName="PenObject" syncable="YES">
|
||||
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||
<attribute name="isSelected" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="orderIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
|
||||
</entity>
|
||||
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
|
||||
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||
@@ -34,4 +43,8 @@
|
||||
<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 name="ToolObject" representedClassName="ToolObject" syncable="YES">
|
||||
<relationship name="memo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MemoObject" inverseName="tool" inverseEntity="MemoObject"/>
|
||||
<relationship name="pens" toMany="YES" deletionRule="Cascade" destinationEntity="PenObject" inverseName="tool" inverseEntity="PenObject"/>
|
||||
</entity>
|
||||
</model>
|
||||