mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-23 09:51:18 +01:00
feat: add memo preview generation
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVertex.swift; sourceTree = "<group>"; };
|
||||
ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToolbar.swift; sourceTree = "<group>"; };
|
||||
ECDDD40C2C366B3B00DF9D5E /* PreviewRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewRenderPass.swift; sourceTree = "<group>"; };
|
||||
ECDDD40E2C368B2700DF9D5E /* MTLTexture++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLTexture++.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>"; };
|
||||
@@ -629,6 +633,7 @@
|
||||
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */,
|
||||
ECC995A42C1EB4CC00B2699A /* Data++.swift */,
|
||||
EC18150E2C2DB13200541369 /* Date++.swift */,
|
||||
ECDDD40E2C368B2700DF9D5E /* MTLTexture++.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -738,6 +743,7 @@
|
||||
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
|
||||
ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */,
|
||||
EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */,
|
||||
ECDDD40C2C366B3B00DF9D5E /* PreviewRenderPass.swift */,
|
||||
);
|
||||
path = RenderPasses;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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<CGFloat, Never>()
|
||||
|
||||
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<Uniforms>.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<Uniforms>.size)
|
||||
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
118
Memola/Canvas/RenderPasses/PreviewRenderPass.swift
Normal file
118
Memola/Canvas/RenderPasses/PreviewRenderPass.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
48
Memola/Extensions/MTLTexture++.swift
Normal file
48
Memola/Extensions/MTLTexture++.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,11 +9,13 @@ import SwiftUI
|
||||
|
||||
struct MemoCard<Preview: View, Detail: View>: 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<Preview: View, Detail: View>: 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)
|
||||
|
||||
@@ -11,9 +11,9 @@ struct MemoGrid<Card: View>: View {
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
let memoObjects: FetchedResults<MemoObject>
|
||||
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<Card: View>: 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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isFavorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isTrash" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="preview" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
|
||||
|
||||
Reference in New Issue
Block a user