feat: implement photo insertion

This commit is contained in:
dscyrescotti
2024-06-15 20:53:45 +07:00
parent 5203f39f96
commit 46330b9a7d
13 changed files with 132 additions and 36 deletions

View File

@@ -84,7 +84,8 @@
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; };
ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; };
ECBE52992C1D60E5006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52982C1D60E5006BDB3D /* CameraView.swift */; };
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; };
ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529D2C1DAB21006BDB3D /* UIImage++.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 */; };
@@ -184,7 +185,8 @@
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = "<group>"; };
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = "<group>"; };
ECBE52982C1D60E5006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage++.swift"; sourceTree = "<group>"; };
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>"; };
@@ -234,7 +236,7 @@
EC1B783A2BF9C68C005A34E2 /* Views */ = {
isa = PBXGroup;
children = (
ECBE52972C1D6087006BDB3D /* CameraView */,
ECBE529B2C1D94A4006BDB3D /* CameraView */,
ECFC51252BF8885000D0D051 /* ColorPicker */,
);
path = Views;
@@ -509,6 +511,7 @@
EC3565532BEFC6AD00A4E0BF /* View++.swift */,
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */,
EC35655B2BF0712A00A4E0BF /* Float++.swift */,
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -679,10 +682,10 @@
path = PhotoPreview;
sourceTree = "<group>";
};
ECBE52972C1D6087006BDB3D /* CameraView */ = {
ECBE529B2C1D94A4006BDB3D /* CameraView */ = {
isa = PBXGroup;
children = (
ECBE52982C1D60E5006BDB3D /* CameraView.swift */,
ECBE529A2C1D94A4006BDB3D /* CameraView.swift */,
);
path = CameraView;
sourceTree = "<group>";
@@ -863,6 +866,7 @@
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */,
ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */,
EC2BEBF62C0F600D005DB0AF /* Box.swift in Sources */,
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
@@ -901,7 +905,7 @@
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */,
ECBE52992C1D60E5006BDB3D /* CameraView.swift in Sources */,
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */,
ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */,
ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */,
ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */,

View File

@@ -304,11 +304,11 @@ extension GraphicContext {
// MARK: - Photo
extension GraphicContext {
func insertPhoto(at point: CGPoint) {
func insertPhoto(at point: CGPoint, url: URL) {
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)
let photo = Photo(url: url, size: size, origin: origin, bounds: bounds, createdAt: .now)
tree.insert(.photo(photo), in: photo.photoBox)
self.previousElement = .photo(photo)
}

View File

@@ -128,8 +128,31 @@ extension Canvas {
// MARK: - Photo
extension Canvas {
func insertPhoto(at point: CGPoint) {
graphicContext.insertPhoto(at: point)
func insertPhoto(at point: CGPoint, url: URL) {
graphicContext.insertPhoto(at: point, url: url)
}
func savePhoto(_ data: Data) -> URL? {
let fileManager = FileManager.default
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let fileName = "\(UUID().uuidString)-\(Date.now.timeIntervalSince1970)"
let folder = directory.appendingPathComponent(canvasID.uriRepresentation().lastPathComponent, conformingTo: .folder)
if !fileManager.fileExists(atPath: folder.path()) {
do {
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
return nil
}
}
let file = folder.appendingPathComponent(fileName, conformingTo: .jpeg)
do {
try data.write(to: file)
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
return nil
}
return file
}
}

View File

@@ -26,6 +26,18 @@ class Textures {
return penTexture
}
@discardableResult
static func createPhotoTexture(for url: URL, on device: MTLDevice) -> MTLTexture? {
let textureLoader = MTKTextureLoader(device: device)
do {
let photoTexture = try textureLoader.newTexture(URL: url, options: [.SRGB: false])
return photoTexture
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
return nil
}
}
static func createGraphicTexture(
from renderer: Renderer,
size: CGSize,

View File

@@ -12,18 +12,31 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
var id: UUID = UUID()
var size: CGSize
var origin: CGPoint
var image: UIImage?
var url: URL?
var bounds: [CGFloat]
var createdAt: Date
var object: PhotoObject?
var texture: MTLTexture?
var vertices: [PhotoVertex] = []
var vertexCount: Int = 0
var vertexBuffer: MTLBuffer?
init(size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date) {
init(image: UIImage?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date) {
self.size = size
self.origin = origin
self.image = image
self.bounds = bounds
self.createdAt = createdAt
generateVertices()
}
init(url: URL?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date) {
self.size = size
self.origin = origin
self.url = url
self.bounds = bounds
self.createdAt = createdAt
generateVertices()
@@ -31,6 +44,7 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
convenience init(object: PhotoObject) {
self.init(
image: UIImage(data: object.image ?? .init()),
size: .init(width: object.width, height: object.height),
origin: .init(x: object.originX, y: object.originY),
bounds: object.bounds,
@@ -45,25 +59,31 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
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)),
PhotoVertex(x: minX, y: minY, textCoord: CGPoint(x: 0, y: 0)),
PhotoVertex(x: minX, y: maxY, textCoord: CGPoint(x: 0, y: 1)),
PhotoVertex(x: maxX, y: minY, textCoord: CGPoint(x: 1, y: 0)),
PhotoVertex(x: maxX, y: maxY, textCoord: CGPoint(x: 1, y: 1)),
]
}
}
extension Photo: Drawable {
func prepare(device: any MTLDevice) {
guard vertexBuffer == nil else { return }
vertexCount = vertices.endIndex
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PhotoVertex>.stride, options: [])
if vertexBuffer == nil {
vertexCount = vertices.endIndex
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PhotoVertex>.stride, options: [])
}
if texture == nil, let url {
texture = Textures.createPhotoTexture(for: url, on: device)
}
}
func draw(device: any MTLDevice, renderEncoder: any MTLRenderCommandEncoder) {
prepare(device: device)
renderEncoder.setFragmentTexture(texture, index: 0)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertices.count)
texture = nil
}
}

View File

@@ -37,11 +37,10 @@ vertex VertexOut vertex_photo(
}
fragment float4 fragment_photo(
VertexOut out [[stage_in]]
// texture2d<float> texture [[texture(0)]]
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);
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
float4 color = float4(texture.sample(textureSampler, out.textCoord));
return color;
}

View File

@@ -19,7 +19,7 @@ public class Tool: NSObject, ObservableObject {
@Published var selectedPen: Pen?
@Published var draggedPen: Pen?
// MARK: - Photo
@Published var selectedImage: UIImage?
@Published var selectedImageURL: URL?
@Published var selection: ToolSelection = .none

View File

@@ -233,8 +233,12 @@ extension CanvasViewController {
}
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
guard let url = tool.selectedImageURL else { return }
withAnimation {
tool.selectedImageURL = nil
}
let point = gesture.location(in: drawingView)
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio))
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), url: url)
drawingView.draw()
}
}

