Merge pull request #27 from dscyrescotti/feature/pen-tool

Redesign pen tool of memo canvas view
This commit is contained in:
Aye Chan
2024-05-19 17:08:36 +08:00
committed by GitHub
51 changed files with 1138 additions and 189 deletions

View File

@@ -7,12 +7,18 @@
objects = {
/* Begin PBXBuildFile section */
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14202BF79C73009BFE5F /* ToolObject.swift */; };
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; };
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; };
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; };
EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; };
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; };
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; };
EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; };
EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; };
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; };
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; };
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; };
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
@@ -68,23 +74,30 @@
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; };
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; };
ECA739082BE623F300A4542E /* PenDockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDockView.swift */; };
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; };
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; };
ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15212BEF21F500455818 /* CanvasObject.swift */; };
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15232BEF223300455818 /* GraphicContextObject.swift */; };
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15252BEF224900455818 /* StrokeObject.swift */; };
ECFA15282BEF225000455818 /* QuadObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15272BEF225000455818 /* QuadObject.swift */; };
ECFC51272BF8885700D0D051 /* ColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFC51262BF8885700D0D051 /* ColorPicker.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
EC0D14202BF79C73009BFE5F /* ToolObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolObject.swift; sourceTree = "<group>"; };
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = "<group>"; };
EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = "<group>"; };
EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = "<group>"; };
EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = "<group>"; };
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = "<group>"; };
EC3565592BF060D900A4E0BF /* Quad.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Quad.metal; sourceTree = "<group>"; };
EC35655B2BF0712A00A4E0BF /* Float++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = "<group>"; };
EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = "<group>"; };
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = "<group>"; };
EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; };
EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = "<group>"; };
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -141,14 +154,14 @@
ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; };
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; };
ECA739072BE623F300A4542E /* PenDockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDockView.swift; sourceTree = "<group>"; };
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = "<group>"; };
ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = "<group>"; };
ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = "<group>"; };
ECFA15232BEF223300455818 /* GraphicContextObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContextObject.swift; sourceTree = "<group>"; };
ECFA15252BEF224900455818 /* StrokeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeObject.swift; sourceTree = "<group>"; };
ECFA15272BEF225000455818 /* QuadObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadObject.swift; sourceTree = "<group>"; };
ECFC51262BF8885700D0D051 /* ColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -178,6 +191,73 @@
path = ViewController;
sourceTree = "<group>";
};
EC1B783A2BF9C68C005A34E2 /* Views */ = {
isa = PBXGroup;
children = (
ECFC51252BF8885000D0D051 /* ColorPicker */,
);
path = Views;
sourceTree = "<group>";
};
EC5050042BF65CBC00B4D86E /* Core */ = {
isa = PBXGroup;
children = (
ECA738BB2BE60E0300A4542E /* Tool.swift */,
);
path = Core;
sourceTree = "<group>";
};
EC5050052BF65CCD00B4D86E /* PenDock */ = {
isa = PBXGroup;
children = (
ECA739072BE623F300A4542E /* PenDockView.swift */,
EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */,
);
path = PenDock;
sourceTree = "<group>";
};
EC5050082BF65D0500B4D86E /* Memo */ = {
isa = PBXGroup;
children = (
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */,
);
path = Memo;
sourceTree = "<group>";
};
EC5050092BF65D5700B4D86E /* Canvas */ = {
isa = PBXGroup;
children = (
ECA738B22BE60D9E00A4542E /* CanvasView.swift */,
);
path = Canvas;
sourceTree = "<group>";
};
EC50500A2BF6672000B4D86E /* Components */ = {
isa = PBXGroup;
children = (
EC1B783A2BF9C68C005A34E2 /* Views */,
EC50500B2BF6673300B4D86E /* ViewModifiers */,
);
path = Components;
sourceTree = "<group>";
};
EC50500B2BF6673300B4D86E /* ViewModifiers */ = {
isa = PBXGroup;
children = (
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */,
EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */,
);
path = ViewModifiers;
sourceTree = "<group>";
};
EC5050102BF670EE00B4D86E /* Config */ = {
isa = PBXGroup;
children = (
EC50500E2BF670EA00B4D86E /* Info.plist */,
);
path = Config;
sourceTree = "<group>";
};
EC7F6BDF2BE5E6E300A34A7B = {
isa = PBXGroup;
children = (
@@ -199,6 +279,8 @@
children = (
ECA738762BE5EE4E00A4542E /* App */,
ECA7387E2BE5FE4200A4542E /* Canvas */,
EC5050102BF670EE00B4D86E /* Config */,
EC50500A2BF6672000B4D86E /* Components */,
ECA738A12BE601F700A4542E /* Extensions */,
ECA738772BE5EEE800A4542E /* Features */,
ECA738FA2BE61B1700A4542E /* Persistence */,
@@ -244,8 +326,8 @@
ECA7387B2BE5EF3500A4542E /* Memo */ = {
isa = PBXGroup;
children = (
ECA7387C2BE5EF4B00A4542E /* MemoView.swift */,
ECA739072BE623F300A4542E /* PenToolView.swift */,
EC5050082BF65D0500B4D86E /* Memo */,
EC5050052BF65CCD00B4D86E /* PenDock */,
);
path = Memo;
sourceTree = "<group>";
@@ -371,8 +453,8 @@
ECA738AB2BE60CB500A4542E /* View */ = {
isa = PBXGroup;
children = (
EC5050092BF65D5700B4D86E /* Canvas */,
ECA738AE2BE60CEC00A4542E /* Bridge */,
ECA738B22BE60D9E00A4542E /* CanvasView.swift */,
);
path = View;
sourceTree = "<group>";
@@ -389,8 +471,8 @@
ECA738B12BE60D8800A4542E /* Tool */ = {
isa = PBXGroup;
children = (
EC5050042BF65CBC00B4D86E /* Core */,
ECA738BD2BE60E2800A4542E /* Pen */,
ECA738BB2BE60E0300A4542E /* Tool.swift */,
);
path = Tool;
sourceTree = "<group>";
@@ -492,7 +574,7 @@
ECA738FE2BE61D5700A4542E /* Models */ = {
isa = PBXGroup;
children = (
ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */,
EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */,
);
path = Models;
sourceTree = "<group>";
@@ -522,10 +604,20 @@
ECFA15232BEF223300455818 /* GraphicContextObject.swift */,
ECFA15252BEF224900455818 /* StrokeObject.swift */,
ECFA15272BEF225000455818 /* QuadObject.swift */,
EC0D14202BF79C73009BFE5F /* ToolObject.swift */,
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
);
path = Objects;
sourceTree = "<group>";
};
ECFC51252BF8885000D0D051 /* ColorPicker */ = {
isa = PBXGroup;
children = (
ECFC51262BF8885700D0D051 /* ColorPicker.swift */,
);
path = ColorPicker;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -627,13 +719,15 @@
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */,
ECA739082BE623F300A4542E /* PenDockView.swift in Sources */,
ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */,
ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */,
ECA7388C2BE6009600A4542E /* Textures.swift in Sources */,
ECFC51272BF8885700D0D051 /* ColorPicker.swift in Sources */,
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */,
ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */,
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */,
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */,
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */,
EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */,
@@ -646,6 +740,8 @@
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,
@@ -655,11 +751,13 @@
ECA738B62BE60DCD00A4542E /* History.swift in Sources */,
ECA738D22BE60F7B00A4542E /* Stroke.swift in Sources */,
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */,
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */,
ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */,
ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */,
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
@@ -798,7 +896,8 @@
DEVELOPMENT_ASSET_PATHS = "\"Memola/Preview Content\"";
DEVELOPMENT_TEAM = 9TYSSFKV5U;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Memola/Config/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -830,7 +929,8 @@
DEVELOPMENT_ASSET_PATHS = "\"Memola/Preview Content\"";
DEVELOPMENT_TEAM = 9TYSSFKV5U;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Memola/Config/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -876,13 +976,14 @@
/* End XCConfigurationList section */
/* Begin XCVersionGroup section */
ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */ = {
EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */,
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */,
);
currentVersion = ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */;
path = MemolaModel.xcdatamodeld;
currentVersion = EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */;
name = MemolaModel.xcdatamodeld;
path = /Users/dscyrescotti/Documents/Projects/Memola/Memola/Resources/Models/MemolaModel.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};

View File

@@ -117,7 +117,7 @@ extension GraphicContext {
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
let stroke = Stroke(
bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
color: pen.color,
color: pen.rgba,
style: pen.strokeStyle.rawValue,
createdAt: .now,
thickness: pen.thickness

View File

@@ -14,12 +14,14 @@ class EraserRenderPass: RenderPass {
var descriptor: MTLRenderPassDescriptor?
var eraserPipelineState: MTLRenderPipelineState?
var quadPipelineState: MTLComputePipelineState?
var stroke: Stroke?
weak var graphicTexture: MTLTexture?
init(renderer: Renderer) {
eraserPipelineState = PipelineStates.createEraserPipelineState(from: renderer)
quadPipelineState = PipelineStates.createQuadPipelineState(from: renderer)
}
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
@@ -27,6 +29,8 @@ class EraserRenderPass: RenderPass {
func draw(on canvas: Canvas, with renderer: Renderer) {
guard let descriptor else { return }
generateVertexBuffer(on: canvas, with: renderer)
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
commandBuffer.label = "Eraser Command Buffer"
@@ -42,4 +46,30 @@ class EraserRenderPass: RenderPass {
renderEncoder.endEncoding()
commandBuffer.commit()
}
private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) {
guard let stroke, !stroke.quads.isEmpty, let quadPipelineState else { return }
guard let quadCommandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
guard let computeEncoder = quadCommandBuffer.makeComputeCommandEncoder() else { return }
computeEncoder.label = "Quad Render Pass"
let quadCount = stroke.quads.endIndex
var quads = stroke.quads
let quadBuffer = renderer.device.makeBuffer(bytes: &quads, length: MemoryLayout<Quad>.stride * quadCount, options: [])
let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout<QuadVertex>.stride * quadCount * 6, options: [])
computeEncoder.setComputePipelineState(quadPipelineState)
computeEncoder.setBuffer(quadBuffer, offset: 0, index: 0)
computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 1)
stroke.vertexBuffer = vertexBuffer
let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1)
let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1)
computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
computeEncoder.endEncoding()
quadCommandBuffer.commit()
quadCommandBuffer.waitUntilCompleted()
}
}

View File

@@ -0,0 +1,109 @@
//
// Tool.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import Combine
import SwiftUI
import CoreData
import Foundation
public class Tool: NSObject, ObservableObject {
let object: ToolObject
@Published var pens: [Pen] = []
@Published var selectedPen: Pen?
@Published var draggedPen: Pen?
let scrollPublisher = PassthroughSubject<String, Never>()
var markers: [Pen] {
pens.filter { $0.strokeStyle == .marker }
}
init(object: ToolObject) {
self.object = object
}
func load() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
pens = object.pens.sortedArray(using: [NSSortDescriptor(key: "orderIndex", ascending: true)]).compactMap {
guard let pen = $0 as? PenObject else { return nil }
return Pen(object: pen)
}
if let selectedPen = pens.first(where: { $0.isSelected }) {
selectPen(selectedPen)
scrollPublisher.send(selectedPen.id)
}
}
}
func selectPen(_ pen: Pen) {
if let selectedPen {
unselectPen(selectedPen)
}
withAnimation {
selectedPen = pen
}
selectedPen?.isSelected = true
withPersistence(\.viewContext) { context in
try context.saveIfNeeded()
}
}
func unselectPen(_ pen: Pen) {
pen.isSelected = false
withAnimation {
selectedPen = nil
}
withPersistence(\.viewContext) { context in
try context.saveIfNeeded()
}
}
func duplicatePen(_ pen: Pen, of originalPen: Pen) {
guard let index = pens.firstIndex(where: { originalPen === $0 }) else { return }
withAnimation {
pens.insert(pen, at: index + 1)
}
selectPen(pen)
withPersistence(\.viewContext) { [pens] context in
for (index, pen) in pens.enumerated() {
pen.object?.orderIndex = Int16(index)
}
try context.saveIfNeeded()
}
}
func addPen(_ pen: Pen) {
withAnimation {
pens.append(pen)
}
selectPen(pen)
if let _pen = pen.object {
object.pens.add(_pen)
}
scrollPublisher.send(pen.id)
withPersistence(\.viewContext) { context in
try context.saveIfNeeded()
}
}
func removePen(_ pen: Pen) {
guard let index = pens.firstIndex(where: { $0 === pen }) else { return }
let deletedPen = withAnimation {
pens.remove(at: index)
}
unselectPen(deletedPen)
if let _pen = deletedPen.object {
_pen.tool = nil
object.pens.remove(_pen)
withPersistence(\.viewContext) { context in
context.delete(_pen)
try context.saveIfNeeded()
}
}
}
}

View File

@@ -7,32 +7,50 @@
import SwiftUI
import Foundation
import UniformTypeIdentifiers
class Pen: NSObject, ObservableObject, Identifiable {
@Published var style: any PenStyle
@Published var color: [CGFloat]
@Published var thickness: CGFloat
var object: PenObject?
init(style: any PenStyle, color: [CGFloat], thickness: CGFloat) {
self.style = style
self.color = color
self.thickness = thickness
let id: String
@Published var style: any PenStyle {
didSet {
object?.style = strokeStyle.rawValue
}
}
@Published var rgba: [CGFloat] {
didSet {
object?.color = rgba
}
}
@Published var thickness: CGFloat {
didSet {
object?.thickness = thickness
}
}
@Published var isSelected: Bool {
didSet {
object?.isSelected = isSelected
}
}
var color: Color {
get { Color.rgba(from: rgba) }
set {
rgba = newValue.components
}
}
init(object: PenObject) {
self.object = object
self.id = object.objectID.uriRepresentation().absoluteString
self.style = (Stroke.Style(rawValue: object.style) ?? .marker).anyPenStyle
self.rgba = object.color
self.thickness = object.thickness
self.isSelected = object.isSelected
super.init()
}
var strokeStyle: Stroke.Style {
switch style {
case is MarkerPenStyle:
return .marker
case is EraserPenStyle:
return .eraser
default:
return .marker
}
}
}
extension Pen {
convenience init(for style: any PenStyle) {
self.init(style: style, color: style.color, thickness: style.thinkness.min)
style.strokeStyle
}
}

View File

@@ -11,7 +11,8 @@ import Foundation
protocol PenStyle {
var icon: (base: String, tip: String?) { get }
var textureName: String { get }
var thinkness: (min: CGFloat, max: CGFloat) { get }
var thickness: (min: CGFloat, max: CGFloat) { get }
var thicknessSteps: [CGFloat] { get }
var color: [CGFloat] { get }
var stepRate: CGFloat { get }
var generator: any StrokeGenerator { get }
@@ -22,4 +23,15 @@ extension PenStyle {
func loadTexture(on device: MTLDevice) -> MTLTexture? {
Textures.createPenTexture(with: textureName, on: device)
}
var strokeStyle: Stroke.Style {
switch self {
case is MarkerPenStyle:
return .marker
case is EraserPenStyle:
return .eraser
default:
return .marker
}
}
}

View File

@@ -12,7 +12,9 @@ struct EraserPenStyle: PenStyle {
var textureName: String = "point-texture"
var thinkness: (min: CGFloat, max: CGFloat) = (0.5, 120)
var thickness: (min: CGFloat, max: CGFloat) = (0.5, 40)
var thicknessSteps: [CGFloat] = [0.5, 1, 2, 5, 7.5, 10, 15, 20, 25, 30, 35, 40]
var color: [CGFloat] = [1, 1, 1, 0]

View File

@@ -12,7 +12,9 @@ struct MarkerPenStyle: PenStyle {
var textureName: String = "point-texture"
var thinkness: (min: CGFloat, max: CGFloat) = (0.5, 120)
var thickness: (min: CGFloat, max: CGFloat) = (0.5, 40)
var thicknessSteps: [CGFloat] = [0.5, 1, 2, 5, 7.5, 10, 15, 20, 25, 30, 35, 40]
var color: [CGFloat] = [1, 0.38, 0.38, 1]

View File

@@ -1,27 +0,0 @@
//
// Tool.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
import Foundation
class Tool: NSObject, ObservableObject {
@Published var pens: [Pen]
@Published var selectedPen: Pen?
override init() {
pens = [
Pen(for: .marker),
Pen(for: .eraser)
]
super.init()
selectedPen = pens.first
}
func changePen(_ pen: Pen) {
selectedPen = pen
}
}

View File

@@ -183,6 +183,7 @@ extension CanvasViewController {
extension CanvasViewController {
func loadMemo() {
tool.load()
canvas.load()
}

View File

@@ -0,0 +1,30 @@
//
// ContextMenuViewModifier.swift
// Memola
//
// Created by Dscyre Scotti on 5/17/24.
//
import SwiftUI
import Foundation
struct ContextMenuViewModifier<MenuContent: View, Preview: View>: ViewModifier {
let condition: Bool
let menuItems: () -> MenuContent
let preview: () -> Preview
@ViewBuilder
func body(content: Content) -> some View {
if condition {
content.contextMenu(menuItems: menuItems, preview: preview)
} else {
content
}
}
}
public extension View {
func contextMenu<MenuContent: View, Preview: View>(if condition: Bool, @ViewBuilder menuItems: @escaping () -> MenuContent, @ViewBuilder preview: @escaping () -> Preview) -> some View {
modifier(ContextMenuViewModifier(condition: condition, menuItems: menuItems, preview: preview))
}
}

View File

@@ -0,0 +1,30 @@
//
// OnDragViewModifier.swift
// Memola
//
// Created by Dscyre Scotti on 5/16/24.
//
import SwiftUI
import Foundation
struct OnDragViewModifier<Preview: View>: ViewModifier {
let condition: Bool
let data: () -> NSItemProvider
let preview: () -> Preview
@ViewBuilder
func body(content: Content) -> some View {
if condition {
content.onDrag(data, preview: preview)
} else {
content
}
}
}
public extension View {
func onDrag<Preview: View>(if condition: Bool, data: @escaping () -> NSItemProvider, @ViewBuilder preview: @escaping () -> Preview) -> some View {
modifier(OnDragViewModifier(condition: condition, data: data, preview: preview))
}
}

View File

@@ -0,0 +1,196 @@
//
// ColorPicker.swift
// Memola
//
// Created by Dscyre Scotti on 5/18/24.
//
import SwiftUI
import Foundation
struct ColorPicker: View {
@State var hue: Double = 1
@State var saturation: Double = 0
@State var brightness: Double = 1
@State var alpha: Double = 1
@Binding var color: Color
let size: CGFloat = 20
var body: some View {
VStack(spacing: 10) {
colorPicker
.frame(width: 200, height: 200)
HStack(spacing: 10) {
hueSlider
alphaSlider
}
}
.padding(10)
.background {
Rectangle()
.fill(.regularMaterial)
.ignoresSafeArea(.all)
}
.onAppear {
let hsba = color.hsba
hue = hsba.hue
saturation = hsba.saturation
brightness = hsba.brightness
alpha = hsba.alpha * 1.43 - 0.43
}
}
@ViewBuilder
var colorPicker: some View {
GeometryReader { proxy in
ZStack {
Color(hue: hue, saturation: 1, brightness: 1)
LinearGradient(
colors: [.white, .clear],
startPoint: .leading,
endPoint: .trailing
)
LinearGradient(
colors: [.black, .clear],
startPoint: .bottom,
endPoint: .top
)
}
.cornerRadius(5)
.drawingGroup()
.overlay {
RoundedRectangle(cornerRadius: 5)
.stroke(Color.gray, lineWidth: 0.2)
}
.overlay(alignment: .bottomLeading) {
Color(hue: hue, saturation: saturation, brightness: brightness)
.frame(width: size, height: size)
.clipShape(Circle())
.padding(1)
.overlay {
Circle()
.strokeBorder(.white, lineWidth: 2)
}
.overlay {
Circle()
.stroke(Color.gray, lineWidth: 0.2)
}
.offset(x: -size + 5, y: size - 5)
.offset(x: max(proxy.size.width * saturation, size - 10), y: min(proxy.size.height * -brightness, -size + 10))
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
saturation = min(1, max(value.location.x / proxy.size.width, 0))
brightness = 1 - min(1, max(value.location.y / proxy.size.height, 0))
updateColor()
}
)
}
}
@ViewBuilder
var hueSlider: some View {
GeometryReader { proxy in
ZStack(alignment: .leading) {
LinearGradient(
colors: (0...10).map { Color(hue: Double($0) * 0.1, saturation: 1, brightness: 1) },
startPoint: .leading,
endPoint: .trailing
)
Color(hue: hue, saturation: 1, brightness: 1)
.frame(width: size, height: size)
.clipShape(Circle())
.padding(1)
.overlay {
Circle()
.strokeBorder(.white, lineWidth: 2)
}
.overlay {
Circle()
.stroke(Color.gray, lineWidth: 0.2)
}
.offset(x: -size)
.offset(x: max(size, proxy.size.width * hue - 2))
}
.frame(width: proxy.size.width, height: proxy.size.height)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
hue = min(1, max(value.location.x / proxy.size.width, 0))
updateColor()
}
.onEnded { value in
hue = min(1, max(value.location.x / proxy.size.width, 0))
updateColor()
}
)
.clipShape(Capsule())
.overlay {
Capsule()
.stroke(Color.gray, lineWidth: 0.2)
}
.frame(height: proxy.size.height)
}
.frame(height: size)
}
@ViewBuilder
var alphaSlider: some View {
GeometryReader { proxy in
let color = Color(hue: hue, saturation: saturation, brightness: brightness)
ZStack(alignment: .leading) {
LinearGradient(
colors: (3...10).map { color.opacity(0.1 * CGFloat($0)) },
startPoint: .leading,
endPoint: .trailing
)
.background {
Image("transparent-grid-rect")
.resizable()
.aspectRatio(contentMode: .fill)
.background(.white)
}
color
.frame(width: size, height: size)
.clipShape(Circle())
.padding(1)
.overlay {
Circle()
.strokeBorder(.white, lineWidth: 2)
}
.overlay {
Circle()
.stroke(Color.gray, lineWidth: 0.2)
}
.offset(x: -size)
.offset(x: max(size, proxy.size.width * alpha - 2))
}
.frame(width: proxy.size.width, height: proxy.size.height)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
alpha = min(1, max(value.location.x / proxy.size.width, 0))
updateColor()
}
.onEnded { value in
alpha = min(1, max(value.location.x / proxy.size.width, 0))
updateColor()
}
)
.clipShape(Capsule())
.overlay {
Capsule()
.stroke(Color.gray, lineWidth: 0.2)
}
.frame(height: proxy.size.height)
}
.frame(height: size)
}
func updateColor() {
color = Color(hue: hue, saturation: saturation, brightness: brightness).opacity(0.7 * alpha + 0.3)
}
}

51
Memola/Config/Info.plist Normal file
View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>UILaunchScreen</key>
<dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>UISupportedInterfaceOrientations~iphone</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
</dict>
</plist>

View File

@@ -18,6 +18,18 @@ extension Color {
}
}
extension Color {
var hsba: (hue: Double, saturation: Double, brightness: Double, alpha: Double) {
let uiColor = UIColor(self)
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
uiColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return (hue, saturation, brightness, alpha)
}
}
extension UIColor {
var components: [CGFloat] {
let uiColor: UIColor = self

View File

@@ -10,9 +10,8 @@ import CoreData
struct MemoView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.managedObjectContext) var managedObjectContext
@StateObject var tool = Tool()
@StateObject var tool: Tool
@StateObject var canvas: Canvas
@StateObject var history = History()
@@ -20,25 +19,28 @@ struct MemoView: View {
init(memo: MemoObject) {
self.memo = memo
self._tool = StateObject(wrappedValue: Tool(object: memo.tool))
self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID))
}
var body: some View {
CanvasView()
.ignoresSafeArea()
.overlay(alignment: .bottomTrailing) {
PenToolView()
.padding()
}
.overlay(alignment: .topTrailing) {
historyTool
.padding()
}
.overlay(alignment: .trailing) {
PenDockView()
.frame(maxHeight: .infinity)
.padding()
}
.overlay(alignment: .topLeading) {
Button {
closeMemo()
} label: {
Image(systemName: "xmark")
.contentShape(.circle)
.padding(15)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
@@ -68,6 +70,7 @@ struct MemoView: View {
history.historyPublisher.send(.undo)
} label: {
Image(systemName: "arrow.uturn.backward.circle")
.contentShape(.circle)
}
.hoverEffect(.lift)
.disabled(history.undoDisabled)
@@ -75,6 +78,7 @@ struct MemoView: View {
history.historyPublisher.send(.redo)
} label: {
Image(systemName: "arrow.uturn.forward.circle")
.contentShape(.circle)
}
.hoverEffect(.lift)
.disabled(history.redoDisabled)
@@ -95,13 +99,8 @@ struct MemoView: View {
}
func closeMemo() {
history.resetRedo()
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
withPersistenceSync(\.viewContext) { context in
try context.saveIfNeeded()
}
dismiss()
}

View File

@@ -0,0 +1,297 @@
//
// PenDockView.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
struct PenDockView: View {
@EnvironmentObject var tool: Tool
let width: CGFloat = 90
let height: CGFloat = 30
let factor: CGFloat = 0.95
@State var refreshScrollId: UUID = UUID()
@State var opensColorPicker: Bool = false
var body: some View {
VStack(alignment: .trailing) {
if let pen = tool.selectedPen {
VStack(spacing: 5) {
penColorView(pen)
penThicknessView(pen)
}
.padding(10)
.frame(width: width * factor - 18)
.background {
RoundedRectangle(cornerRadius: 20)
.fill(.regularMaterial)
}
.transition(.move(edge: .trailing).combined(with: .opacity))
} else {
Color.clear
.frame(width: width * factor - 18, height: 50)
}
penScrollView
}
.fixedSize()
}
var penScrollView: some View {
ScrollViewReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0) {
ForEach(tool.pens) { pen in
penView(pen)
.id(pen.id)
.scrollTransition { content, phase in
content
.scaleEffect(phase.isIdentity ? 1 : 0.04, anchor: .trailing)
}
}
}
.padding(.vertical, 10)
.padding(.leading, 40)
.id(refreshScrollId)
}
.onReceive(tool.scrollPublisher) { id in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation {
proxy.scrollTo(id)
}
}
}
}
.frame(maxHeight:( (height * factor + 10) * 6) + 20)
.fixedSize()
.background(alignment: .trailing) {
RoundedRectangle(cornerRadius: 20)
.fill(.regularMaterial)
.frame(width: width * factor - 18)
}
.clipShape(.rect(cornerRadii: .init(bottomTrailing: 20, topTrailing: 20)))
.overlay(alignment: .bottomLeading) {
newPenButton
.offset(x: 60, y: 10)
}
}
func penView(_ pen: Pen) -> some View {
ZStack {
penShadowView(pen)
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.rgba))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: width * factor, height: height * factor)
.padding(.vertical, 5)
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
.onTapGesture {
if tool.selectedPen === pen {
tool.unselectPen(pen)
} else {
tool.selectPen(pen)
}
}
.padding(.leading, 10)
.contextMenu(if: pen.strokeStyle != .eraser) {
ControlGroup {
Button {
tool.selectPen(pen)
} label: {
Label(
title: { Text("Select") },
icon: { Image(systemName: "pencil.tip.crop.circle") }
)
}
Button {
let originalPen = pen
let pen = PenObject.createObject(\.viewContext, penStyle: originalPen.style)
pen.color = originalPen.rgba
pen.thickness = originalPen.thickness
pen.isSelected = true
pen.tool = tool.object
let _pen = Pen(object: pen)
tool.duplicatePen(_pen, of: originalPen)
} label: {
Label(
title: { Text("Duplicate") },
icon: { Image(systemName: "plus.square.on.square") }
)
}
Button(role: .destructive) {
tool.removePen(pen)
} label: {
Label(
title: { Text("Remove") },
icon: { Image(systemName: "trash") }
)
}
.disabled(tool.markers.count <= 1)
}
.controlGroupStyle(.menu)
} preview: {
penPreviewView(pen)
.drawingGroup()
.contentShape(.contextMenuPreview, .rect(cornerRadius: 10))
}
.onDrag(if: pen.strokeStyle != .eraser) {
tool.draggedPen = pen
return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider()
} preview: {
penPreviewView(pen)
.contentShape(.dragPreview, .rect(cornerRadius: 10))
}
.onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool, action: { refreshScrollId = UUID() }))
.offset(x: tool.selectedPen === pen ? 0 : 25)
}
func penColorView(_ pen: Pen) -> some View {
Button {
opensColorPicker = true
} label: {
let hsba = pen.color.hsba
let baseColor = Color(hue: hsba.hue, saturation: hsba.saturation, brightness: hsba.brightness)
GeometryReader { proxy in
HStack(spacing: 0) {
baseColor
.frame(width: proxy.size.width / 2)
Image("transparent-grid-square")
.resizable()
.scaleEffect(3)
.aspectRatio(contentMode: .fill)
.opacity(1 - hsba.alpha)
.frame(width: proxy.size.width / 2)
.clipped()
}
}
.background(baseColor)
.clipShape(RoundedRectangle(cornerRadius: 10))
.frame(height: 28)
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 0.4)
}
.padding(0.2)
.padding(.top, 4)
.drawingGroup()
}
.hoverEffect(.lift)
.popover(isPresented: $opensColorPicker) {
let color = Binding(
get: { pen.color },
set: {
pen.color = $0
tool.objectWillChange.send()
}
)
ColorPicker(color: color)
.presentationCompactAdaptation(.popover)
}
}
@ViewBuilder
func penThicknessView(_ pen: Pen) -> some View {
let minimum: CGFloat = pen.style.thickness.min
let maximum: CGFloat = pen.style.thickness.max
let start: CGFloat = 5
let end: CGFloat = 15
let selection = Binding(
get: { pen.thickness },
set: {
pen.thickness = $0
tool.objectWillChange.send()
}
)
Picker("", selection: selection) {
ForEach(pen.style.thicknessSteps, id: \.self) { step in
let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (1 / step)
if pen.thickness == step {
Circle()
.fill(.black)
.frame(width: size, height: size)
} else {
Circle()
.stroke(Color.black, lineWidth: 1)
.frame(width: size, height: size)
}
}
}
.pickerStyle(.wheel)
.frame(width: width * factor - 18, height: 40)
}
var newPenButton: some View {
Button {
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
if let color = (tool.selectedPen ?? tool.pens.last)?.rgba {
pen.color = color
}
pen.isSelected = true
pen.tool = tool.object
pen.orderIndex = Int16(tool.pens.count)
let _pen = Pen(object: pen)
tool.addPen(_pen)
} label: {
Image(systemName: "plus.circle.fill")
.font(.title2)
.padding(1)
.contentShape(.circle)
.background {
Circle()
.fill(.white)
}
}
.foregroundStyle(.green)
.hoverEffect(.lift)
}
func penPreviewView(_ pen: Pen) -> some View {
ZStack {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.rgba))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: width * factor, height: height * factor)
.padding(.vertical, 5)
.padding(.leading, 10)
}
func penShadowView(_ pen: Pen) -> some View {
ZStack {
Group {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
}
Image(pen.style.icon.base)
.resizable()
.renderingMode(.template)
}
.drawingGroup()
.foregroundStyle(.black.opacity(0.2))
.blur(radius: 3)
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color(red: pen.rgba[0], green: pen.rgba[1], blue: pen.rgba[2]))
.blur(radius: 0.5)
}
}
}
}

