mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-26 03:11:19 +01:00
feat: implement photo insertion
This commit is contained in:
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
24
Memola/Extensions/UIImage++.swift
Normal file
24
Memola/Extensions/UIImage++.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user