View File

@@ -8,7 +8,9 @@
import SwiftUI
struct CameraView: UIViewControllerRepresentable {
@Binding var image: UIImage?
@Binding var url: URL?
@ObservedObject var canvas: Canvas
@Environment(\.dismiss) private var dismiss
@@ -33,7 +35,10 @@ struct CameraView: UIViewControllerRepresentable {
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
parent.image = info[.originalImage] as? UIImage
let image = (info[.originalImage] as? UIImage)?.imageWithUpOrientation()
if let image, let data = image.jpegData(compressionQuality: 1) {
parent.url = parent.canvas.savePhoto(data)
}
parent.dismiss()
}

View File

@@ -0,0 +1,24 @@
//
// UIImage++.swift
// Memola
//
// Created by Dscyre Scotti on 6/15/24.
//
import UIKit
import Foundation
extension UIImage {
func imageWithUpOrientation() -> UIImage? {
switch imageOrientation {
case .up:
return self
default:
UIGraphicsBeginImageContextWithOptions(size, false, scale)
draw(in: CGRect(origin: .zero, size: size))
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
}
}

View File

@@ -35,8 +35,8 @@ struct MemoView: View {
PenDock(tool: tool, canvas: canvas)
.transition(.move(edge: .trailing))
case .photo:
if let image = tool.selectedImage {
PhotoPreview(image: image, tool: tool)
if let url = tool.selectedImageURL {
PhotoPreview(url: url, tool: tool)
.transition(.move(edge: .trailing))
}
default:

View File

@@ -8,11 +8,15 @@
import SwiftUI
struct PhotoPreview: View {
let image: UIImage
let url: URL
@ObservedObject var tool: Tool
var data: Data {
(try? Data(contentsOf: url)) ?? Data()
}
var body: some View {
Image(uiImage: image)
Image(uiImage: UIImage(data: data) ?? UIImage())
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
@@ -27,7 +31,7 @@ struct PhotoPreview: View {
.overlay(alignment: .topLeading) {
Button {
withAnimation {
tool.selectedImage = nil
tool.selectedImageURL = nil
}
} label: {
Image(systemName: "xmark.circle.fill")

View File

@@ -61,8 +61,9 @@ struct Toolbar: View {
Task {
let data = try? await newValue?.loadTransferable(type: Data.self)
if let data {
let url = canvas.savePhoto(data)
withAnimation {
tool.selectedImage = UIImage(data: data)
tool.selectedImageURL = url
}
}
photoItem = nil
@@ -70,7 +71,7 @@ struct Toolbar: View {
}
}
.fullScreenCover(isPresented: $opensCamera) {
CameraView(image: $tool.selectedImage)
CameraView(url: $tool.selectedImageURL, canvas: canvas)
.ignoresSafeArea()
}
.alert("Camera Access Denied", isPresented: $isCameraAccessDenied) {
@@ -166,7 +167,7 @@ struct Toolbar: View {
.clipShape(.rect(cornerRadius: 8))
}
.hoverEffect(.lift)
PhotosPicker(selection: $photoItem, matching: .images) {
PhotosPicker(selection: $photoItem, matching: .images, preferredItemEncoding: .compatible) {
Image(systemName: "photo.fill.on.rectangle.fill")
.contentShape(.circle)
.frame(width: size, height: size)