View File

@@ -0,0 +1,40 @@
//
// PenDropDelegate.swift
// Memola
//
// Created by Dscyre Scotti on 5/16/24.
//
import SwiftUI
import Foundation
struct PenDropDelegate: DropDelegate {
let id: String
@ObservedObject var tool: Tool
let action: () -> Void
func performDrop(info: DropInfo) -> Bool {
tool.draggedPen = nil
action()
return true
}
func dropEntered(info: DropInfo) {
guard let draggedPen = tool.draggedPen else { return }
if draggedPen.id != id {
let fromIndex = tool.pens.firstIndex(where: { $0.id == draggedPen.id })!
let toIndex = tool.pens.firstIndex(where: { $0.id == id })!
guard tool.pens[toIndex].strokeStyle != .eraser else { return }
withAnimation {
tool.pens.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
tool.objectWillChange.send()
}
withPersistence(\.viewContext) { context in
for (index, pen) in tool.pens.enumerated() {
pen.object?.orderIndex = Int16(index)
}
try context.saveIfNeeded()
}
}
}
}

View File

@@ -1,82 +0,0 @@
//
// PenToolView.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
struct PenToolView: View {
@EnvironmentObject var tool: Tool
var body: some View {
VStack {
if let pen = tool.selectedPen {
let thicknessBounds = pen.style.thinkness
let thickness = Binding {
max(pen.thickness, pen.style.thinkness.min)
} set: { newValue in
tool.selectedPen?.thickness = newValue
}
let color = Binding {
Color.rgba(from: pen.color)
} set: { newValue in
tool.selectedPen?.color = newValue.components
tool.objectWillChange.send()
}
HStack {
ColorPicker("", selection: color)
.frame(width: 40, height: 40)
Slider(value: thickness, in: thicknessBounds.min...thicknessBounds.max)
.frame(width: 180, height: 40)
}
}
HStack {
ForEach(tool.pens) { pen in
penView(pen)
.overlay(alignment: .bottom) {
if tool.selectedPen === pen {
Circle()
.frame(width: 5, height: 5)
.offset(y: 7.5)
.foregroundStyle(Color.rgba(from: pen.color))
}
}
}
}
.padding(15)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
@ViewBuilder
func penView(_ pen: Pen) -> some View {
Button {
if tool.selectedPen === pen {
tool.selectedPen = nil
} else {
tool.changePen(pen)
}
} label: {
ZStack {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.color))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: 30, height: 65)
.drawingGroup()
.hoverEffect(.lift)
}
.buttonStyle(.plain)
}
}

