From c46424ba87cb75828874c6137793b1877c8ec5a5 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Fri, 5 Jul 2024 17:49:11 +0700 Subject: [PATCH] feat: add memo preview generation --- Memola.xcodeproj/project.pbxproj | 8 ++ Memola/Canvas/Core/Canvas.swift | 49 ++++++++ Memola/Canvas/Core/Renderer.swift | 20 +++ Memola/Canvas/Core/Textures.swift | 23 ++++ Memola/Canvas/Elements/Core/Element.swift | 9 ++ .../RenderPasses/EraserRenderPass.swift | 15 ++- .../RenderPasses/GraphicRenderPass.swift | 2 +- .../Canvas/RenderPasses/PhotoRenderPass.swift | 15 ++- .../RenderPasses/PreviewRenderPass.swift | 118 ++++++++++++++++++ .../RenderPasses/StrokeRenderPass.swift | 22 +++- .../ViewController/CanvasViewController.swift | 1 + Memola/Extensions/MTLTexture++.swift | 48 +++++++ .../Dashboard/Details/Memos/MemosView.swift | 8 +- .../Dashboard/Details/Shared/MemoCard.swift | 8 +- .../Dashboard/Details/Shared/MemoGrid.swift | 32 +++-- .../Details/Shared/MemoPreview.swift | 17 ++- .../Dashboard/Details/Trash/TrashView.swift | 10 +- .../Features/Dashboard/Sidebar/Sidebar.swift | 2 +- Memola/Features/Memo/Toolbar/Toolbar.swift | 17 +-- Memola/Persistence/Objects/MemoObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 1 + 21 files changed, 377 insertions(+), 49 deletions(-) create mode 100644 Memola/Canvas/RenderPasses/PreviewRenderPass.swift create mode 100644 Memola/Extensions/MTLTexture++.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 55311f8..8ce3dd4 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -112,6 +112,8 @@ ECD12A932C1B062000B96E12 /* Photo.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A922C1B062000B96E12 /* Photo.metal */; }; ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */; }; ECDAC07B2C318DBC0000ED77 /* ElementToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */; }; + ECDDD40D2C366B3B00DF9D5E /* PreviewRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDDD40C2C366B3B00DF9D5E /* PreviewRenderPass.swift */; }; + ECDDD40F2C368B2700DF9D5E /* MTLTexture++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDDD40E2C368B2700DF9D5E /* MTLTexture++.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 */; }; @@ -232,6 +234,8 @@ ECD12A922C1B062000B96E12 /* Photo.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Photo.metal; sourceTree = ""; }; ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVertex.swift; sourceTree = ""; }; ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToolbar.swift; sourceTree = ""; }; + ECDDD40C2C366B3B00DF9D5E /* PreviewRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewRenderPass.swift; sourceTree = ""; }; + ECDDD40E2C368B2700DF9D5E /* MTLTexture++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLTexture++.swift"; sourceTree = ""; }; ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = ""; }; ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = ""; }; ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = ""; }; @@ -629,6 +633,7 @@ ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */, ECC995A42C1EB4CC00B2699A /* Data++.swift */, EC18150E2C2DB13200541369 /* Date++.swift */, + ECDDD40E2C368B2700DF9D5E /* MTLTexture++.swift */, ); path = Extensions; sourceTree = ""; @@ -738,6 +743,7 @@ ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */, ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */, EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */, + ECDDD40C2C366B3B00DF9D5E /* PreviewRenderPass.swift */, ); path = RenderPasses; sourceTree = ""; @@ -991,6 +997,7 @@ ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */, EC1815082C2D980B00541369 /* Sort.swift in Sources */, ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */, + ECDDD40D2C366B3B00DF9D5E /* PreviewRenderPass.swift in Sources */, ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */, ECA738CD2BE60F2F00A4542E /* PointGridContext.swift in Sources */, ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */, @@ -1067,6 +1074,7 @@ EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */, EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */, EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */, + ECDDD40F2C368B2700DF9D5E /* MTLTexture++.swift in Sources */, ECC995A52C1EB4CC00B2699A /* Data++.swift in Sources */, ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */, ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */, diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 6e823dd..1798f51 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -25,6 +25,7 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable { let defaultZoomScale: CGFloat = 20 var transform: simd_float4x4 = .init() + var previewTransform: simd_float4x4 = .init() var clipBounds: CGRect = .zero var bounds: CGRect = .zero var uniformsBuffer: MTLBuffer? @@ -37,6 +38,8 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable { let zoomPublisher = PassthroughSubject() + weak var renderer: Renderer? + init(size: CGSize, canvasID: NSManagedObjectID, gridMode: Int16) { self.size = size self.canvasID = canvasID @@ -78,6 +81,23 @@ extension Canvas { context.refreshAllObjects() } } + + func save(for memoObject: MemoObject, completion: @escaping () -> Void) { + state = .closing + let previewImage = renderer?.drawPreview(on: self) + memoObject.preview = previewImage?.jpegData(compressionQuality: 0.8) + withPersistenceSync(\.viewContext) { context in + try context.saveIfNeeded() + } + withPersistence(\.backgroundContext) { [weak self] context in + try context.saveIfNeeded() + context.refreshAllObjects() + DispatchQueue.main.async { [weak self] in + self?.state = .closed + completion() + } + } + } } // MARK: - Dimension @@ -92,6 +112,29 @@ extension Canvas { self.transform = simd_float4x4(transform1 * transform2 * transform3) } + func updatePreviewTransform(to targetRect: CGRect) { + let bounds = CGRect(origin: .zero, size: size) + let translationTransform = CGAffineTransform(translationX: -targetRect.origin.x, y: -targetRect.origin.y) + + let scaleX = bounds.width / targetRect.width + let scaleY = bounds.height / targetRect.height + let scalingTransform = CGAffineTransform(scaleX: scaleX, y: scaleY) + + let combinedTransform = translationTransform.concatenating(scalingTransform) + + let normalizeX = CGAffineTransform(scaleX: 1.0 / bounds.width, y: 1.0) + let normalizeY = CGAffineTransform(scaleX: 1.0, y: 1.0 / bounds.height) + let normalizeTransform = normalizeX.concatenating(normalizeY) + + let normalizedTransform = combinedTransform.concatenating(normalizeTransform) + + let renderScale = CGAffineTransform(scaleX: 2.0, y: 2.0) + let renderTranslation = CGAffineTransform(translationX: -1.0, y: -1.0) + let transform = normalizedTransform.concatenating(renderScale).concatenating(renderTranslation) + + self.previewTransform = simd_float4x4(transform) + } + func updateClipBounds(_ scrollView: UIScrollView, on drawingView: DrawingView) { let ratio = drawingView.ratio let bounds = scrollView.convert(scrollView.bounds, to: drawingView) @@ -197,6 +240,12 @@ extension Canvas { uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout.size) renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11) } + + func setPreviewUniformsBuffer(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) { + var uniforms = Uniforms(transform: previewTransform) + uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout.size) + renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11) + } } // MARK: - State diff --git a/Memola/Canvas/Core/Renderer.swift b/Memola/Canvas/Core/Renderer.swift index 0f143f3..e5292b9 100644 --- a/Memola/Canvas/Core/Renderer.swift +++ b/Memola/Canvas/Core/Renderer.swift @@ -40,6 +40,9 @@ final class Renderer { lazy var photoBackgroundRenderPass: PhotoBackgroundRenderPass = { PhotoBackgroundRenderPass(renderer: self) }() + lazy var previewRenderPass: PreviewRenderPass = { + PreviewRenderPass(renderer: self) + }() init(canvasView: MTKView) { guard let device = MTLCreateSystemDefaultDevice() else { @@ -106,4 +109,21 @@ final class Renderer { viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture viewPortRenderPass.draw(into: commandBuffer, on: canvas, with: self) } + + func drawPreview(on canvas: Canvas) -> UIImage? { + guard let commandBuffer = commandQueue.makeCommandBuffer() else { + NSLog("[Memola] - Unable to create command buffer") + return nil + } + strokeRenderPass.eraserRenderPass = eraserRenderPass + previewRenderPass.photoRenderPass = photoRenderPass + previewRenderPass.strokeRenderPass = strokeRenderPass + previewRenderPass.eraserRenderPass = eraserRenderPass + previewRenderPass.draw(into: commandBuffer, on: canvas, with: self) + + guard let cgImage = previewRenderPass.previewTexture?.getImage() else { + return nil + } + return UIImage(cgImage: cgImage, scale: 1.0, orientation: .downMirrored) + } } diff --git a/Memola/Canvas/Core/Textures.swift b/Memola/Canvas/Core/Textures.swift index 838ab4a..53ee6c7 100644 --- a/Memola/Canvas/Core/Textures.swift +++ b/Memola/Canvas/Core/Textures.swift @@ -139,4 +139,27 @@ class Textures { texture.label = "Photo Background Texture" return texture } + + static func createPreviewTexture( + from renderer: Renderer, + size: CGSize, + pixelFormat: MTLPixelFormat? = nil + ) -> MTLTexture? { + let width = Int(size.width) + let height = Int(size.height) + guard width > 0, height > 0 else { return nil } + let descriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: pixelFormat ?? renderer.pixelFormat, + width: width, + height: height, + mipmapped: false + ) + descriptor.storageMode = .shared + descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite] + guard let texture = renderer.device.makeTexture(descriptor: descriptor) else { + return nil + } + texture.label = "Preview Texture" + return texture + } } diff --git a/Memola/Canvas/Elements/Core/Element.swift b/Memola/Canvas/Elements/Core/Element.swift index b17c489..18eec90 100644 --- a/Memola/Canvas/Elements/Core/Element.swift +++ b/Memola/Canvas/Elements/Core/Element.swift @@ -41,6 +41,15 @@ enum Element: Equatable, Comparable { } } + var box: Box { + switch self { + case .stroke(let anyStroke): + anyStroke.value.strokeBox + case .photo(let photo): + photo.photoBox + } + } + var elementGroupType: ElementGroup.ElementGroupType { switch self { case .stroke(let anyStroke): diff --git a/Memola/Canvas/RenderPasses/EraserRenderPass.swift b/Memola/Canvas/RenderPasses/EraserRenderPass.swift index 30b8c90..229a452 100644 --- a/Memola/Canvas/RenderPasses/EraserRenderPass.swift +++ b/Memola/Canvas/RenderPasses/EraserRenderPass.swift @@ -28,6 +28,15 @@ class EraserRenderPass: RenderPass { @discardableResult func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool { + draw(into: commandBuffer, on: canvas, with: renderer, isPreview: false) + } + + @discardableResult + func drawPreview(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool { + draw(into: commandBuffer, on: canvas, with: renderer, isPreview: true) + } + + private func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer, isPreview: Bool) -> Bool { guard let elementGroup else { return false } guard let descriptor else { return false } @@ -65,7 +74,11 @@ class EraserRenderPass: RenderPass { guard let eraserPipelineState else { return false } renderEncoder.setRenderPipelineState(eraserPipelineState) - canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + if isPreview { + canvas.setPreviewUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + } else { + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + } if let indexBuffer { renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index 81a4da6..c13d903 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -69,7 +69,7 @@ class GraphicRenderPass: RenderPass { draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer) } let end = Date.now.timeIntervalSince1970 * 1000 - NSLog("[Memola] - duration: \(end - start)") + NSLog("[Memola] - graphic duration: \(end - start)") renderer.redrawsGraphicRender = false } if let element = graphicContext.previousElement { diff --git a/Memola/Canvas/RenderPasses/PhotoRenderPass.swift b/Memola/Canvas/RenderPasses/PhotoRenderPass.swift index 843aa18..6761a0e 100644 --- a/Memola/Canvas/RenderPasses/PhotoRenderPass.swift +++ b/Memola/Canvas/RenderPasses/PhotoRenderPass.swift @@ -26,6 +26,15 @@ class PhotoRenderPass: RenderPass { @discardableResult func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool { + draw(into: commandBuffer, on: canvas, with: renderer, isPreview: false) + } + + @discardableResult + func drawPreview(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool { + draw(into: commandBuffer, on: canvas, with: renderer, isPreview: true) + } + + private func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer, isPreview: Bool) -> Bool { guard let elementGroup else { return false } guard let descriptor else { return false } @@ -42,7 +51,11 @@ class PhotoRenderPass: RenderPass { guard let photoPipelineState else { return false } renderEncoder.setRenderPipelineState(photoPipelineState) - canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + if isPreview { + canvas.setPreviewUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + } else { + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + } for photo in photos { photo.draw(device: renderer.device, renderEncoder: renderEncoder) diff --git a/Memola/Canvas/RenderPasses/PreviewRenderPass.swift b/Memola/Canvas/RenderPasses/PreviewRenderPass.swift new file mode 100644 index 0000000..d263eb5 --- /dev/null +++ b/Memola/Canvas/RenderPasses/PreviewRenderPass.swift @@ -0,0 +1,118 @@ +// +// PreviewRenderPass.swift +// Memola +// +// Created by Dscyre Scotti on 7/4/24. +// + +import MetalKit +import Foundation + +final class PreviewRenderPass: RenderPass { + var label: String = "Preview Render Pass" + + var descriptor: MTLRenderPassDescriptor? + var previewPipelineState: MTLRenderPipelineState? + var previewTexture: MTLTexture? + + weak var photoRenderPass: PhotoRenderPass? + weak var strokeRenderPass: StrokeRenderPass? + weak var eraserRenderPass: EraserRenderPass? + + init(renderer: Renderer) { + descriptor = MTLRenderPassDescriptor() + previewPipelineState = renderer.graphicRenderPass.graphicPipelineState + } + + func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { } + + @discardableResult + func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool { + let tree = canvas.graphicContext.tree + if !tree.isEmpty { + var elementGroups: [ElementGroup] = [] + let start = Date.now.timeIntervalSince1970 * 1000 + var bounds: [CGFloat] = [] + for _element in tree.traverse() { + if bounds.isEmpty { + bounds = [ + _element.box.minX, + _element.box.minY, + _element.box.maxX, + _element.box.maxY + ] + } else { + bounds = [ + min(_element.box.minX, bounds[0]), + min(_element.box.minY, bounds[1]), + max(_element.box.maxX, bounds[2]), + max(_element.box.maxY, bounds[3]) + ] + } + if elementGroups.isEmpty { + let _elementGroup = ElementGroup(_element) + elementGroups.append(_elementGroup) + } else { + guard let _elementGroup = elementGroups.last else { continue } + if _elementGroup.isSameElement(_element) { + _elementGroup.add(_element) + } else { + let _elementGroup = ElementGroup(_element) + elementGroups.append(_elementGroup) + } + } + } + let origin = CGPoint(x: bounds[0], y: bounds[1]) + let size = CGSize(width: bounds[2] - bounds[0], height: bounds[3] - bounds[1]) + previewTexture = createPreviewTexture(for: size, with: renderer) + descriptor?.colorAttachments[0].texture = previewTexture + descriptor?.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) + descriptor?.colorAttachments[0].storeAction = .store + descriptor?.colorAttachments[0].loadAction = .clear + canvas.updatePreviewTransform(to: CGRect(origin: origin, size: size)) + for elementGroup in elementGroups { + draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer) + descriptor?.colorAttachments[0].loadAction = .load + } + let end = Date.now.timeIntervalSince1970 * 1000 + NSLog("[Memola] - preview duration: \(end - start)") + } + + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + return true + } + + private func createPreviewTexture(for size: CGSize, with renderer: Renderer) -> MTLTexture? { + let ratio = size.width / size.height + let dimension: CGFloat = 800 + let width: CGFloat + let height: CGFloat + if dimension * ratio > dimension { + height = dimension + width = dimension * ratio + } else { + height = dimension / ratio + width = dimension + } + return Textures.createPreviewTexture(from: renderer, size: CGSize(width: width, height: height)) + } + + private func draw(for elementGroup: ElementGroup, into commandBuffer: MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { + switch elementGroup.type { + case .stroke: + strokeRenderPass?.elementGroup = elementGroup + strokeRenderPass?.graphicDescriptor = descriptor + strokeRenderPass?.graphicPipelineState = previewPipelineState + strokeRenderPass?.drawPreview(into: commandBuffer, on: canvas, with: renderer) + case .eraser: + eraserRenderPass?.elementGroup = elementGroup + eraserRenderPass?.descriptor = descriptor + eraserRenderPass?.drawPreview(into: commandBuffer, on: canvas, with: renderer) + case .photo: + photoRenderPass?.elementGroup = elementGroup + photoRenderPass?.descriptor = descriptor + photoRenderPass?.drawPreview(into: commandBuffer, on: canvas, with: renderer) + } + } +} diff --git a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift index de89aa5..f319616 100644 --- a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift +++ b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift @@ -36,6 +36,15 @@ class StrokeRenderPass: RenderPass { @discardableResult func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool { + draw(into: commandBuffer, on: canvas, with: renderer, isPreview: false) + } + + @discardableResult + func drawPreview(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool { + draw(into: commandBuffer, on: canvas, with: renderer, isPreview: true) + } + + private func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer, isPreview: Bool) -> Bool { guard let elementGroup else { return false } guard let descriptor else { return false } @@ -81,7 +90,11 @@ class StrokeRenderPass: RenderPass { guard let strokePipelineState else { return false } renderEncoder.setRenderPipelineState(strokePipelineState) - canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + if isPreview { + canvas.setPreviewUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + } else { + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + } if let penStyle = penStroke?.penStyle, let indexBuffer { if penStyle.textureName != nil { @@ -126,7 +139,12 @@ class StrokeRenderPass: RenderPass { renderEncoder.setRenderPipelineState(eraserPipelineState) - canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + if isPreview { + canvas.setPreviewUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + } else { + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + } + if let erasedIndexBuffer { renderEncoder.setVertexBuffer(erasedVertexBuffer, offset: 0, index: 0) renderEncoder.drawIndexedPrimitives( diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 6ec4d48..3875932 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -33,6 +33,7 @@ class CanvasViewController: UIViewController { self.drawingView = DrawingView(tool: tool, canvas: canvas, history: history) self.renderer = Renderer(canvasView: drawingView.renderView) super.init(nibName: nil, bundle: nil) + self.canvas.renderer = renderer } required init?(coder: NSCoder) { diff --git a/Memola/Extensions/MTLTexture++.swift b/Memola/Extensions/MTLTexture++.swift new file mode 100644 index 0000000..dbc4e45 --- /dev/null +++ b/Memola/Extensions/MTLTexture++.swift @@ -0,0 +1,48 @@ +// +// MTLTexture++.swift +// Memola +// +// Created by Dscyre Scotti on 7/4/24. +// + +import MetalKit +import Foundation + +extension MTLTexture { + private func bytes() -> UnsafeMutableRawPointer { + let width = self.width + let height = self.height + let rowBytes = self.width * 4 + let p = malloc(width * height * 4)! + getBytes(p, bytesPerRow: rowBytes, from: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0) + return p + } + + func getImage() -> CGImage? { + let bytes = self.bytes() + let pColorSpace = CGColorSpaceCreateDeviceRGB() + let rawBitmapInfo = CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue + let bitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo) + + let selftureSize = self.width * self.height * 4 + let rowBytes = self.width * 4 + if let provider = CGDataProvider(dataInfo: nil, data: bytes, size: selftureSize, releaseData: { _, p, _ in + p.deallocate() + }) { + return CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: rowBytes, + space: pColorSpace, + bitmapInfo: bitmapInfo, + provider: provider, + decode: nil, + shouldInterpolate: true, + intent: CGColorRenderingIntent.defaultIntent + ) + } + return nil + } +} diff --git a/Memola/Features/Dashboard/Details/Memos/MemosView.swift b/Memola/Features/Dashboard/Details/Memos/MemosView.swift index c931e3b..d82b122 100644 --- a/Memola/Features/Dashboard/Details/Memos/MemosView.swift +++ b/Memola/Features/Dashboard/Details/Memos/MemosView.swift @@ -42,8 +42,8 @@ struct MemosView: View { } var body: some View { - MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in - memoCard(memoObject) + MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject, cellWidth in + memoCard(memoObject, cellWidth) } .navigationTitle(horizontalSizeClass == .compact ? "Memos" : "") .navigationBarTitleDisplayMode(.inline) @@ -130,8 +130,8 @@ struct MemosView: View { } } - func memoCard(_ memoObject: MemoObject) -> some View { - MemoCard(memoObject: memoObject) { card in + func memoCard(_ memoObject: MemoObject, _ cellWidth: CGFloat) -> some View { + MemoCard(memoObject: memoObject, cellWidth: cellWidth) { card in card .contextMenu { Button { diff --git a/Memola/Features/Dashboard/Details/Shared/MemoCard.swift b/Memola/Features/Dashboard/Details/Shared/MemoCard.swift index fb24331..9f75ee5 100644 --- a/Memola/Features/Dashboard/Details/Shared/MemoCard.swift +++ b/Memola/Features/Dashboard/Details/Shared/MemoCard.swift @@ -9,11 +9,13 @@ import SwiftUI struct MemoCard: View { let memoObject: MemoObject + let cellWidth: CGFloat let modifyPreview: ((MemoPreview) -> Preview)? let details: () -> Detail - init(memoObject: MemoObject, @ViewBuilder modifyPreview: @escaping (MemoPreview) -> Preview, @ViewBuilder details: @escaping () -> Detail) { + init(memoObject: MemoObject, cellWidth: CGFloat, @ViewBuilder modifyPreview: @escaping (MemoPreview) -> Preview, @ViewBuilder details: @escaping () -> Detail) { self.memoObject = memoObject + self.cellWidth = cellWidth self.modifyPreview = modifyPreview self.details = details } @@ -21,9 +23,9 @@ struct MemoCard: View { var body: some View { VStack(alignment: .leading, spacing: 5) { if let modifyPreview { - modifyPreview(MemoPreview()) + modifyPreview(MemoPreview(preview: memoObject.preview, cellWidth: cellWidth)) } else { - MemoPreview() + MemoPreview(preview: memoObject.preview, cellWidth: cellWidth) } VStack(alignment: .leading, spacing: 2) { Text(memoObject.title) diff --git a/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift b/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift index e443f53..5b54b88 100644 --- a/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift +++ b/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift @@ -11,9 +11,9 @@ struct MemoGrid: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass let memoObjects: FetchedResults let placeholder: Placeholder.Info - @ViewBuilder let card: (MemoObject) -> Card + @ViewBuilder let card: (MemoObject, CGFloat) -> Card - var cellWidth: CGFloat { + var maxCellWidth: CGFloat { if horizontalSizeClass == .compact { return 180 } @@ -21,21 +21,27 @@ struct MemoGrid: View { } var body: some View { - if memoObjects.isEmpty { - Placeholder(info: placeholder) - } else { - GeometryReader { proxy in - let count = Int(proxy.size.width / cellWidth) - let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 15), count: count) - ScrollView { - LazyVGrid(columns: columns, spacing: 15) { - ForEach(memoObjects) { memoObject in - card(memoObject) + Group { + if memoObjects.isEmpty { + Placeholder(info: placeholder) + } else { + GeometryReader { proxy in + let spacing: CGFloat = 15 + let padding: CGFloat = 20 + let count = Int(proxy.size.width / maxCellWidth) + let cellWidth = (proxy.size.width - spacing * CGFloat(count - 2) - padding * 2.0) / CGFloat(count) + let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: spacing), count: count) + ScrollView { + LazyVGrid(columns: columns, spacing: spacing) { + ForEach(memoObjects) { memoObject in + card(memoObject, cellWidth) + } } + .padding(padding) } - .padding() } } } + .background(Color(uiColor: .secondarySystemBackground)) } } diff --git a/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift b/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift index 6ec7f01..b978ae8 100644 --- a/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift +++ b/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift @@ -10,6 +10,8 @@ import SwiftUI struct MemoPreview: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass + let preview: Data? + let cellWidth: CGFloat var cellHeight: CGFloat { if horizontalSizeClass == .compact { return 120 @@ -18,8 +20,17 @@ struct MemoPreview: View { } var body: some View { - Rectangle() - .frame(height: cellHeight) - .clipShape(RoundedRectangle(cornerRadius: 10)) + Group { + if let preview, let previewImage = UIImage(data: preview) { + Image(uiImage: previewImage) + .resizable() + .aspectRatio(contentMode: .fill) + } else { + Rectangle() + .fill(.white) + } + } + .frame(width: cellWidth, height: cellHeight) + .clipShape(RoundedRectangle(cornerRadius: 10)) } } diff --git a/Memola/Features/Dashboard/Details/Trash/TrashView.swift b/Memola/Features/Dashboard/Details/Trash/TrashView.swift index 5deff1a..815e068 100644 --- a/Memola/Features/Dashboard/Details/Trash/TrashView.swift +++ b/Memola/Features/Dashboard/Details/Trash/TrashView.swift @@ -42,9 +42,9 @@ struct TrashView: View { } set: { _ in deletedMemo = nil } - MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in - memoCard(memoObject) - } + MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject, cellWidth in + memoCard(memoObject, cellWidth) + } .navigationTitle(horizontalSizeClass == .compact ? "Trash" : "") .navigationBarTitleDisplayMode(.inline) .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) @@ -87,8 +87,8 @@ struct TrashView: View { } } - func memoCard(_ memoObject: MemoObject) -> some View { - MemoCard(memoObject: memoObject) { card in + func memoCard(_ memoObject: MemoObject, _ cellWidth: CGFloat) -> some View { + MemoCard(memoObject: memoObject, cellWidth: cellWidth) { card in card .contextMenu { Button { diff --git a/Memola/Features/Dashboard/Sidebar/Sidebar.swift b/Memola/Features/Dashboard/Sidebar/Sidebar.swift index 99ba758..06eb780 100644 --- a/Memola/Features/Dashboard/Sidebar/Sidebar.swift +++ b/Memola/Features/Dashboard/Sidebar/Sidebar.swift @@ -38,7 +38,7 @@ struct Sidebar: View { .listStyle(.sidebar) .navigationTitle(horizontalSizeClass == .compact ? "Memola" : "") .scrollContentBackground(.hidden) - .background(Color(uiColor: .secondarySystemFill)) + .background(Color(uiColor: .secondarySystemBackground)) .navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250) .navigationBarTitleDisplayMode(horizontalSizeClass == .compact ? .automatic : .inline) } diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 9b9531c..1f41fbd 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -148,21 +148,8 @@ struct Toolbar: View { } func closeMemo() { - withAnimation { - canvas.state = .closing - } - withPersistenceSync(\.viewContext) { context in - try context.saveIfNeeded() - } - withPersistence(\.backgroundContext) { context in - try context.saveIfNeeded() - context.refreshAllObjects() - DispatchQueue.main.async { - withAnimation { - canvas.state = .closed - } - dismiss() - } + canvas.save(for: memo) { + dismiss() } } } diff --git a/Memola/Persistence/Objects/MemoObject.swift b/Memola/Persistence/Objects/MemoObject.swift index c62d8f4..73c2f42 100644 --- a/Memola/Persistence/Objects/MemoObject.swift +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -17,6 +17,7 @@ final class MemoObject: NSManagedObject, Identifiable { @NSManaged var deletedAt: Date? @NSManaged var isFavorite: Bool @NSManaged var isTrash: Bool + @NSManaged var preview: Data? @NSManaged var tool: ToolObject @NSManaged var canvas: CanvasObject } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index e043833..317085d 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -32,6 +32,7 @@ +