mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-07-12 15:52:41 +02:00
Merge pull request #45 from dscyrescotti/feature/photo
Implement photo insertion
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; };
|
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; };
|
||||||
EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; };
|
EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; };
|
||||||
EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; };
|
EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; };
|
||||||
|
EC37FB122C1B2DD90008D976 /* ToolSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC37FB112C1B2DD90008D976 /* ToolSelection.swift */; };
|
||||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; };
|
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; };
|
||||||
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; };
|
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; };
|
||||||
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; };
|
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; };
|
||||||
@@ -82,6 +83,18 @@
|
|||||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
||||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
|
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
|
||||||
ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; };
|
ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; };
|
||||||
|
ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; };
|
||||||
|
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; };
|
||||||
|
ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */; };
|
||||||
|
ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC995A22C1E8F2800B2699A /* PhotoItem.swift */; };
|
||||||
|
ECC995A52C1EB4CC00B2699A /* Data++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC995A42C1EB4CC00B2699A /* Data++.swift */; };
|
||||||
|
ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; };
|
||||||
|
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; };
|
||||||
|
ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */; };
|
||||||
|
ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A8E2C1AEBA400B96E12 /* Photo.swift */; };
|
||||||
|
ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */; };
|
||||||
|
ECD12A932C1B062000B96E12 /* Photo.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A922C1B062000B96E12 /* Photo.metal */; };
|
||||||
|
ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */; };
|
||||||
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; };
|
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; };
|
||||||
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
|
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
|
||||||
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; };
|
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; };
|
||||||
@@ -109,6 +122,7 @@
|
|||||||
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.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>"; };
|
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>"; };
|
EC35655B2BF0712A00A4E0BF /* Float++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = "<group>"; };
|
||||||
|
EC37FB112C1B2DD90008D976 /* ToolSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolSelection.swift; sourceTree = "<group>"; };
|
||||||
EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.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>"; };
|
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>"; };
|
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = "<group>"; };
|
||||||
@@ -172,6 +186,18 @@
|
|||||||
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>"; };
|
||||||
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>"; };
|
||||||
ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = "<group>"; };
|
ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = "<group>"; };
|
||||||
|
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = "<group>"; };
|
||||||
|
ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
||||||
|
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoItem.swift; sourceTree = "<group>"; };
|
||||||
|
ECC995A42C1EB4CC00B2699A /* Data++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data++.swift"; sourceTree = "<group>"; };
|
||||||
|
ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = "<group>"; };
|
||||||
|
ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = "<group>"; };
|
||||||
|
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = "<group>"; };
|
||||||
|
ECD12A8E2C1AEBA400B96E12 /* Photo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = "<group>"; };
|
||||||
|
ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoRenderPass.swift; sourceTree = "<group>"; };
|
||||||
|
ECD12A922C1B062000B96E12 /* Photo.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Photo.metal; sourceTree = "<group>"; };
|
||||||
|
ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVertex.swift; sourceTree = "<group>"; };
|
||||||
ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = "<group>"; };
|
ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = "<group>"; };
|
||||||
ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
|
ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
|
||||||
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = "<group>"; };
|
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = "<group>"; };
|
||||||
@@ -214,6 +240,7 @@
|
|||||||
EC1B783A2BF9C68C005A34E2 /* Views */ = {
|
EC1B783A2BF9C68C005A34E2 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
ECBE529B2C1D94A4006BDB3D /* CameraView */,
|
||||||
ECFC51252BF8885000D0D051 /* ColorPicker */,
|
ECFC51252BF8885000D0D051 /* ColorPicker */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
@@ -241,6 +268,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ECA738BB2BE60E0300A4542E /* Tool.swift */,
|
ECA738BB2BE60E0300A4542E /* Tool.swift */,
|
||||||
|
EC37FB112C1B2DD90008D976 /* ToolSelection.swift */,
|
||||||
);
|
);
|
||||||
path = Core;
|
path = Core;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -372,6 +400,7 @@
|
|||||||
ECA7387B2BE5EF3500A4542E /* Memo */ = {
|
ECA7387B2BE5EF3500A4542E /* Memo */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
ECBE52942C1D58F5006BDB3D /* PhotoPreview */,
|
||||||
EC1B783B2BFA0AAC005A34E2 /* Toolbar */,
|
EC1B783B2BFA0AAC005A34E2 /* Toolbar */,
|
||||||
EC5050082BF65D0500B4D86E /* Memo */,
|
EC5050082BF65D0500B4D86E /* Memo */,
|
||||||
EC5050052BF65CCD00B4D86E /* PenDock */,
|
EC5050052BF65CCD00B4D86E /* PenDock */,
|
||||||
@@ -382,8 +411,8 @@
|
|||||||
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
|
ECA7387E2BE5FE4200A4542E /* Canvas */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
ECD12A872C19EF8700B96E12 /* Elements */,
|
||||||
EC2BEBF22C0F5FE1005DB0AF /* RTree */,
|
EC2BEBF22C0F5FE1005DB0AF /* RTree */,
|
||||||
ECA738F92BE6130000A4542E /* Geometries */,
|
|
||||||
ECA738812BE5FEEE00A4542E /* Abstracts */,
|
ECA738812BE5FEEE00A4542E /* Abstracts */,
|
||||||
ECA738992BE6018900A4542E /* Buffers */,
|
ECA738992BE6018900A4542E /* Buffers */,
|
||||||
ECA738C72BE60EE200A4542E /* Contexts */,
|
ECA738C72BE60EE200A4542E /* Contexts */,
|
||||||
@@ -435,6 +464,7 @@
|
|||||||
ECA738942BE6012D00A4542E /* ViewPort.metal */,
|
ECA738942BE6012D00A4542E /* ViewPort.metal */,
|
||||||
ECA738962BE6014200A4542E /* Graphic.metal */,
|
ECA738962BE6014200A4542E /* Graphic.metal */,
|
||||||
EC3565592BF060D900A4E0BF /* Quad.metal */,
|
EC3565592BF060D900A4E0BF /* Quad.metal */,
|
||||||
|
ECD12A922C1B062000B96E12 /* Photo.metal */,
|
||||||
);
|
);
|
||||||
path = Shaders;
|
path = Shaders;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -463,6 +493,7 @@
|
|||||||
ECA7389B2BE601AF00A4542E /* GridVertex.swift */,
|
ECA7389B2BE601AF00A4542E /* GridVertex.swift */,
|
||||||
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */,
|
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */,
|
||||||
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */,
|
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */,
|
||||||
|
ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */,
|
||||||
);
|
);
|
||||||
path = Vertices;
|
path = Vertices;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -484,6 +515,8 @@
|
|||||||
EC3565532BEFC6AD00A4E0BF /* View++.swift */,
|
EC3565532BEFC6AD00A4E0BF /* View++.swift */,
|
||||||
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */,
|
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */,
|
||||||
EC35655B2BF0712A00A4E0BF /* Float++.swift */,
|
EC35655B2BF0712A00A4E0BF /* Float++.swift */,
|
||||||
|
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */,
|
||||||
|
ECC995A42C1EB4CC00B2699A /* Data++.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -590,6 +623,7 @@
|
|||||||
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */,
|
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */,
|
||||||
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */,
|
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */,
|
||||||
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
|
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
|
||||||
|
ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */,
|
||||||
);
|
);
|
||||||
path = RenderPasses;
|
path = RenderPasses;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -645,6 +679,49 @@
|
|||||||
path = Core;
|
path = Core;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
ECBE52942C1D58F5006BDB3D /* PhotoPreview */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */,
|
||||||
|
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */,
|
||||||
|
);
|
||||||
|
path = PhotoPreview;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECBE529B2C1D94A4006BDB3D /* CameraView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECBE529A2C1D94A4006BDB3D /* CameraView.swift */,
|
||||||
|
);
|
||||||
|
path = CameraView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECD12A872C19EF8700B96E12 /* Elements */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECD12A8D2C1AEB8000B96E12 /* Photo */,
|
||||||
|
ECD12A882C19EF9500B96E12 /* Core */,
|
||||||
|
ECA738F92BE6130000A4542E /* Geometries */,
|
||||||
|
);
|
||||||
|
path = Elements;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECD12A882C19EF9500B96E12 /* Core */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECD12A892C19EFB000B96E12 /* Element.swift */,
|
||||||
|
);
|
||||||
|
path = Core;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
ECD12A8D2C1AEB8000B96E12 /* Photo */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECD12A8E2C1AEBA400B96E12 /* Photo.swift */,
|
||||||
|
);
|
||||||
|
path = Photo;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
ECE883B82C009DC30045C53D /* Strokes */ = {
|
ECE883B82C009DC30045C53D /* Strokes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -676,6 +753,8 @@
|
|||||||
EC0D14202BF79C73009BFE5F /* ToolObject.swift */,
|
EC0D14202BF79C73009BFE5F /* ToolObject.swift */,
|
||||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
|
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
|
||||||
EC9AB09E2C1401A40076AF58 /* EraserObject.swift */,
|
EC9AB09E2C1401A40076AF58 /* EraserObject.swift */,
|
||||||
|
ECD12A852C19EE3900B96E12 /* ElementObject.swift */,
|
||||||
|
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */,
|
||||||
);
|
);
|
||||||
path = Objects;
|
path = Objects;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -763,6 +842,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */,
|
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */,
|
||||||
|
ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */,
|
||||||
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
|
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
|
||||||
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
|
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
|
||||||
EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */,
|
EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */,
|
||||||
@@ -786,11 +866,13 @@
|
|||||||
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
|
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
|
||||||
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
|
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
|
||||||
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */,
|
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */,
|
||||||
|
EC37FB122C1B2DD90008D976 /* ToolSelection.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 */,
|
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */,
|
||||||
|
ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */,
|
||||||
EC2BEBF62C0F600D005DB0AF /* Box.swift in Sources */,
|
EC2BEBF62C0F600D005DB0AF /* Box.swift in Sources */,
|
||||||
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
||||||
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
|
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
|
||||||
@@ -805,6 +887,9 @@
|
|||||||
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
|
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
|
||||||
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */,
|
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */,
|
||||||
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
|
||||||
|
ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */,
|
||||||
|
ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */,
|
||||||
|
ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */,
|
||||||
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */,
|
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */,
|
||||||
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
||||||
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
|
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
|
||||||
@@ -817,6 +902,8 @@
|
|||||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
||||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
||||||
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
|
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
|
||||||
|
ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */,
|
||||||
|
ECD12A932C1B062000B96E12 /* Photo.metal in Sources */,
|
||||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||||
EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */,
|
EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */,
|
||||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||||
@@ -824,6 +911,7 @@
|
|||||||
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
||||||
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
|
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
|
||||||
EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */,
|
EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */,
|
||||||
|
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */,
|
||||||
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
|
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
|
||||||
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
|
||||||
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
|
||||||
@@ -836,12 +924,16 @@
|
|||||||
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */,
|
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */,
|
||||||
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */,
|
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */,
|
||||||
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
|
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
|
||||||
|
ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */,
|
||||||
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
|
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
|
||||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||||
EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */,
|
EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */,
|
||||||
|
ECC995A52C1EB4CC00B2699A /* Data++.swift in Sources */,
|
||||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||||
|
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */,
|
||||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
||||||
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */,
|
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */,
|
||||||
|
ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */,
|
||||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||||
@@ -992,6 +1084,8 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTLLINKER_FLAGS = "";
|
||||||
|
MTL_COMPILER_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.Memola;
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.Memola;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
@@ -1025,6 +1119,8 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTLLINKER_FLAGS = "";
|
||||||
|
MTL_COMPILER_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.Memola;
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.Memola;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// PhotoVertex.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PhotoVertex {
|
||||||
|
var position: vector_float4
|
||||||
|
var textCoord: vector_float2
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PhotoVertex {
|
||||||
|
init(x: CGFloat, y: CGFloat, textCoord: CGPoint) {
|
||||||
|
self.position = [x.float, y.float, 0, 1]
|
||||||
|
self.textCoord = [textCoord.x.float, textCoord.y.float]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,12 +11,12 @@ import CoreData
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class GraphicContext: @unchecked Sendable {
|
final class GraphicContext: @unchecked Sendable {
|
||||||
var tree: RTree = RTree<AnyStroke>(maxEntries: 8)
|
var tree: RTree = RTree<Element>(maxEntries: 8)
|
||||||
var eraserStrokes: Set<EraserStroke> = []
|
var eraserStrokes: Set<EraserStroke> = []
|
||||||
var object: GraphicContextObject?
|
var object: GraphicContextObject?
|
||||||
|
|
||||||
var currentStroke: (any Stroke)?
|
var currentElement: Element?
|
||||||
var previousStroke: (any Stroke)?
|
var previousElement: Element?
|
||||||
|
|
||||||
var currentPoint: CGPoint?
|
var currentPoint: CGPoint?
|
||||||
var renderType: RenderType = .finished
|
var renderType: RenderType = .finished
|
||||||
@@ -48,9 +48,9 @@ final class GraphicContext: @unchecked Sendable {
|
|||||||
switch stroke.style {
|
switch stroke.style {
|
||||||
case .marker:
|
case .marker:
|
||||||
guard let penStroke = stroke.stroke(as: PenStroke.self) else { return }
|
guard let penStroke = stroke.stroke(as: PenStroke.self) else { return }
|
||||||
tree.remove(penStroke.anyStroke, in: penStroke.strokeBox)
|
tree.remove(penStroke.element, in: penStroke.strokeBox)
|
||||||
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
||||||
penStroke?.object?.graphicContext = nil
|
penStroke?.object?.element?.graphicContext = nil
|
||||||
try context.saveIfNeeded()
|
try context.saveIfNeeded()
|
||||||
context.refreshAllObjects()
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,14 @@ final class GraphicContext: @unchecked Sendable {
|
|||||||
context.refreshAllObjects()
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousStroke = nil
|
previousElement = nil
|
||||||
|
case .photo(let photo):
|
||||||
|
tree.remove(photo.element, in: photo.photoBox)
|
||||||
|
withPersistence(\.backgroundContext) { [weak photo] context in
|
||||||
|
photo?.object?.element?.graphicContext = nil
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +88,9 @@ final class GraphicContext: @unchecked Sendable {
|
|||||||
guard let penStroke = stroke.stroke(as: PenStroke.self) else {
|
guard let penStroke = stroke.stroke(as: PenStroke.self) else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
tree.insert(penStroke.anyStroke, in: penStroke.strokeBox)
|
tree.insert(penStroke.element, in: penStroke.strokeBox)
|
||||||
withPersistence(\.backgroundContext) { [weak self, weak penStroke] context in
|
withPersistence(\.backgroundContext) { [weak self, weak penStroke] context in
|
||||||
penStroke?.object?.graphicContext = self?.object
|
penStroke?.object?.element?.graphicContext = self?.object
|
||||||
try context.saveIfNeeded()
|
try context.saveIfNeeded()
|
||||||
context.refreshAllObjects()
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
@@ -104,7 +111,14 @@ final class GraphicContext: @unchecked Sendable {
|
|||||||
context.refreshAllObjects()
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousStroke = nil
|
previousElement = nil
|
||||||
|
case .photo(let photo):
|
||||||
|
tree.insert(photo.element, in: photo.photoBox)
|
||||||
|
withPersistence(\.backgroundContext) { [weak self, weak photo] context in
|
||||||
|
photo?.object?.element?.graphicContext = self?.object
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,34 +128,45 @@ extension GraphicContext {
|
|||||||
guard let object else { return }
|
guard let object else { return }
|
||||||
let queue = OperationQueue()
|
let queue = OperationQueue()
|
||||||
queue.qualityOfService = .userInteractive
|
queue.qualityOfService = .userInteractive
|
||||||
object.strokes.forEach { stroke in
|
object.elements.forEach { element in
|
||||||
guard let stroke = stroke as? StrokeObject, stroke.style == 0 else { return }
|
guard let element = element as? ElementObject else { return }
|
||||||
let _stroke = PenStroke(object: stroke)
|
switch element.type {
|
||||||
tree.insert(_stroke.anyStroke, in: _stroke.strokeBox)
|
case 0:
|
||||||
if _stroke.isVisible(in: bounds) {
|
guard let stroke = element.stroke, stroke.style == 0 else { return }
|
||||||
let id = stroke.objectID
|
let _stroke = PenStroke(object: stroke)
|
||||||
queue.addOperation { [weak self] in
|
tree.insert(_stroke.element, in: _stroke.strokeBox)
|
||||||
guard let self else { return }
|
if _stroke.isVisible(in: bounds) {
|
||||||
withPersistenceSync(\.newBackgroundContext) { [weak _stroke] context in
|
let id = stroke.objectID
|
||||||
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
queue.addOperation { [weak self] in
|
||||||
_stroke?.loadQuads(from: stroke, with: self)
|
guard let self else { return }
|
||||||
|
withPersistenceSync(\.newBackgroundContext) { [weak _stroke] context in
|
||||||
|
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
||||||
|
_stroke?.loadQuads(from: stroke, with: self)
|
||||||
|
context.refreshAllObjects()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withPersistence(\.backgroundContext) { [weak self, weak _stroke] context in
|
||||||
|
guard let self else { return }
|
||||||
|
_stroke?.loadQuads(with: self)
|
||||||
context.refreshAllObjects()
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
case 1:
|
||||||
withPersistence(\.backgroundContext) { [weak self, weak _stroke] context in
|
guard let photo = element.photo, photo.imageURL != nil else { return }
|
||||||
guard let self else { return }
|
let _photo = Photo(object: photo)
|
||||||
_stroke?.loadQuads(with: self)
|
tree.insert(_photo.element, in: _photo.photoBox)
|
||||||
context.refreshAllObjects()
|
default:
|
||||||
}
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
queue.waitUntilAllOperationsAreFinished()
|
queue.waitUntilAllOperationsAreFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadQuads(_ bounds: CGRect, on context: NSManagedObjectContext) {
|
func loadQuads(_ bounds: CGRect, on context: NSManagedObjectContext) {
|
||||||
for _stroke in self.tree.search(box: bounds.box) {
|
for element in self.tree.search(box: bounds.box) {
|
||||||
guard let stroke = _stroke.stroke(as: PenStroke.self), stroke.isEmpty else { continue }
|
guard let stroke = element.stroke(as: PenStroke.self), stroke.isEmpty else { continue }
|
||||||
stroke.loadQuads(with: self)
|
stroke.loadQuads(with: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,6 +188,7 @@ extension GraphicContext: Drawable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Stroke
|
||||||
extension GraphicContext {
|
extension GraphicContext {
|
||||||
func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke {
|
func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke {
|
||||||
let stroke: any Stroke
|
let stroke: any Stroke
|
||||||
@@ -185,8 +211,13 @@ extension GraphicContext {
|
|||||||
stroke.createdAt = _stroke.createdAt
|
stroke.createdAt = _stroke.createdAt
|
||||||
stroke.quads = []
|
stroke.quads = []
|
||||||
stroke.erasers = .init()
|
stroke.erasers = .init()
|
||||||
stroke.graphicContext = graphicContext
|
let element = ElementObject(\.backgroundContext)
|
||||||
graphicContext?.strokes.add(stroke)
|
element.createdAt = _stroke.createdAt
|
||||||
|
element.type = 0
|
||||||
|
element.graphicContext = graphicContext
|
||||||
|
stroke.element = element
|
||||||
|
element.stroke = stroke
|
||||||
|
graphicContext?.elements.add(element)
|
||||||
_stroke.object = stroke
|
_stroke.object = stroke
|
||||||
try context.saveIfNeeded()
|
try context.saveIfNeeded()
|
||||||
}
|
}
|
||||||
@@ -215,14 +246,14 @@ extension GraphicContext {
|
|||||||
}
|
}
|
||||||
stroke = eraserStroke
|
stroke = eraserStroke
|
||||||
}
|
}
|
||||||
currentStroke = stroke
|
currentElement = .stroke(stroke.anyStroke)
|
||||||
currentPoint = point
|
currentPoint = point
|
||||||
currentStroke?.begin(at: point)
|
currentElement?.stroke()?.begin(at: point)
|
||||||
return stroke
|
return stroke
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendStroke(with point: CGPoint) {
|
func appendStroke(with point: CGPoint) {
|
||||||
guard let currentStroke else { return }
|
guard let currentStroke = currentElement?.stroke() else { return }
|
||||||
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.stepRate else {
|
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.stepRate else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -231,11 +262,11 @@ extension GraphicContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func endStroke(at point: CGPoint) {
|
func endStroke(at point: CGPoint) {
|
||||||
guard currentPoint != nil, let currentStroke = currentStroke else { return }
|
guard currentPoint != nil, let currentStroke = currentElement?.stroke() else { return }
|
||||||
currentStroke.finish(at: point)
|
currentStroke.finish(at: point)
|
||||||
if let penStroke = currentStroke.stroke(as: PenStroke.self) {
|
if let penStroke = currentStroke.stroke(as: PenStroke.self) {
|
||||||
penStroke.saveQuads()
|
penStroke.saveQuads()
|
||||||
tree.insert(currentStroke.anyStroke, in: currentStroke.strokeBox)
|
tree.insert(currentStroke.element, in: currentStroke.strokeBox)
|
||||||
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
withPersistence(\.backgroundContext) { [weak penStroke] context in
|
||||||
guard let penStroke else { return }
|
guard let penStroke else { return }
|
||||||
penStroke.object?.bounds = penStroke.bounds
|
penStroke.object?.bounds = penStroke.bounds
|
||||||
@@ -252,21 +283,20 @@ extension GraphicContext {
|
|||||||
context.refreshAllObjects()
|
context.refreshAllObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousStroke = currentStroke
|
previousElement = currentElement
|
||||||
self.currentStroke = nil
|
self.currentElement = nil
|
||||||
self.currentPoint = nil
|
self.currentPoint = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelStroke() {
|
func cancelStroke() {
|
||||||
if let stroke = currentStroke {
|
if let stroke = currentElement?.stroke() {
|
||||||
switch stroke.style {
|
switch stroke.style {
|
||||||
case .marker:
|
case .marker:
|
||||||
guard let _stroke = stroke.stroke(as: PenStroke.self) else { break }
|
guard let _stroke = stroke.stroke(as: PenStroke.self) else { break }
|
||||||
withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke] context in
|
withPersistence(\.backgroundContext) { [weak graphicContext = object, weak _stroke] context in
|
||||||
guard let _stroke else { return }
|
guard let _stroke else { return }
|
||||||
if let stroke = _stroke.object {
|
if let element = _stroke.object?.element {
|
||||||
graphicContext?.strokes.remove(stroke)
|
graphicContext?.elements.remove(element)
|
||||||
context.delete(stroke)
|
|
||||||
}
|
}
|
||||||
try context.saveIfNeeded()
|
try context.saveIfNeeded()
|
||||||
}
|
}
|
||||||
@@ -281,11 +311,45 @@ extension GraphicContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentStroke = nil
|
currentElement = nil
|
||||||
currentPoint = nil
|
currentPoint = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Photo
|
||||||
|
extension GraphicContext {
|
||||||
|
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) -> Photo {
|
||||||
|
let size = photoItem.dimension
|
||||||
|
let origin = point
|
||||||
|
let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2]
|
||||||
|
let photo = Photo(url: photoItem.id, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoItem.bookmark)
|
||||||
|
tree.insert(photo.element, in: photo.photoBox)
|
||||||
|
withPersistence(\.backgroundContext) { [weak _photo = photo, weak graphicContext = object] context in
|
||||||
|
guard let _photo else { return }
|
||||||
|
let photo = PhotoObject(\.backgroundContext)
|
||||||
|
photo.imageURL = _photo.url
|
||||||
|
photo.bounds = _photo.bounds
|
||||||
|
photo.width = _photo.size.width
|
||||||
|
photo.originY = _photo.origin.y
|
||||||
|
photo.originX = _photo.origin.x
|
||||||
|
photo.height = _photo.size.height
|
||||||
|
photo.createdAt = _photo.createdAt
|
||||||
|
photo.bookmark = _photo.bookmark
|
||||||
|
let element = ElementObject(\.backgroundContext)
|
||||||
|
element.createdAt = _photo.createdAt
|
||||||
|
element.type = 1
|
||||||
|
element.graphicContext = graphicContext
|
||||||
|
photo.element = element
|
||||||
|
element.photo = photo
|
||||||
|
graphicContext?.elements.add(element)
|
||||||
|
_photo.object = photo
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
|
self.previousElement = .photo(photo)
|
||||||
|
return photo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension GraphicContext {
|
extension GraphicContext {
|
||||||
enum RenderType {
|
enum RenderType {
|
||||||
case inProgress
|
case inProgress
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hasValidStroke: Bool {
|
var hasValidStroke: Bool {
|
||||||
if let currentStroke = graphicContext.currentStroke {
|
if let currentElement = graphicContext.currentElement {
|
||||||
return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80
|
return Date.now.timeIntervalSince(currentElement.createdAt) * 1000 > 80
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ extension Canvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Graphic Context
|
// MARK: - Stroke
|
||||||
extension Canvas {
|
extension Canvas {
|
||||||
func beginTouch(at point: CGPoint, pen: Pen) -> any Stroke {
|
func beginTouch(at point: CGPoint, pen: Pen) -> any Stroke {
|
||||||
graphicContext.beginStroke(at: point, pen: pen)
|
graphicContext.beginStroke(at: point, pen: pen)
|
||||||
@@ -126,6 +126,13 @@ extension Canvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Photo
|
||||||
|
extension Canvas {
|
||||||
|
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) -> Photo {
|
||||||
|
graphicContext.insertPhoto(at: point, photoItem: photoItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Rendering
|
// MARK: - Rendering
|
||||||
extension Canvas {
|
extension Canvas {
|
||||||
func renderGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
func renderGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||||
|
|||||||
@@ -93,6 +93,28 @@ struct PipelineStates {
|
|||||||
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func createPhotoPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
|
||||||
|
let device = renderer.device
|
||||||
|
let library = renderer.library
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
|
||||||
|
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_photo")
|
||||||
|
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_photo")
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
|
||||||
|
pipelineDescriptor.label = "Photo Pipeline State"
|
||||||
|
|
||||||
|
let attachment = pipelineDescriptor.colorAttachments[0]
|
||||||
|
attachment?.isBlendingEnabled = true
|
||||||
|
attachment?.rgbBlendOperation = .add
|
||||||
|
attachment?.sourceRGBBlendFactor = .sourceAlpha
|
||||||
|
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||||
|
attachment?.alphaBlendOperation = .add
|
||||||
|
attachment?.sourceAlphaBlendFactor = .one
|
||||||
|
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||||
|
|
||||||
|
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
static func createViewPortPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil, isUpdate: Bool = false) -> MTLRenderPipelineState? {
|
static func createViewPortPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil, isUpdate: Bool = false) -> MTLRenderPipelineState? {
|
||||||
var label: String
|
var label: String
|
||||||
var vertexName: String
|
var vertexName: String
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ final class Renderer {
|
|||||||
lazy var eraserRenderPass: EraserRenderPass = {
|
lazy var eraserRenderPass: EraserRenderPass = {
|
||||||
EraserRenderPass(renderer: self)
|
EraserRenderPass(renderer: self)
|
||||||
}()
|
}()
|
||||||
|
lazy var photoRenderPass: PhotoRenderPass = {
|
||||||
|
PhotoRenderPass(renderer: self)
|
||||||
|
}()
|
||||||
lazy var graphicRenderPass: GraphicRenderPass = {
|
lazy var graphicRenderPass: GraphicRenderPass = {
|
||||||
GraphicRenderPass(renderer: self)
|
GraphicRenderPass(renderer: self)
|
||||||
}()
|
}()
|
||||||
@@ -66,12 +69,14 @@ final class Renderer {
|
|||||||
|
|
||||||
func draw(in view: MTKView, on canvas: Canvas) {
|
func draw(in view: MTKView, on canvas: Canvas) {
|
||||||
if !updatesViewPort {
|
if !updatesViewPort {
|
||||||
|
graphicRenderPass.photoRenderPass = photoRenderPass
|
||||||
graphicRenderPass.strokeRenderPass = strokeRenderPass
|
graphicRenderPass.strokeRenderPass = strokeRenderPass
|
||||||
graphicRenderPass.eraserRenderPass = eraserRenderPass
|
graphicRenderPass.eraserRenderPass = eraserRenderPass
|
||||||
graphicRenderPass.draw(on: canvas, with: self)
|
graphicRenderPass.draw(on: canvas, with: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture
|
cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture
|
||||||
|
cacheRenderPass.photoRenderPass = photoRenderPass
|
||||||
cacheRenderPass.strokeRenderPass = strokeRenderPass
|
cacheRenderPass.strokeRenderPass = strokeRenderPass
|
||||||
cacheRenderPass.eraserRenderPass = eraserRenderPass
|
cacheRenderPass.eraserRenderPass = eraserRenderPass
|
||||||
cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture
|
cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture
|
||||||
|
|||||||
@@ -20,12 +20,34 @@ class Textures {
|
|||||||
if let penTexture = penTextures[textureName] {
|
if let penTexture = penTextures[textureName] {
|
||||||
return penTexture
|
return penTexture
|
||||||
}
|
}
|
||||||
|
let options: [MTKTextureLoader.Option: Any] = [
|
||||||
|
.SRGB: false,
|
||||||
|
.generateMipmaps: true,
|
||||||
|
.textureStorageMode: NSNumber(value: MTLStorageMode.private.rawValue)
|
||||||
|
]
|
||||||
let textureLoader = MTKTextureLoader(device: device)
|
let textureLoader = MTKTextureLoader(device: device)
|
||||||
let penTexture = try? textureLoader.newTexture(name: textureName, scaleFactor: 1.0, bundle: .main, options: [.SRGB: false])
|
let penTexture = try? textureLoader.newTexture(name: textureName, scaleFactor: 1.0, bundle: .main, options: options)
|
||||||
penTextures[textureName] = penTexture
|
penTextures[textureName] = penTexture
|
||||||
return penTexture
|
return penTexture
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func createPhotoTexture(for url: URL, on device: MTLDevice) -> MTLTexture? {
|
||||||
|
let textureLoader = MTKTextureLoader(device: device)
|
||||||
|
do {
|
||||||
|
let options: [MTKTextureLoader.Option: Any] = [
|
||||||
|
.SRGB: false,
|
||||||
|
.generateMipmaps: true,
|
||||||
|
.textureStorageMode: NSNumber(value: MTLStorageMode.private.rawValue)
|
||||||
|
]
|
||||||
|
let photoTexture = try textureLoader.newTexture(URL: url, options: options)
|
||||||
|
return photoTexture
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func createGraphicTexture(
|
static func createGraphicTexture(
|
||||||
from renderer: Renderer,
|
from renderer: Renderer,
|
||||||
size: CGSize,
|
size: CGSize,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// Element.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum Element: Equatable, Comparable {
|
||||||
|
case stroke(AnyStroke)
|
||||||
|
case photo(Photo)
|
||||||
|
|
||||||
|
func stroke() -> (any Stroke)? {
|
||||||
|
guard case let .stroke(anyStroke) = self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return anyStroke.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func stroke<S: Stroke>(as type: S.Type) -> S? {
|
||||||
|
guard case let .stroke(anyStroke) = self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return anyStroke.stroke(as: type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func photo() -> Photo? {
|
||||||
|
guard case let .photo(photo) = self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return photo
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdAt: Date {
|
||||||
|
switch self {
|
||||||
|
case .stroke(let anyStroke):
|
||||||
|
anyStroke.value.createdAt
|
||||||
|
case .photo(let photo):
|
||||||
|
photo.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: Element, rhs: Element) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case let (.stroke(leftStroke), .stroke(rightStroke)):
|
||||||
|
leftStroke < rightStroke
|
||||||
|
case let (.photo(leftPhoto), .photo(rightPhoto)):
|
||||||
|
leftPhoto < rightPhoto
|
||||||
|
case let (.photo(photo), .stroke(stroke)):
|
||||||
|
photo.createdAt < stroke.value.createdAt
|
||||||
|
case let (.stroke(stroke), .photo(photo)):
|
||||||
|
stroke.value.createdAt < photo.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -125,4 +125,8 @@ extension Stroke {
|
|||||||
var anyStroke: AnyStroke {
|
var anyStroke: AnyStroke {
|
||||||
AnyStroke(self)
|
AnyStroke(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var element: Element {
|
||||||
|
.stroke(anyStroke)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//
|
||||||
|
// Photo.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class Photo: @unchecked Sendable, Equatable, Comparable {
|
||||||
|
var id: UUID = UUID()
|
||||||
|
var size: CGSize
|
||||||
|
var origin: CGPoint
|
||||||
|
var image: UIImage?
|
||||||
|
var url: URL?
|
||||||
|
var bounds: [CGFloat]
|
||||||
|
var createdAt: Date
|
||||||
|
var bookmark: Data?
|
||||||
|
|
||||||
|
var object: PhotoObject?
|
||||||
|
|
||||||
|
var texture: MTLTexture?
|
||||||
|
var vertices: [PhotoVertex] = []
|
||||||
|
var vertexCount: Int = 0
|
||||||
|
var vertexBuffer: MTLBuffer?
|
||||||
|
|
||||||
|
init(url: URL?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date, bookmark: Data?) {
|
||||||
|
self.size = size
|
||||||
|
self.origin = origin
|
||||||
|
self.url = url
|
||||||
|
self.bounds = bounds
|
||||||
|
self.createdAt = createdAt
|
||||||
|
self.bookmark = bookmark
|
||||||
|
generateVertices()
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(object: PhotoObject) {
|
||||||
|
self.init(
|
||||||
|
url: object.imageURL,
|
||||||
|
size: .init(width: object.width, height: object.height),
|
||||||
|
origin: .init(x: object.originX, y: object.originY),
|
||||||
|
bounds: object.bounds,
|
||||||
|
createdAt: object.createdAt ?? .now,
|
||||||
|
bookmark: object.bookmark
|
||||||
|
)
|
||||||
|
self.object = object
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateVertices() {
|
||||||
|
let minX = origin.x - size.width / 2
|
||||||
|
let maxX = origin.x + size.width / 2
|
||||||
|
let minY = origin.y - size.height / 2
|
||||||
|
let maxY = origin.y + size.height / 2
|
||||||
|
vertices = [
|
||||||
|
PhotoVertex(x: minX, y: minY, textCoord: CGPoint(x: 0, y: 0)),
|
||||||
|
PhotoVertex(x: minX, y: maxY, textCoord: CGPoint(x: 0, y: 1)),
|
||||||
|
PhotoVertex(x: maxX, y: minY, textCoord: CGPoint(x: 1, y: 0)),
|
||||||
|
PhotoVertex(x: maxX, y: maxY, textCoord: CGPoint(x: 1, y: 1)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Photo: Drawable {
|
||||||
|
func prepare(device: any MTLDevice) {
|
||||||
|
if vertexBuffer == nil {
|
||||||
|
vertexCount = vertices.endIndex
|
||||||
|
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PhotoVertex>.stride, options: [])
|
||||||
|
}
|
||||||
|
if texture == nil, let url = bookmark?.getBookmarkURL() {
|
||||||
|
texture = Textures.createPhotoTexture(for: url, on: device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw(device: any MTLDevice, renderEncoder: any MTLRenderCommandEncoder) {
|
||||||
|
prepare(device: device)
|
||||||
|
renderEncoder.setFragmentTexture(texture, index: 0)
|
||||||
|
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||||
|
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertices.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Photo {
|
||||||
|
static func == (lhs: Photo, rhs: Photo) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: Photo, rhs: Photo) -> Bool {
|
||||||
|
lhs.createdAt < rhs.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Photo {
|
||||||
|
var photoBounds: CGRect {
|
||||||
|
let x = bounds[0]
|
||||||
|
let y = bounds[1]
|
||||||
|
let width = bounds[2] - x
|
||||||
|
let height = bounds[3] - y
|
||||||
|
return CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
var photoBox: Box {
|
||||||
|
Box(minX: bounds[0], minY: bounds[1], maxX: bounds[2], maxY: bounds[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func isVisible(in bounds: CGRect) -> Bool {
|
||||||
|
bounds.contains(photoBounds) || bounds.intersects(photoBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
var element: Element {
|
||||||
|
.photo(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,20 @@ class History: ObservableObject {
|
|||||||
try context.saveIfNeeded()
|
try context.saveIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case .photo(let _photo):
|
||||||
|
if let url = _photo.bookmark?.getBookmarkURL() {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withPersistence(\.backgroundContext) { context in
|
||||||
|
if let photo = _photo.object {
|
||||||
|
context.delete(photo)
|
||||||
|
}
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
redoStack.removeAll()
|
redoStack.removeAll()
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ import Foundation
|
|||||||
|
|
||||||
enum HistoryEvent {
|
enum HistoryEvent {
|
||||||
case stroke(any Stroke)
|
case stroke(any Stroke)
|
||||||
|
case photo(Photo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ class RTree<T> where T: Equatable & Comparable {
|
|||||||
.sorted(by: <)
|
.sorted(by: <)
|
||||||
result = _merge(result, children)
|
result = _merge(result, children)
|
||||||
} else {
|
} else {
|
||||||
queue.append(contentsOf: node.children)
|
let nodes = node.children.sorted {
|
||||||
|
guard let first = $0.value, let second = $1.value else { return false }
|
||||||
|
return first < second
|
||||||
|
}
|
||||||
|
queue.append(contentsOf: nodes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class CacheRenderPass: RenderPass {
|
|||||||
weak var graphicTexture: MTLTexture?
|
weak var graphicTexture: MTLTexture?
|
||||||
var cacheTexture: MTLTexture?
|
var cacheTexture: MTLTexture?
|
||||||
|
|
||||||
|
weak var photoRenderPass: PhotoRenderPass?
|
||||||
weak var strokeRenderPass: StrokeRenderPass?
|
weak var strokeRenderPass: StrokeRenderPass?
|
||||||
weak var eraserRenderPass: EraserRenderPass?
|
weak var eraserRenderPass: EraserRenderPass?
|
||||||
var clearsTexture: Bool = true
|
var clearsTexture: Bool = true
|
||||||
@@ -34,7 +35,7 @@ class CacheRenderPass: RenderPass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
guard let descriptor, let strokeRenderPass, let eraserRenderPass else { return }
|
guard let descriptor, let strokeRenderPass, let eraserRenderPass, let photoRenderPass else { return }
|
||||||
|
|
||||||
copyTexture(on: canvas, with: renderer)
|
copyTexture(on: canvas, with: renderer)
|
||||||
|
|
||||||
@@ -45,18 +46,26 @@ class CacheRenderPass: RenderPass {
|
|||||||
descriptor.colorAttachments[0].storeAction = .store
|
descriptor.colorAttachments[0].storeAction = .store
|
||||||
|
|
||||||
let graphicContext = canvas.graphicContext
|
let graphicContext = canvas.graphicContext
|
||||||
if let stroke = graphicContext.currentStroke {
|
if let element = graphicContext.currentElement {
|
||||||
switch stroke.style {
|
switch element {
|
||||||
case .eraser:
|
case .stroke(let anyStroke):
|
||||||
eraserRenderPass.stroke = stroke
|
let stroke = anyStroke.value
|
||||||
eraserRenderPass.descriptor = descriptor
|
switch stroke.style {
|
||||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
case .eraser:
|
||||||
case .marker:
|
eraserRenderPass.stroke = stroke
|
||||||
canvas.setGraphicRenderType(.inProgress)
|
eraserRenderPass.descriptor = descriptor
|
||||||
strokeRenderPass.stroke = stroke
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
strokeRenderPass.graphicDescriptor = descriptor
|
case .marker:
|
||||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
canvas.setGraphicRenderType(.inProgress)
|
||||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
strokeRenderPass.stroke = stroke
|
||||||
|
strokeRenderPass.graphicDescriptor = descriptor
|
||||||
|
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||||
|
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
|
case .photo(let photo):
|
||||||
|
photoRenderPass.photo = photo
|
||||||
|
photoRenderPass.descriptor = descriptor
|
||||||
|
photoRenderPass.draw(on: canvas, with: renderer)
|
||||||
}
|
}
|
||||||
clearsTexture = false
|
clearsTexture = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class GraphicRenderPass: RenderPass {
|
|||||||
|
|
||||||
var graphicPipelineState: MTLRenderPipelineState?
|
var graphicPipelineState: MTLRenderPipelineState?
|
||||||
|
|
||||||
|
weak var photoRenderPass: PhotoRenderPass?
|
||||||
weak var strokeRenderPass: StrokeRenderPass?
|
weak var strokeRenderPass: StrokeRenderPass?
|
||||||
weak var eraserRenderPass: EraserRenderPass?
|
weak var eraserRenderPass: EraserRenderPass?
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ class GraphicRenderPass: RenderPass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
guard let strokeRenderPass, let eraserRenderPass else { return }
|
guard let strokeRenderPass, let eraserRenderPass, let photoRenderPass else { return }
|
||||||
guard let descriptor else { return }
|
guard let descriptor else { return }
|
||||||
|
|
||||||
guard let graphicPipelineState else { return }
|
guard let graphicPipelineState else { return }
|
||||||
@@ -45,21 +46,59 @@ class GraphicRenderPass: RenderPass {
|
|||||||
let graphicContext = canvas.graphicContext
|
let graphicContext = canvas.graphicContext
|
||||||
if renderer.redrawsGraphicRender {
|
if renderer.redrawsGraphicRender {
|
||||||
canvas.setGraphicRenderType(.finished)
|
canvas.setGraphicRenderType(.finished)
|
||||||
for _stroke in graphicContext.tree.search(box: canvas.bounds.box) {
|
for _element in graphicContext.tree.search(box: canvas.bounds.box) {
|
||||||
let stroke = _stroke.value
|
if graphicContext.previousElement == _element || graphicContext.currentElement == _element {
|
||||||
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
guard stroke.isVisible(in: canvas.bounds) else { continue }
|
switch _element {
|
||||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
case .stroke(let _stroke):
|
||||||
clearsTexture = false
|
let stroke = _stroke.value
|
||||||
|
guard stroke.isVisible(in: canvas.bounds) else { continue }
|
||||||
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
|
clearsTexture = false
|
||||||
|
switch stroke.style {
|
||||||
|
case .eraser:
|
||||||
|
eraserRenderPass.stroke = stroke
|
||||||
|
eraserRenderPass.descriptor = descriptor
|
||||||
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
case .marker:
|
||||||
|
canvas.setGraphicRenderType(.finished)
|
||||||
|
strokeRenderPass.stroke = stroke
|
||||||
|
strokeRenderPass.graphicDescriptor = descriptor
|
||||||
|
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||||
|
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
|
||||||
|
if let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads {
|
||||||
|
descriptor.colorAttachments[0].loadAction = .load
|
||||||
|
eraserRenderPass.stroke = stroke
|
||||||
|
eraserRenderPass.descriptor = descriptor
|
||||||
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .photo(let photo):
|
||||||
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
|
clearsTexture = false
|
||||||
|
photoRenderPass.photo = photo
|
||||||
|
photoRenderPass.descriptor = descriptor
|
||||||
|
photoRenderPass.draw(on: canvas, with: renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderer.redrawsGraphicRender = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let element = graphicContext.previousElement {
|
||||||
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
|
clearsTexture = false
|
||||||
|
switch element {
|
||||||
|
case .stroke(let anyStroke):
|
||||||
|
let stroke = anyStroke.value
|
||||||
switch stroke.style {
|
switch stroke.style {
|
||||||
case .eraser:
|
case .eraser:
|
||||||
eraserRenderPass.stroke = stroke
|
eraserRenderPass.stroke = stroke
|
||||||
eraserRenderPass.descriptor = descriptor
|
eraserRenderPass.descriptor = descriptor
|
||||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
case .marker:
|
case .marker:
|
||||||
canvas.setGraphicRenderType(.finished)
|
canvas.setGraphicRenderType(.newlyFinished)
|
||||||
strokeRenderPass.stroke = stroke
|
strokeRenderPass.stroke = stroke
|
||||||
strokeRenderPass.graphicDescriptor = descriptor
|
strokeRenderPass.graphicDescriptor = descriptor
|
||||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||||
@@ -72,33 +111,12 @@ class GraphicRenderPass: RenderPass {
|
|||||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case .photo(let photo):
|
||||||
|
photoRenderPass.photo = photo
|
||||||
|
photoRenderPass.descriptor = descriptor
|
||||||
|
photoRenderPass.draw(on: canvas, with: renderer)
|
||||||
}
|
}
|
||||||
renderer.redrawsGraphicRender = false
|
graphicContext.previousElement = nil
|
||||||
}
|
|
||||||
|
|
||||||
if let stroke = graphicContext.previousStroke {
|
|
||||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
|
||||||
clearsTexture = false
|
|
||||||
switch stroke.style {
|
|
||||||
case .eraser:
|
|
||||||
eraserRenderPass.stroke = stroke
|
|
||||||
eraserRenderPass.descriptor = descriptor
|
|
||||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
|
||||||
case .marker:
|
|
||||||
canvas.setGraphicRenderType(.newlyFinished)
|
|
||||||
strokeRenderPass.stroke = stroke
|
|
||||||
strokeRenderPass.graphicDescriptor = descriptor
|
|
||||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
|
||||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
|
||||||
|
|
||||||
if let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads {
|
|
||||||
descriptor.colorAttachments[0].loadAction = .load
|
|
||||||
eraserRenderPass.stroke = stroke
|
|
||||||
eraserRenderPass.descriptor = descriptor
|
|
||||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graphicContext.previousStroke = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let eraserStrokes = graphicContext.eraserStrokes
|
let eraserStrokes = graphicContext.eraserStrokes
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// PhotoRenderPass.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MetalKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class PhotoRenderPass: RenderPass {
|
||||||
|
var label: String = "Photo Render Pass"
|
||||||
|
|
||||||
|
var descriptor: MTLRenderPassDescriptor?
|
||||||
|
|
||||||
|
var photoPipelineState: MTLRenderPipelineState?
|
||||||
|
weak var graphicTexture: MTLTexture?
|
||||||
|
|
||||||
|
var photo: Photo?
|
||||||
|
|
||||||
|
init(renderer: Renderer) {
|
||||||
|
photoPipelineState = PipelineStates.createPhotoPipelineState(from: renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
||||||
|
|
||||||
|
func draw(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
guard let descriptor else { return }
|
||||||
|
|
||||||
|
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
||||||
|
commandBuffer.label = "Photo Command Buffer"
|
||||||
|
|
||||||
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
|
||||||
|
renderEncoder.label = label
|
||||||
|
|
||||||
|
guard let photoPipelineState else { return }
|
||||||
|
renderEncoder.setRenderPipelineState(photoPipelineState)
|
||||||
|
|
||||||
|
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
photo?.draw(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
|
|
||||||
|
renderEncoder.endEncoding()
|
||||||
|
commandBuffer.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// Photo.metal
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct VertexIn {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 textCoord;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexOut {
|
||||||
|
float4 position [[position]];
|
||||||
|
float2 textCoord;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
float4x4 transform;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex VertexOut vertex_photo(
|
||||||
|
constant VertexIn *vertices [[buffer(0)]],
|
||||||
|
constant Uniforms &uniforms [[buffer(11)]],
|
||||||
|
uint vertexId [[vertex_id]]
|
||||||
|
) {
|
||||||
|
VertexIn in = vertices[vertexId];
|
||||||
|
|
||||||
|
VertexOut out;
|
||||||
|
out.position = uniforms.transform * in.position;
|
||||||
|
out.textCoord = in.textCoord;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment float4 fragment_photo(
|
||||||
|
VertexOut out [[stage_in]],
|
||||||
|
texture2d<float> texture [[texture(0)]]
|
||||||
|
) {
|
||||||
|
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
|
||||||
|
float4 color = float4(texture.sample(textureSampler, out.textCoord));
|
||||||
|
return color;
|
||||||
|
}
|
||||||
@@ -14,8 +14,14 @@ public class Tool: NSObject, ObservableObject {
|
|||||||
let object: ToolObject
|
let object: ToolObject
|
||||||
|
|
||||||
@Published var pens: [Pen] = []
|
@Published var pens: [Pen] = []
|
||||||
|
|
||||||
|
// MARK: - Pen
|
||||||
@Published var selectedPen: Pen?
|
@Published var selectedPen: Pen?
|
||||||
@Published var draggedPen: Pen?
|
@Published var draggedPen: Pen?
|
||||||
|
// MARK: - Photo
|
||||||
|
@Published var selectedPhotoItem: PhotoItem?
|
||||||
|
|
||||||
|
@Published var selection: ToolSelection = .none
|
||||||
|
|
||||||
let scrollPublisher = PassthroughSubject<String, Never>()
|
let scrollPublisher = PassthroughSubject<String, Never>()
|
||||||
var markers: [Pen] {
|
var markers: [Pen] {
|
||||||
@@ -106,4 +112,80 @@ public class Tool: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) {
|
||||||
|
guard let resizedImage = resizePhoto(of: image) else { return }
|
||||||
|
let photoItem = bookmarkPhoto(of: resizedImage, with: canvasID)
|
||||||
|
withAnimation {
|
||||||
|
selectedPhotoItem = photoItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resizePhoto(of image: UIImage) -> UIImage? {
|
||||||
|
let targetSize = CGSize(width: 768, height: 768)
|
||||||
|
let size = image.size
|
||||||
|
let widthRatio = targetSize.width / size.width
|
||||||
|
let heightRatio = targetSize.height / size.height
|
||||||
|
let newSize = CGSize(
|
||||||
|
width: size.width * min(widthRatio, heightRatio),
|
||||||
|
height: size.height * min(widthRatio, heightRatio)
|
||||||
|
)
|
||||||
|
let rect = CGRect(origin: .zero, size: newSize)
|
||||||
|
|
||||||
|
UIGraphicsBeginImageContextWithOptions(newSize, true, 1.0)
|
||||||
|
image.draw(in: rect)
|
||||||
|
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
|
UIGraphicsEndImageContext()
|
||||||
|
|
||||||
|
return newImage
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bookmarkPhoto(of image: UIImage, with canvasID: NSManagedObjectID) -> PhotoItem? {
|
||||||
|
guard let data = image.jpegData(compressionQuality: 1) else { return nil }
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let fileName = "\(UUID().uuidString)-\(Int(Date.now.timeIntervalSince1970))"
|
||||||
|
let folder = directory.appendingPathComponent(canvasID.uriRepresentation().lastPathComponent, conformingTo: .folder)
|
||||||
|
|
||||||
|
if !fileManager.fileExists(atPath: folder.path()) {
|
||||||
|
do {
|
||||||
|
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let file = folder.appendingPathComponent(fileName, conformingTo: .jpeg)
|
||||||
|
do {
|
||||||
|
try data.write(to: file)
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var photoBookmark: PhotoItem?
|
||||||
|
do {
|
||||||
|
let bookmark = try file.bookmarkData(options: .minimalBookmark)
|
||||||
|
photoBookmark = PhotoItem(id: file, image: image, bookmark: bookmark)
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
return photoBookmark
|
||||||
|
}
|
||||||
|
|
||||||
|
func unselectPhoto() {
|
||||||
|
guard let photoItem = selectedPhotoItem else { return }
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
if let url = photoItem.bookmark.getBookmarkURL() {
|
||||||
|
do {
|
||||||
|
try fileManager.removeItem(at: url)
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withAnimation {
|
||||||
|
selectedPhotoItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// ToolSelection.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ToolSelection: Equatable {
|
||||||
|
case none
|
||||||
|
case pen
|
||||||
|
case photo
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ class CanvasViewController: UIViewController {
|
|||||||
drawingView.renderView
|
drawingView.renderView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var photoInsertGesture: UITapGestureRecognizer?
|
||||||
|
|
||||||
let tool: Tool
|
let tool: Tool
|
||||||
let canvas: Canvas
|
let canvas: Canvas
|
||||||
let history: History
|
let history: History
|
||||||
@@ -40,6 +42,7 @@ class CanvasViewController: UIViewController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
configureViews()
|
configureViews()
|
||||||
|
configureGestures()
|
||||||
configureListeners()
|
configureListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +179,11 @@ extension CanvasViewController {
|
|||||||
self?.penChanged(to: pen)
|
self?.penChanged(to: pen)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
tool.$selection
|
||||||
|
.sink { [weak self] selection in
|
||||||
|
self?.toolSelectionChanged(to: selection)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
history.historyPublisher
|
history.historyPublisher
|
||||||
.sink { [weak self] action in
|
.sink { [weak self] action in
|
||||||
@@ -216,6 +224,26 @@ extension CanvasViewController: MTKViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension CanvasViewController {
|
||||||
|
func configureGestures() {
|
||||||
|
let photoInsertGesture = UITapGestureRecognizer(target: self, action: #selector(recognizeTapGesture))
|
||||||
|
photoInsertGesture.numberOfTapsRequired = 1
|
||||||
|
self.photoInsertGesture = photoInsertGesture
|
||||||
|
scrollView.addGestureRecognizer(photoInsertGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard let photoItem = tool.selectedPhotoItem else { return }
|
||||||
|
withAnimation {
|
||||||
|
tool.selectedPhotoItem = nil
|
||||||
|
}
|
||||||
|
let point = gesture.location(in: drawingView)
|
||||||
|
let photo = canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), photoItem: photoItem)
|
||||||
|
history.addUndo(.photo(photo))
|
||||||
|
drawingView.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension CanvasViewController: UIScrollViewDelegate {
|
extension CanvasViewController: UIScrollViewDelegate {
|
||||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||||
drawingView
|
drawingView
|
||||||
@@ -300,10 +328,31 @@ extension CanvasViewController {
|
|||||||
if let pen, let device = drawingView.renderView.device {
|
if let pen, let device = drawingView.renderView.device {
|
||||||
pen.style.loadTexture(on: device)
|
pen.style.loadTexture(on: device)
|
||||||
}
|
}
|
||||||
let isPenSelected = pen != nil
|
}
|
||||||
scrollView.isScrollEnabled = !isPenSelected
|
|
||||||
drawingView.isUserInteractionEnabled = isPenSelected
|
func toolSelectionChanged(to selection: ToolSelection) {
|
||||||
isPenSelected ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction()
|
let enablesScrolling: Bool
|
||||||
|
let enablesDrawing: Bool
|
||||||
|
let enablesPhotoInsertion: Bool
|
||||||
|
switch selection {
|
||||||
|
case .none:
|
||||||
|
enablesScrolling = true
|
||||||
|
enablesDrawing = false
|
||||||
|
enablesPhotoInsertion = false
|
||||||
|
case .pen:
|
||||||
|
enablesScrolling = false
|
||||||
|
enablesDrawing = true
|
||||||
|
enablesPhotoInsertion = false
|
||||||
|
penChanged(to: tool.selectedPen)
|
||||||
|
case .photo:
|
||||||
|
enablesScrolling = true
|
||||||
|
enablesDrawing = false
|
||||||
|
enablesPhotoInsertion = true
|
||||||
|
}
|
||||||
|
scrollView.isScrollEnabled = enablesScrolling
|
||||||
|
drawingView.isUserInteractionEnabled = enablesDrawing
|
||||||
|
photoInsertGesture?.isEnabled = enablesPhotoInsertion
|
||||||
|
enablesDrawing ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DrawingView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateDrawableSize(with size: CGSize) {
|
func updateDrawableSize(with size: CGSize) {
|
||||||
renderView.drawableSize = size.multiply(by: 3)
|
renderView.drawableSize = size.multiply(by: 2.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
@@ -87,20 +87,20 @@ class DrawingView: UIView {
|
|||||||
guard !disablesUserInteraction else { return }
|
guard !disablesUserInteraction else { return }
|
||||||
canvas.moveTouch(to: point.muliply(by: ratio))
|
canvas.moveTouch(to: point.muliply(by: ratio))
|
||||||
if canvas.hasValidStroke {
|
if canvas.hasValidStroke {
|
||||||
renderView.draw()
|
draw()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func touchEnded(at point: CGPoint) {
|
func touchEnded(at point: CGPoint) {
|
||||||
guard !disablesUserInteraction else { return }
|
guard !disablesUserInteraction else { return }
|
||||||
canvas.endTouch(at: point.muliply(by: ratio))
|
canvas.endTouch(at: point.muliply(by: ratio))
|
||||||
renderView.draw()
|
draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
func touchCancelled() {
|
func touchCancelled() {
|
||||||
if canvas.graphicContext.currentStroke != nil {
|
if canvas.graphicContext.currentElement != nil {
|
||||||
canvas.cancelTouch()
|
canvas.cancelTouch()
|
||||||
renderView.draw()
|
draw()
|
||||||
history.restoreUndo()
|
history.restoreUndo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,4 +114,8 @@ class DrawingView: UIView {
|
|||||||
self?.disablesUserInteraction = false
|
self?.disablesUserInteraction = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func draw() {
|
||||||
|
renderView.draw()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// CameraView.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/15/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CameraView: UIViewControllerRepresentable {
|
||||||
|
@Binding var image: UIImage?
|
||||||
|
|
||||||
|
@ObservedObject var canvas: Canvas
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||||
|
let picker = UIImagePickerController()
|
||||||
|
picker.sourceType = .camera
|
||||||
|
picker.delegate = context.coordinator
|
||||||
|
return picker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { }
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
|
||||||
|
let parent: CameraView
|
||||||
|
|
||||||
|
init(_ parent: CameraView) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||||
|
parent.image = (info[.originalImage] as? UIImage)?.imageWithUpOrientation()
|
||||||
|
parent.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||||
|
parent.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Memola requires access to the camera to capture photos.</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Data++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/16/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
func getBookmarkURL() -> URL? {
|
||||||
|
var isStale = false
|
||||||
|
guard let bookmarkURL = try? URL(resolvingBookmarkData: self, options: .withoutUI, relativeTo: nil, bookmarkDataIsStale: &isStale) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bookmarkURL
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// UIImage++.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/15/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension UIImage {
|
||||||
|
func imageWithUpOrientation() -> UIImage? {
|
||||||
|
switch imageOrientation {
|
||||||
|
case .up:
|
||||||
|
return self
|
||||||
|
default:
|
||||||
|
UIGraphicsBeginImageContextWithOptions(size, false, scale)
|
||||||
|
draw(in: CGRect(origin: .zero, size: size))
|
||||||
|
let result = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
|
UIGraphicsEndImageContext()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,15 +29,26 @@ struct MemoView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
CanvasView(tool: tool, canvas: canvas, history: history)
|
CanvasView(tool: tool, canvas: canvas, history: history)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.overlay(alignment: .trailing) {
|
.overlay(alignment: .bottomTrailing) {
|
||||||
PenDock(tool: tool, canvas: canvas)
|
switch tool.selection {
|
||||||
|
case .pen:
|
||||||
|
PenDock(tool: tool, canvas: canvas)
|
||||||
|
.transition(.move(edge: .trailing))
|
||||||
|
case .photo:
|
||||||
|
if let photoItem = tool.selectedPhotoItem {
|
||||||
|
PhotoPreview(photoItem: photoItem, tool: tool)
|
||||||
|
.transition(.move(edge: .trailing))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.overlay(alignment: .bottomLeading) {
|
.overlay(alignment: .bottomLeading) {
|
||||||
zoomControl
|
zoomControl
|
||||||
}
|
}
|
||||||
.disabled(textFieldState)
|
.disabled(textFieldState)
|
||||||
.overlay(alignment: .top) {
|
.overlay(alignment: .top) {
|
||||||
Toolbar(size: size, memo: memo, canvas: canvas, history: history)
|
Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history)
|
||||||
}
|
}
|
||||||
.disabled(canvas.state == .loading || canvas.state == .closing)
|
.disabled(canvas.state == .loading || canvas.state == .closing)
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ struct PenDock: View {
|
|||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
|
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if tool.selectedPen === pen {
|
if tool.selectedPen !== pen {
|
||||||
tool.unselectPen(pen)
|
|
||||||
} else {
|
|
||||||
tool.selectPen(pen)
|
tool.selectPen(pen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// PhotoItem.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/16/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PhotoItem: Identifiable, Equatable {
|
||||||
|
var id: URL
|
||||||
|
let image: UIImage
|
||||||
|
let bookmark: Data
|
||||||
|
|
||||||
|
var dimension: CGSize {
|
||||||
|
let size = image.size
|
||||||
|
let maxSize = max(size.width, size.height)
|
||||||
|
let width = size.width * 128 / maxSize
|
||||||
|
let height = size.height * 128 / maxSize
|
||||||
|
return CGSize(width: width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// PhotoPreview.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/15/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PhotoPreview: View {
|
||||||
|
let photoItem: PhotoItem
|
||||||
|
@ObservedObject var tool: Tool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Image(uiImage: photoItem.image)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 100, height: 100)
|
||||||
|
.cornerRadius(5)
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: 5)
|
||||||
|
.stroke(Color.gray, lineWidth: 0.2)
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.background(.regularMaterial)
|
||||||
|
.cornerRadius(5)
|
||||||
|
.overlay(alignment: .topLeading) {
|
||||||
|
Button {
|
||||||
|
tool.unselectPhoto()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.font(.title2)
|
||||||
|
.padding(1)
|
||||||
|
.contentShape(.circle)
|
||||||
|
.background {
|
||||||
|
Circle()
|
||||||
|
.fill(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
.offset(x: -12, y: -12)
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,23 +6,31 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import PhotosUI
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
struct Toolbar: View {
|
struct Toolbar: View {
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
@ObservedObject var tool: Tool
|
||||||
@ObservedObject var canvas: Canvas
|
@ObservedObject var canvas: Canvas
|
||||||
@ObservedObject var history: History
|
@ObservedObject var history: History
|
||||||
|
|
||||||
@State var memo: MemoObject
|
|
||||||
@State var title: String
|
@State var title: String
|
||||||
|
@State var memo: MemoObject
|
||||||
|
@State var opensCamera: Bool = false
|
||||||
|
@State var photosPickerItem: PhotosPickerItem?
|
||||||
|
@State var isCameraAccessDenied: Bool = false
|
||||||
|
|
||||||
@FocusState var textFieldState: Bool
|
@FocusState var textFieldState: Bool
|
||||||
|
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
|
|
||||||
init(size: CGFloat, memo: MemoObject, canvas: Canvas, history: History) {
|
init(size: CGFloat, memo: MemoObject, tool: Tool, canvas: Canvas, history: History) {
|
||||||
self.size = size
|
self.size = size
|
||||||
self.memo = memo
|
self.memo = memo
|
||||||
|
self.tool = tool
|
||||||
self.canvas = canvas
|
self.canvas = canvas
|
||||||
self.history = history
|
self.history = history
|
||||||
self.title = memo.title
|
self.title = memo.title
|
||||||
@@ -30,18 +38,57 @@ struct Toolbar: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 5) {
|
HStack(spacing: 5) {
|
||||||
if !canvas.locksCanvas {
|
HStack(spacing: 5) {
|
||||||
closeButton
|
if !canvas.locksCanvas {
|
||||||
titleField
|
closeButton
|
||||||
|
titleField
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
if !canvas.locksCanvas {
|
elementTool
|
||||||
historyControl
|
HStack(spacing: 5) {
|
||||||
|
if !canvas.locksCanvas {
|
||||||
|
historyControl
|
||||||
|
}
|
||||||
|
lockButton
|
||||||
}
|
}
|
||||||
lockButton
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
}
|
}
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
|
.onChange(of: photosPickerItem) { oldValue, newValue in
|
||||||
|
if newValue != nil {
|
||||||
|
Task {
|
||||||
|
let data = try? await newValue?.loadTransferable(type: Data.self)
|
||||||
|
if let data, let image = UIImage(data: data) {
|
||||||
|
tool.selectPhoto(image, for: canvas.canvasID)
|
||||||
|
}
|
||||||
|
photosPickerItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fullScreenCover(isPresented: $opensCamera) {
|
||||||
|
let image: Binding<UIImage?> = Binding {
|
||||||
|
tool.selectedPhotoItem?.image
|
||||||
|
} set: { image in
|
||||||
|
guard let image else { return }
|
||||||
|
tool.selectPhoto(image, for: canvas.canvasID)
|
||||||
|
}
|
||||||
|
CameraView(image: image, canvas: canvas)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
.alert("Camera Access Denied", isPresented: $isCameraAccessDenied) {
|
||||||
|
Button {
|
||||||
|
if let url = URL(string: UIApplication.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Open Settings")
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) { }
|
||||||
|
} message: {
|
||||||
|
Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var closeButton: some View {
|
var closeButton: some View {
|
||||||
@@ -82,13 +129,76 @@ struct Toolbar: View {
|
|||||||
.transition(.move(edge: .top).combined(with: .blurReplace))
|
.transition(.move(edge: .top).combined(with: .blurReplace))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var elementTool: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
tool.selection = tool.selection == .pen ? .none : .pen
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "pencil")
|
||||||
|
.fontWeight(.heavy)
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.background(tool.selection == .pen ? Color.accentColor : Color.clear)
|
||||||
|
.foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
tool.selection = tool.selection == .photo ? .none : .photo
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "photo")
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.background(tool.selection == .photo ? Color.accentColor : Color.clear)
|
||||||
|
.foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
if tool.selection == .photo {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Button {
|
||||||
|
openCamera()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "camera.fill")
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
|
||||||
|
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
if tool.selection == .photo {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.white.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var historyControl: some View {
|
var historyControl: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Button {
|
Button {
|
||||||
history.historyPublisher.send(.undo)
|
history.historyPublisher.send(.undo)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "arrow.uturn.backward.circle")
|
Image(systemName: "arrow.uturn.backward.circle")
|
||||||
|
|
||||||
.contentShape(.circle)
|
.contentShape(.circle)
|
||||||
}
|
}
|
||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
@@ -111,6 +221,7 @@ struct Toolbar: View {
|
|||||||
|
|
||||||
var lockButton: some View {
|
var lockButton: some View {
|
||||||
Button {
|
Button {
|
||||||
|
#warning("TODO: need to revisit toggale logic")
|
||||||
withAnimation {
|
withAnimation {
|
||||||
canvas.locksCanvas.toggle()
|
canvas.locksCanvas.toggle()
|
||||||
}
|
}
|
||||||
@@ -132,6 +243,26 @@ struct Toolbar: View {
|
|||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openCamera() {
|
||||||
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
|
switch status {
|
||||||
|
case .notDetermined:
|
||||||
|
AVCaptureDevice.requestAccess(for: .video) { status in
|
||||||
|
withAnimation {
|
||||||
|
if status {
|
||||||
|
opensCamera = true
|
||||||
|
} else {
|
||||||
|
isCameraAccessDenied = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .authorized:
|
||||||
|
opensCamera = true
|
||||||
|
default:
|
||||||
|
isCameraAccessDenied = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func closeMemo() {
|
func closeMemo() {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
canvas.state = .closing
|
canvas.state = .closing
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ struct MemosView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let graphicContextObject = GraphicContextObject(\.viewContext)
|
let graphicContextObject = GraphicContextObject(\.viewContext)
|
||||||
graphicContextObject.strokes = []
|
graphicContextObject.elements = []
|
||||||
|
|
||||||
memoObject.canvas = canvasObject
|
memoObject.canvas = canvasObject
|
||||||
memoObject.tool = toolObject
|
memoObject.tool = toolObject
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
import CoreData
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
@objc(CanvasObject)
|
@objc(CanvasObject)
|
||||||
final class CanvasObject: NSManagedObject {
|
final class CanvasObject: NSManagedObject {
|
||||||
@NSManaged var width: CGFloat
|
@NSManaged var width: CGFloat
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// ElementObject.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(ElementObject)
|
||||||
|
final class ElementObject: NSManagedObject {
|
||||||
|
@NSManaged var type: Int16
|
||||||
|
@NSManaged var createdAt: Date?
|
||||||
|
@NSManaged var photo: PhotoObject?
|
||||||
|
@NSManaged var stroke: StrokeObject?
|
||||||
|
@NSManaged var graphicContext: GraphicContextObject?
|
||||||
|
}
|
||||||
@@ -11,5 +11,5 @@ import Foundation
|
|||||||
@objc(GraphicContextObject)
|
@objc(GraphicContextObject)
|
||||||
final class GraphicContextObject: NSManagedObject {
|
final class GraphicContextObject: NSManagedObject {
|
||||||
@NSManaged var canvas: CanvasObject?
|
@NSManaged var canvas: CanvasObject?
|
||||||
@NSManaged var strokes: NSMutableOrderedSet
|
@NSManaged var elements: NSMutableOrderedSet
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// PhotoObject.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(PhotoObject)
|
||||||
|
class PhotoObject: NSManagedObject {
|
||||||
|
@NSManaged var width: CGFloat
|
||||||
|
@NSManaged var originY: CGFloat
|
||||||
|
@NSManaged var originX: CGFloat
|
||||||
|
@NSManaged var height: CGFloat
|
||||||
|
@NSManaged var bounds: [CGFloat]
|
||||||
|
@NSManaged var createdAt: Date?
|
||||||
|
@NSManaged var imageURL: URL?
|
||||||
|
@NSManaged var bookmark: Data?
|
||||||
|
@NSManaged var element: ElementObject?
|
||||||
|
}
|
||||||
@@ -17,5 +17,5 @@ final class StrokeObject: NSManagedObject {
|
|||||||
@NSManaged var thickness: CGFloat
|
@NSManaged var thickness: CGFloat
|
||||||
@NSManaged var quads: NSMutableOrderedSet
|
@NSManaged var quads: NSMutableOrderedSet
|
||||||
@NSManaged var erasers: NSMutableSet
|
@NSManaged var erasers: NSMutableSet
|
||||||
@NSManaged var graphicContext: GraphicContextObject?
|
@NSManaged var element: ElementObject?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,13 @@
|
|||||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContextObject" inverseName="canvas" inverseEntity="GraphicContextObject"/>
|
<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"/>
|
<relationship name="memo" maxCount="1" deletionRule="Deny" destinationEntity="MemoObject" inverseName="canvas" inverseEntity="MemoObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="ElementObject" representedClassName="ElementObject" syncable="YES">
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="elements" inverseEntity="GraphicContextObject"/>
|
||||||
|
<relationship name="photo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PhotoObject" inverseName="element" inverseEntity="PhotoObject"/>
|
||||||
|
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="element" inverseEntity="StrokeObject"/>
|
||||||
|
</entity>
|
||||||
<entity name="EraserObject" representedClassName="EraserObject" syncable="YES">
|
<entity name="EraserObject" representedClassName="EraserObject" syncable="YES">
|
||||||
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
@@ -17,7 +24,7 @@
|
|||||||
</entity>
|
</entity>
|
||||||
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
|
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
|
||||||
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CanvasObject" inverseName="graphicContext" inverseEntity="CanvasObject"/>
|
<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"/>
|
<relationship name="elements" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="ElementObject" inverseName="graphicContext" inverseEntity="ElementObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
|
<entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
@@ -34,6 +41,17 @@
|
|||||||
<attribute name="thickness" attributeType="Double" defaultValueString="0.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"/>
|
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="PhotoObject" representedClassName="PhotoObject" syncable="YES">
|
||||||
|
<attribute name="bookmark" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||||
|
<attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="originY" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="width" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="element" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ElementObject" inverseName="photo" inverseEntity="ElementObject"/>
|
||||||
|
</entity>
|
||||||
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
|
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
|
||||||
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
<attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
<attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
@@ -50,8 +68,8 @@
|
|||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="style" 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"/>
|
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="element" maxCount="1" deletionRule="Nullify" destinationEntity="ElementObject" inverseName="stroke" inverseEntity="ElementObject"/>
|
||||||
<relationship name="erasers" toMany="YES" deletionRule="Nullify" destinationEntity="EraserObject" inverseName="strokes" inverseEntity="EraserObject"/>
|
<relationship name="erasers" toMany="YES" deletionRule="Nullify" destinationEntity="EraserObject" inverseName="strokes" inverseEntity="EraserObject"/>
|
||||||
<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"/>
|
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="stroke" inverseEntity="QuadObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="ToolObject" representedClassName="ToolObject" syncable="YES">
|
<entity name="ToolObject" representedClassName="ToolObject" syncable="YES">
|
||||||
|
|||||||
Reference in New Issue
Block a user