View File

@@ -62,28 +62,45 @@ struct MemosView: View {
}
func createMemo(title: String) {
do {
let memoObject = MemoObject(context: managedObjectContext)
memoObject.title = title
memoObject.createdAt = .now
memoObject.updatedAt = .now
let memoObject = MemoObject(\.viewContext)
memoObject.title = title
memoObject.createdAt = .now
memoObject.updatedAt = .now
let canvasObject = CanvasObject(context: managedObjectContext)
canvasObject.width = 8_000
canvasObject.height = 8_000
let canvasObject = CanvasObject(context: managedObjectContext)
canvasObject.width = 8_000
canvasObject.height = 8_000
let graphicContextObject = GraphicContextObject(context: managedObjectContext)
graphicContextObject.strokes = []
let toolObject = ToolObject(\.viewContext)
toolObject.pens = []
memoObject.canvas = canvasObject
canvasObject.memo = memoObject
canvasObject.graphicContext = graphicContextObject
graphicContextObject.canvas = canvasObject
let eraserPenObject = PenObject.createObject(\.viewContext, penStyle: .eraser)
eraserPenObject.orderIndex = 0
let markerPenObject = PenObject.createObject(\.viewContext, penStyle: .marker)
markerPenObject.orderIndex = 1
try managedObjectContext.save()
openMemo(for: memoObject)
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
let graphicContextObject = GraphicContextObject(\.viewContext)
graphicContextObject.strokes = []
memoObject.canvas = canvasObject
memoObject.tool = toolObject
canvasObject.memo = memoObject
canvasObject.graphicContext = graphicContextObject
toolObject.memo = memoObject
toolObject.pens = [eraserPenObject, markerPenObject]
eraserPenObject.tool = toolObject
markerPenObject.tool = toolObject
graphicContextObject.canvas = canvasObject
withPersistenceSync(\.viewContext) { context in
try context.save()
DispatchQueue.main.async {
openMemo(for: memoObject)
}
}
}

View File

@@ -10,9 +10,10 @@ import Foundation
@objc(MemoObject)
final class MemoObject: NSManagedObject, Identifiable {
@NSManaged var title: String
@NSManaged var data: Data
@NSManaged var title: String
@NSManaged var createdAt: Date
@NSManaged var updatedAt: Date
@NSManaged var tool: ToolObject
@NSManaged var canvas: CanvasObject
}

View File

@@ -0,0 +1,30 @@
//
// PenObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/17/24.
//
import CoreData
import Foundation
@objc(PenObject)
class PenObject: NSManagedObject {
@NSManaged var color: [CGFloat]
@NSManaged var style: Int16
@NSManaged var thickness: CGFloat
@NSManaged var isSelected: Bool
@NSManaged var orderIndex: Int16
@NSManaged var tool: ToolObject?
}
extension PenObject {
static func createObject(_ keyPath: KeyPath<Persistence, NSManagedObjectContext>, penStyle: any PenStyle) -> PenObject {
let object = PenObject(context: Persistence.shared[keyPath: keyPath])
object.color = penStyle.color
object.style = penStyle.strokeStyle.rawValue
object.isSelected = false
object.thickness = penStyle.thickness.min
return object
}
}

View File

@@ -0,0 +1,15 @@
//
// ToolObject.swift
// Memola
//
// Created by Dscyre Scotti on 5/17/24.
//
import CoreData
import Foundation
@objc(ToolObject)
class ToolObject: NSManagedObject {
@NSManaged var pens: NSMutableSet
@NSManaged var memo: MemoObject?
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "transparency_grid 1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "transparency_grid 1@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "transparency_grid 1@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "transparency_grid 1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "transparency_grid 1@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "transparency_grid 1@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "bullet-base.png",
"filename" : "marker-base.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "bullet-base@2x.png",
"filename" : "marker-base@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bullet-base@3x.png",
"filename" : "marker-base@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "bullet-tip.png",
"filename" : "marker-tip.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "bullet-tip@2x.png",
"filename" : "marker-tip@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bullet-tip@3x.png",
"filename" : "marker-tip@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -15,6 +15,15 @@
<attribute name="title" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
<relationship name="tool" maxCount="1" deletionRule="Cascade" destinationEntity="ToolObject" inverseName="memo" inverseEntity="ToolObject"/>
</entity>
<entity name="PenObject" representedClassName="PenObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<attribute name="isSelected" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="orderIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
</entity>
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
@@ -34,4 +43,8 @@
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="strokes" inverseEntity="GraphicContextObject"/>
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="QuadObject" inverseName="stroke" inverseEntity="QuadObject"/>
</entity>
<entity name="ToolObject" representedClassName="ToolObject" syncable="YES">
<relationship name="memo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MemoObject" inverseName="tool" inverseEntity="MemoObject"/>
<relationship name="pens" toMany="YES" deletionRule="Cascade" destinationEntity="PenObject" inverseName="tool" inverseEntity="PenObject"/>
</entity>
</model>