mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-28 12:12:00 +01:00
feat: add photo render pass
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
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 */; };
|
||||
EC37FB122C1B2DD90008D976 /* ToolSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC37FB112C1B2DD90008D976 /* ToolSelection.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 */; };
|
||||
@@ -84,6 +85,11 @@
|
||||
ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.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 */; };
|
||||
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
|
||||
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; };
|
||||
@@ -111,6 +117,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -176,6 +183,11 @@
|
||||
ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.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>"; };
|
||||
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>"; };
|
||||
@@ -245,6 +257,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738BB2BE60E0300A4542E /* Tool.swift */,
|
||||
EC37FB112C1B2DD90008D976 /* ToolSelection.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
@@ -439,6 +452,7 @@
|
||||
ECA738942BE6012D00A4542E /* ViewPort.metal */,
|
||||
ECA738962BE6014200A4542E /* Graphic.metal */,
|
||||
EC3565592BF060D900A4E0BF /* Quad.metal */,
|
||||
ECD12A922C1B062000B96E12 /* Photo.metal */,
|
||||
);
|
||||
path = Shaders;
|
||||
sourceTree = "<group>";
|
||||
@@ -467,6 +481,7 @@
|
||||
ECA7389B2BE601AF00A4542E /* GridVertex.swift */,
|
||||
ECA7389D2BE601CB00A4542E /* QuadVertex.swift */,
|
||||
ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */,
|
||||
ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */,
|
||||
);
|
||||
path = Vertices;
|
||||
sourceTree = "<group>";
|
||||
@@ -594,6 +609,7 @@
|
||||
ECA738DD2BE610A000A4542E /* ViewPortRenderPass.swift */,
|
||||
ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */,
|
||||
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
|
||||
ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */,
|
||||
);
|
||||
path = RenderPasses;
|
||||
sourceTree = "<group>";
|
||||
@@ -652,6 +668,7 @@
|
||||
ECD12A872C19EF8700B96E12 /* Elements */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECD12A8D2C1AEB8000B96E12 /* Photo */,
|
||||
ECD12A882C19EF9500B96E12 /* Core */,
|
||||
ECA738F92BE6130000A4542E /* Geometries */,
|
||||
);
|
||||
@@ -666,6 +683,14 @@
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECD12A8D2C1AEB8000B96E12 /* Photo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECD12A8E2C1AEBA400B96E12 /* Photo.swift */,
|
||||
);
|
||||
path = Photo;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ECE883B82C009DC30045C53D /* Strokes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -698,6 +723,7 @@
|
||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */,
|
||||
EC9AB09E2C1401A40076AF58 /* EraserObject.swift */,
|
||||
ECD12A852C19EE3900B96E12 /* ElementObject.swift */,
|
||||
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */,
|
||||
);
|
||||
path = Objects;
|
||||
sourceTree = "<group>";
|
||||
@@ -785,6 +811,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */,
|
||||
ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */,
|
||||
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
|
||||
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
|
||||
EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */,
|
||||
@@ -808,6 +835,7 @@
|
||||
ECA738CD2BE60F2F00A4542E /* GridContext.swift in Sources */,
|
||||
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
|
||||
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */,
|
||||
EC37FB122C1B2DD90008D976 /* ToolSelection.swift in Sources */,
|
||||
ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */,
|
||||
ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */,
|
||||
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
||||
@@ -827,6 +855,8 @@
|
||||
ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */,
|
||||
EC0D14262BF7A8C9009BFE5F /* PenObject.swift 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 */,
|
||||
ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */,
|
||||
@@ -840,6 +870,8 @@
|
||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
||||
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
|
||||
ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */,
|
||||
ECD12A932C1B062000B96E12 /* Photo.metal in Sources */,
|
||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||
EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */,
|
||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||
@@ -1016,6 +1048,8 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTLLINKER_FLAGS = "";
|
||||
MTL_COMPILER_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.Memola;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -1049,6 +1083,8 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTLLINKER_FLAGS = "";
|
||||
MTL_COMPILER_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.Memola;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
|
||||
21
Memola/Canvas/Buffers/Vertices/PhotoVertex.swift
Normal file
21
Memola/Canvas/Buffers/Vertices/PhotoVertex.swift
Normal file
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ final class GraphicContext: @unchecked Sendable {
|
||||
var eraserStrokes: Set<EraserStroke> = []
|
||||
var object: GraphicContextObject?
|
||||
|
||||
var currentStroke: (any Stroke)?
|
||||
var previousStroke: (any Stroke)?
|
||||
var currentElement: Element?
|
||||
var previousElement: Element?
|
||||
|
||||
var currentPoint: CGPoint?
|
||||
var renderType: RenderType = .finished
|
||||
@@ -69,7 +69,7 @@ final class GraphicContext: @unchecked Sendable {
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
}
|
||||
previousStroke = nil
|
||||
previousElement = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ final class GraphicContext: @unchecked Sendable {
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
}
|
||||
previousStroke = nil
|
||||
previousElement = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,7 @@ extension GraphicContext: Drawable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Stroke
|
||||
extension GraphicContext {
|
||||
func beginStroke(at point: CGPoint, pen: Pen) -> any Stroke {
|
||||
let stroke: any Stroke
|
||||
@@ -231,14 +232,14 @@ extension GraphicContext {
|
||||
}
|
||||
stroke = eraserStroke
|
||||
}
|
||||
currentStroke = stroke
|
||||
currentElement = .stroke(stroke.anyStroke)
|
||||
currentPoint = point
|
||||
currentStroke?.begin(at: point)
|
||||
currentElement?.stroke()?.begin(at: point)
|
||||
return stroke
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@@ -247,7 +248,7 @@ extension GraphicContext {
|
||||
}
|
||||
|
||||
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)
|
||||
if let penStroke = currentStroke.stroke(as: PenStroke.self) {
|
||||
penStroke.saveQuads()
|
||||
@@ -268,13 +269,13 @@ extension GraphicContext {
|
||||
context.refreshAllObjects()
|
||||
}
|
||||
}
|
||||
previousStroke = currentStroke
|
||||
self.currentStroke = nil
|
||||
previousElement = currentElement
|
||||
self.currentElement = nil
|
||||
self.currentPoint = nil
|
||||
}
|
||||
|
||||
func cancelStroke() {
|
||||
if let stroke = currentStroke {
|
||||
if let stroke = currentElement?.stroke() {
|
||||
switch stroke.style {
|
||||
case .marker:
|
||||
guard let _stroke = stroke.stroke(as: PenStroke.self) else { break }
|
||||
@@ -296,11 +297,23 @@ extension GraphicContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
currentStroke = nil
|
||||
currentElement = nil
|
||||
currentPoint = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Photo
|
||||
extension GraphicContext {
|
||||
func insertPhoto(at point: CGPoint) {
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
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(size: size, origin: origin, bounds: bounds, createdAt: .now)
|
||||
tree.insert(.photo(photo), in: photo.photoBox)
|
||||
self.previousElement = .photo(photo)
|
||||
}
|
||||
}
|
||||
|
||||
extension GraphicContext {
|
||||
enum RenderType {
|
||||
case inProgress
|
||||
|
||||
@@ -39,8 +39,8 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
|
||||
}
|
||||
|
||||
var hasValidStroke: Bool {
|
||||
if let currentStroke = graphicContext.currentStroke {
|
||||
return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80
|
||||
if let currentElement = graphicContext.currentElement {
|
||||
return Date.now.timeIntervalSince(currentElement.createdAt) * 1000 > 80
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -103,7 +103,7 @@ extension Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Graphic Context
|
||||
// MARK: - Stroke
|
||||
extension Canvas {
|
||||
func beginTouch(at point: CGPoint, pen: Pen) -> any Stroke {
|
||||
graphicContext.beginStroke(at: point, pen: pen)
|
||||
@@ -126,6 +126,13 @@ extension Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Photo
|
||||
extension Canvas {
|
||||
func insertPhoto(at point: CGPoint) {
|
||||
graphicContext.insertPhoto(at: point)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Rendering
|
||||
extension Canvas {
|
||||
func renderGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
|
||||
@@ -93,6 +93,28 @@ struct PipelineStates {
|
||||
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? {
|
||||
var label: String
|
||||
var vertexName: String
|
||||
|
||||
@@ -25,6 +25,9 @@ final class Renderer {
|
||||
lazy var eraserRenderPass: EraserRenderPass = {
|
||||
EraserRenderPass(renderer: self)
|
||||
}()
|
||||
lazy var photoRenderPass: PhotoRenderPass = {
|
||||
PhotoRenderPass(renderer: self)
|
||||
}()
|
||||
lazy var graphicRenderPass: GraphicRenderPass = {
|
||||
GraphicRenderPass(renderer: self)
|
||||
}()
|
||||
@@ -66,12 +69,14 @@ final class Renderer {
|
||||
|
||||
func draw(in view: MTKView, on canvas: Canvas) {
|
||||
if !updatesViewPort {
|
||||
graphicRenderPass.photoRenderPass = photoRenderPass
|
||||
graphicRenderPass.strokeRenderPass = strokeRenderPass
|
||||
graphicRenderPass.eraserRenderPass = eraserRenderPass
|
||||
graphicRenderPass.draw(on: canvas, with: self)
|
||||
}
|
||||
|
||||
cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture
|
||||
cacheRenderPass.photoRenderPass = photoRenderPass
|
||||
cacheRenderPass.strokeRenderPass = strokeRenderPass
|
||||
cacheRenderPass.eraserRenderPass = eraserRenderPass
|
||||
cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture
|
||||
|
||||
@@ -9,7 +9,14 @@ import Foundation
|
||||
|
||||
enum Element: Equatable, Comparable {
|
||||
case stroke(AnyStroke)
|
||||
case photo
|
||||
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 {
|
||||
@@ -17,4 +24,20 @@ enum Element: Equatable, Comparable {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
Memola/Canvas/Elements/Photo/Photo.swift
Normal file
100
Memola/Canvas/Elements/Photo/Photo.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// 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 bounds: [CGFloat]
|
||||
var createdAt: Date
|
||||
|
||||
var object: PhotoObject?
|
||||
|
||||
var vertices: [PhotoVertex] = []
|
||||
var vertexCount: Int = 0
|
||||
var vertexBuffer: MTLBuffer?
|
||||
|
||||
init(size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date) {
|
||||
self.size = size
|
||||
self.origin = origin
|
||||
self.bounds = bounds
|
||||
self.createdAt = createdAt
|
||||
generateVertices()
|
||||
}
|
||||
|
||||
convenience init(object: PhotoObject) {
|
||||
self.init(
|
||||
size: .init(width: object.width, height: object.height),
|
||||
origin: .init(x: object.originX, y: object.originY),
|
||||
bounds: object.bounds,
|
||||
createdAt: object.createdAt ?? .now
|
||||
)
|
||||
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: 1)),
|
||||
PhotoVertex(x: minX, y: maxY, textCoord: CGPoint(x: 0, y: 0)),
|
||||
PhotoVertex(x: maxX, y: minY, textCoord: CGPoint(x: 1, y: 1)),
|
||||
PhotoVertex(x: maxX, y: maxY, textCoord: CGPoint(x: 1, y: 0)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension Photo: Drawable {
|
||||
func prepare(device: any MTLDevice) {
|
||||
guard vertexBuffer == nil else { return }
|
||||
vertexCount = vertices.endIndex
|
||||
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PhotoVertex>.stride, options: [])
|
||||
}
|
||||
|
||||
func draw(device: any MTLDevice, renderEncoder: any MTLRenderCommandEncoder) {
|
||||
prepare(device: device)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class CacheRenderPass: RenderPass {
|
||||
weak var graphicTexture: MTLTexture?
|
||||
var cacheTexture: MTLTexture?
|
||||
|
||||
weak var photoRenderPass: PhotoRenderPass?
|
||||
weak var strokeRenderPass: StrokeRenderPass?
|
||||
weak var eraserRenderPass: EraserRenderPass?
|
||||
var clearsTexture: Bool = true
|
||||
@@ -34,7 +35,7 @@ class CacheRenderPass: RenderPass {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -45,18 +46,26 @@ class CacheRenderPass: RenderPass {
|
||||
descriptor.colorAttachments[0].storeAction = .store
|
||||
|
||||
let graphicContext = canvas.graphicContext
|
||||
if let stroke = graphicContext.currentStroke {
|
||||
switch stroke.style {
|
||||
case .eraser:
|
||||
eraserRenderPass.stroke = stroke
|
||||
eraserRenderPass.descriptor = descriptor
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
case .marker:
|
||||
canvas.setGraphicRenderType(.inProgress)
|
||||
strokeRenderPass.stroke = stroke
|
||||
strokeRenderPass.graphicDescriptor = descriptor
|
||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
||||
if let element = graphicContext.currentElement {
|
||||
switch element {
|
||||
case .stroke(let anyStroke):
|
||||
let stroke = anyStroke.value
|
||||
switch stroke.style {
|
||||
case .eraser:
|
||||
eraserRenderPass.stroke = stroke
|
||||
eraserRenderPass.descriptor = descriptor
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
case .marker:
|
||||
canvas.setGraphicRenderType(.inProgress)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class GraphicRenderPass: RenderPass {
|
||||
|
||||
var graphicPipelineState: MTLRenderPipelineState?
|
||||
|
||||
weak var photoRenderPass: PhotoRenderPass?
|
||||
weak var strokeRenderPass: StrokeRenderPass?
|
||||
weak var eraserRenderPass: EraserRenderPass?
|
||||
|
||||
@@ -32,7 +33,7 @@ class GraphicRenderPass: RenderPass {
|
||||
}
|
||||
|
||||
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 graphicPipelineState else { return }
|
||||
@@ -46,12 +47,12 @@ class GraphicRenderPass: RenderPass {
|
||||
if renderer.redrawsGraphicRender {
|
||||
canvas.setGraphicRenderType(.finished)
|
||||
for _element in graphicContext.tree.search(box: canvas.bounds.box) {
|
||||
if graphicContext.previousElement == _element || graphicContext.currentElement == _element {
|
||||
continue
|
||||
}
|
||||
switch _element {
|
||||
case .stroke(let _stroke):
|
||||
let stroke = _stroke.value
|
||||
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
||||
continue
|
||||
}
|
||||
guard stroke.isVisible(in: canvas.bounds) else { continue }
|
||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||
clearsTexture = false
|
||||
@@ -74,36 +75,48 @@ class GraphicRenderPass: RenderPass {
|
||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
||||
}
|
||||
}
|
||||
case .photo:
|
||||
break
|
||||
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 stroke = graphicContext.previousStroke {
|
||||
if let element = graphicContext.previousElement {
|
||||
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
|
||||
switch element {
|
||||
case .stroke(let anyStroke):
|
||||
let stroke = anyStroke.value
|
||||
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)
|
||||
}
|
||||
}
|
||||
case .photo(let photo):
|
||||
photoRenderPass.photo = photo
|
||||
photoRenderPass.descriptor = descriptor
|
||||
photoRenderPass.draw(on: canvas, with: renderer)
|
||||
}
|
||||
graphicContext.previousStroke = nil
|
||||
graphicContext.previousElement = nil
|
||||
}
|
||||
|
||||
let eraserStrokes = graphicContext.eraserStrokes
|
||||
|
||||
45
Memola/Canvas/RenderPasses/PhotoRenderPass.swift
Normal file
45
Memola/Canvas/RenderPasses/PhotoRenderPass.swift
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
47
Memola/Canvas/Shaders/Photo.metal
Normal file
47
Memola/Canvas/Shaders/Photo.metal
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// 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;
|
||||
return float4(1, 0, 1, 1);
|
||||
}
|
||||
@@ -17,6 +17,8 @@ public class Tool: NSObject, ObservableObject {
|
||||
@Published var selectedPen: Pen?
|
||||
@Published var draggedPen: Pen?
|
||||
|
||||
@Published var selection: ToolSelection = .none
|
||||
|
||||
let scrollPublisher = PassthroughSubject<String, Never>()
|
||||
var markers: [Pen] {
|
||||
pens.filter { $0.strokeStyle == .marker }
|
||||
|
||||
14
Memola/Canvas/Tool/Core/ToolSelection.swift
Normal file
14
Memola/Canvas/Tool/Core/ToolSelection.swift
Normal file
@@ -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
|
||||
}
|
||||
|
||||
var photoInsertGesture: UITapGestureRecognizer?
|
||||
|
||||
let tool: Tool
|
||||
let canvas: Canvas
|
||||
let history: History
|
||||
@@ -40,6 +42,7 @@ class CanvasViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
configureViews()
|
||||
configureGestures()
|
||||
configureListeners()
|
||||
}
|
||||
|
||||
@@ -176,6 +179,11 @@ extension CanvasViewController {
|
||||
self?.penChanged(to: pen)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
tool.$selection
|
||||
.sink { [weak self] selection in
|
||||
self?.toolSelectionChanged(to: selection)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
history.historyPublisher
|
||||
.sink { [weak self] action in
|
||||
@@ -216,6 +224,21 @@ 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) {
|
||||
let point = gesture.location(in: drawingView)
|
||||
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio))
|
||||
drawingView.draw()
|
||||
}
|
||||
}
|
||||
|
||||
extension CanvasViewController: UIScrollViewDelegate {
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
drawingView
|
||||
@@ -300,10 +323,31 @@ extension CanvasViewController {
|
||||
if let pen, let device = drawingView.renderView.device {
|
||||
pen.style.loadTexture(on: device)
|
||||
}
|
||||
let isPenSelected = pen != nil
|
||||
scrollView.isScrollEnabled = !isPenSelected
|
||||
drawingView.isUserInteractionEnabled = isPenSelected
|
||||
isPenSelected ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction()
|
||||
}
|
||||
|
||||
func toolSelectionChanged(to selection: ToolSelection) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,20 +87,20 @@ class DrawingView: UIView {
|
||||
guard !disablesUserInteraction else { return }
|
||||
canvas.moveTouch(to: point.muliply(by: ratio))
|
||||
if canvas.hasValidStroke {
|
||||
renderView.draw()
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
func touchEnded(at point: CGPoint) {
|
||||
guard !disablesUserInteraction else { return }
|
||||
canvas.endTouch(at: point.muliply(by: ratio))
|
||||
renderView.draw()
|
||||
draw()
|
||||
}
|
||||
|
||||
func touchCancelled() {
|
||||
if canvas.graphicContext.currentStroke != nil {
|
||||
if canvas.graphicContext.currentElement != nil {
|
||||
canvas.cancelTouch()
|
||||
renderView.draw()
|
||||
draw()
|
||||
history.restoreUndo()
|
||||
}
|
||||
}
|
||||
@@ -114,4 +114,8 @@ class DrawingView: UIView {
|
||||
self?.disablesUserInteraction = false
|
||||
}
|
||||
}
|
||||
|
||||
func draw() {
|
||||
renderView.draw()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,14 +30,17 @@ struct MemoView: View {
|
||||
CanvasView(tool: tool, canvas: canvas, history: history)
|
||||
.ignoresSafeArea()
|
||||
.overlay(alignment: .trailing) {
|
||||
PenDock(tool: tool, canvas: canvas)
|
||||
if tool.selection == .pen {
|
||||
PenDock(tool: tool, canvas: canvas)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottomLeading) {
|
||||
zoomControl
|
||||
}
|
||||
.disabled(textFieldState)
|
||||
.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)
|
||||
.overlay {
|
||||
|
||||
@@ -86,9 +86,7 @@ struct PenDock: View {
|
||||
.padding(.vertical, 5)
|
||||
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
|
||||
.onTapGesture {
|
||||
if tool.selectedPen === pen {
|
||||
tool.unselectPen(pen)
|
||||
} else {
|
||||
if tool.selectedPen !== pen {
|
||||
tool.selectPen(pen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import Foundation
|
||||
struct Toolbar: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@ObservedObject var tool: Tool
|
||||
@ObservedObject var canvas: Canvas
|
||||
@ObservedObject var history: History
|
||||
|
||||
@@ -20,9 +21,10 @@ struct Toolbar: View {
|
||||
|
||||
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.memo = memo
|
||||
self.tool = tool
|
||||
self.canvas = canvas
|
||||
self.history = history
|
||||
self.title = memo.title
|
||||
@@ -30,15 +32,21 @@ struct Toolbar: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 5) {
|
||||
if !canvas.locksCanvas {
|
||||
closeButton
|
||||
titleField
|
||||
HStack(spacing: 5) {
|
||||
if !canvas.locksCanvas {
|
||||
closeButton
|
||||
titleField
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if !canvas.locksCanvas {
|
||||
historyControl
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
elementTool
|
||||
HStack(spacing: 5) {
|
||||
if !canvas.locksCanvas {
|
||||
historyControl
|
||||
}
|
||||
lockButton
|
||||
}
|
||||
lockButton
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.padding(10)
|
||||
@@ -82,6 +90,39 @@ struct Toolbar: View {
|
||||
.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")
|
||||
.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)
|
||||
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)
|
||||
}
|
||||
.background(.regularMaterial)
|
||||
.clipShape(.rect(cornerRadius: 8))
|
||||
}
|
||||
|
||||
var historyControl: some View {
|
||||
HStack {
|
||||
Button {
|
||||
@@ -111,6 +152,7 @@ struct Toolbar: View {
|
||||
|
||||
var lockButton: some View {
|
||||
Button {
|
||||
#warning("TODO: need to revisit toggale logic")
|
||||
withAnimation {
|
||||
canvas.locksCanvas.toggle()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import Foundation
|
||||
final class ElementObject: NSManagedObject {
|
||||
@NSManaged var type: Int16
|
||||
@NSManaged var createdAt: Date?
|
||||
@NSManaged var photo: PhotoObject?
|
||||
@NSManaged var stroke: StrokeObject?
|
||||
@NSManaged var graphicContext: GraphicContextObject?
|
||||
}
|
||||
|
||||
21
Memola/Persistence/Objects/PhotoObject.swift
Normal file
21
Memola/Persistence/Objects/PhotoObject.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// 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 image: Data?
|
||||
@NSManaged var element: ElementObject?
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
<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">
|
||||
@@ -40,6 +41,16 @@
|
||||
<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="PhotoObject" representedClassName="PhotoObject" syncable="YES">
|
||||
<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="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
|
||||
<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">
|
||||
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||
<attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
|
||||
Reference in New Issue
Block a user