mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-04-25 01:58:52 +02: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 */; };
|
ECD12A932C1B062000B96E12 /* Photo.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A922C1B062000B96E12 /* Photo.metal */; };
|
||||||
ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */; };
|
ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */; };
|
||||||
ECDAC07B2C318DBC0000ED77 /* ElementToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDAC07A2C318DBC0000ED77 /* ElementToolbar.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 */; };
|
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; };
|
||||||
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
|
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
|
||||||
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = "<group>"; };
|
||||||
@@ -629,6 +633,7 @@
|
|||||||
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */,
|
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */,
|
||||||
ECC995A42C1EB4CC00B2699A /* Data++.swift */,
|
ECC995A42C1EB4CC00B2699A /* Data++.swift */,
|
||||||
EC18150E2C2DB13200541369 /* Date++.swift */,
|
EC18150E2C2DB13200541369 /* Date++.swift */,
|
||||||
|
ECDDD40E2C368B2700DF9D5E /* MTLTexture++.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -738,6 +743,7 @@
|
|||||||
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
|
ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */,
|
||||||
ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */,
|
ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */,
|
||||||
EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */,
|
EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */,
|
||||||
|
ECDDD40C2C366B3B00DF9D5E /* PreviewRenderPass.swift */,
|
||||||
);
|
);
|
||||||
path = RenderPasses;
|
path = RenderPasses;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -991,6 +997,7 @@
|
|||||||
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */,
|
||||||
EC1815082C2D980B00541369 /* Sort.swift in Sources */,
|
EC1815082C2D980B00541369 /* Sort.swift in Sources */,
|
||||||
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
|
ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */,
|
||||||
|
ECDDD40D2C366B3B00DF9D5E /* PreviewRenderPass.swift in Sources */,
|
||||||
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
|
ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */,
|
||||||
ECA738CD2BE60F2F00A4542E /* PointGridContext.swift in Sources */,
|
ECA738CD2BE60F2F00A4542E /* PointGridContext.swift in Sources */,
|
||||||
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
|
ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */,
|
||||||
@@ -1067,6 +1074,7 @@
|
|||||||
EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */,
|
EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */,
|
||||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||||
EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */,
|
EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */,
|
||||||
|
ECDDD40F2C368B2700DF9D5E /* MTLTexture++.swift in Sources */,
|
||||||
ECC995A52C1EB4CC00B2699A /* Data++.swift in Sources */,
|
ECC995A52C1EB4CC00B2699A /* Data++.swift in Sources */,
|
||||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||||
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */,
|
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
|
|||||||
let defaultZoomScale: CGFloat = 20
|
let defaultZoomScale: CGFloat = 20
|
||||||
|
|
||||||
var transform: simd_float4x4 = .init()
|
var transform: simd_float4x4 = .init()
|
||||||
|
var previewTransform: simd_float4x4 = .init()
|
||||||
var clipBounds: CGRect = .zero
|
var clipBounds: CGRect = .zero
|
||||||
var bounds: CGRect = .zero
|
var bounds: CGRect = .zero
|
||||||
var uniformsBuffer: MTLBuffer?
|
var uniformsBuffer: MTLBuffer?
|
||||||
@@ -37,6 +38,8 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
|
|||||||
|
|
||||||
let zoomPublisher = PassthroughSubject<CGFloat, Never>()
|
let zoomPublisher = PassthroughSubject<CGFloat, Never>()
|
||||||
|
|
||||||
|
weak var renderer: Renderer?
|
||||||
|
|
||||||
init(size: CGSize, canvasID: NSManagedObjectID, gridMode: Int16) {
|
init(size: CGSize, canvasID: NSManagedObjectID, gridMode: Int16) {
|
||||||
self.size = size
|
self.size = size
|
||||||
self.canvasID = canvasID
|
self.canvasID = canvasID
|
||||||
@@ -78,6 +81,23 @@ extension Canvas {
|
|||||||
context.refreshAllObjects()
|
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
|
// MARK: - Dimension
|
||||||
@@ -92,6 +112,29 @@ extension Canvas {
|
|||||||
self.transform = simd_float4x4(transform1 * transform2 * transform3)
|
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) {
|
func updateClipBounds(_ scrollView: UIScrollView, on drawingView: DrawingView) {
|
||||||
let ratio = drawingView.ratio
|
let ratio = drawingView.ratio
|
||||||
let bounds = scrollView.convert(scrollView.bounds, to: drawingView)
|
let bounds = scrollView.convert(scrollView.bounds, to: drawingView)
|
||||||
@@ -197,6 +240,12 @@ extension Canvas {
|
|||||||
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
||||||
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
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
|
// MARK: - State
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ final class Renderer {
|
|||||||
lazy var photoBackgroundRenderPass: PhotoBackgroundRenderPass = {
|
lazy var photoBackgroundRenderPass: PhotoBackgroundRenderPass = {
|
||||||
PhotoBackgroundRenderPass(renderer: self)
|
PhotoBackgroundRenderPass(renderer: self)
|
||||||
}()
|
}()
|
||||||
|
lazy var previewRenderPass: PreviewRenderPass = {
|
||||||
|
PreviewRenderPass(renderer: self)
|
||||||
|
}()
|
||||||
|
|
||||||
init(canvasView: MTKView) {
|
init(canvasView: MTKView) {
|
||||||
guard let device = MTLCreateSystemDefaultDevice() else {
|
guard let device = MTLCreateSystemDefaultDevice() else {
|
||||||
@@ -106,4 +109,21 @@ final class Renderer {
|
|||||||
viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture
|
viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture
|
||||||
viewPortRenderPass.draw(into: commandBuffer, on: canvas, with: self)
|
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"
|
texture.label = "Photo Background Texture"
|
||||||
return 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 {
|
var elementGroupType: ElementGroup.ElementGroupType {
|
||||||
switch self {
|
switch self {
|
||||||
case .stroke(let anyStroke):
|
case .stroke(let anyStroke):
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ class EraserRenderPass: RenderPass {
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool {
|
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 elementGroup else { return false }
|
||||||
guard let descriptor else { return false }
|
guard let descriptor else { return false }
|
||||||
|
|
||||||
@@ -65,7 +74,11 @@ class EraserRenderPass: RenderPass {
|
|||||||
guard let eraserPipelineState else { return false }
|
guard let eraserPipelineState else { return false }
|
||||||
renderEncoder.setRenderPipelineState(eraserPipelineState)
|
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 {
|
if let indexBuffer {
|
||||||
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class GraphicRenderPass: RenderPass {
|
|||||||
draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer)
|
draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer)
|
||||||
}
|
}
|
||||||
let end = Date.now.timeIntervalSince1970 * 1000
|
let end = Date.now.timeIntervalSince1970 * 1000
|
||||||
NSLog("[Memola] - duration: \(end - start)")
|
NSLog("[Memola] - graphic duration: \(end - start)")
|
||||||
renderer.redrawsGraphicRender = false
|
renderer.redrawsGraphicRender = false
|
||||||
}
|
}
|
||||||
if let element = graphicContext.previousElement {
|
if let element = graphicContext.previousElement {
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ class PhotoRenderPass: RenderPass {
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool {
|
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 elementGroup else { return false }
|
||||||
guard let descriptor else { return false }
|
guard let descriptor else { return false }
|
||||||
|
|
||||||
@@ -42,7 +51,11 @@ class PhotoRenderPass: RenderPass {
|
|||||||
guard let photoPipelineState else { return false }
|
guard let photoPipelineState else { return false }
|
||||||
renderEncoder.setRenderPipelineState(photoPipelineState)
|
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 {
|
for photo in photos {
|
||||||
photo.draw(device: renderer.device, renderEncoder: renderEncoder)
|
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
|
@discardableResult
|
||||||
func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) -> Bool {
|
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 elementGroup else { return false }
|
||||||
guard let descriptor else { return false }
|
guard let descriptor else { return false }
|
||||||
|
|
||||||
@@ -81,7 +90,11 @@ class StrokeRenderPass: RenderPass {
|
|||||||
guard let strokePipelineState else { return false }
|
guard let strokePipelineState else { return false }
|
||||||
renderEncoder.setRenderPipelineState(strokePipelineState)
|
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 let penStyle = penStroke?.penStyle, let indexBuffer {
|
||||||
if penStyle.textureName != nil {
|
if penStyle.textureName != nil {
|
||||||
@@ -126,7 +139,12 @@ class StrokeRenderPass: RenderPass {
|
|||||||
|
|
||||||
renderEncoder.setRenderPipelineState(eraserPipelineState)
|
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 {
|
if let erasedIndexBuffer {
|
||||||
renderEncoder.setVertexBuffer(erasedVertexBuffer, offset: 0, index: 0)
|
renderEncoder.setVertexBuffer(erasedVertexBuffer, offset: 0, index: 0)
|
||||||
renderEncoder.drawIndexedPrimitives(
|
renderEncoder.drawIndexedPrimitives(
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class CanvasViewController: UIViewController {
|
|||||||
self.drawingView = DrawingView(tool: tool, canvas: canvas, history: history)
|
self.drawingView = DrawingView(tool: tool, canvas: canvas, history: history)
|
||||||
self.renderer = Renderer(canvasView: drawingView.renderView)
|
self.renderer = Renderer(canvasView: drawingView.renderView)
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
self.canvas.renderer = renderer
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
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 {
|
var body: some View {
|
||||||
MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in
|
MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject, cellWidth in
|
||||||
memoCard(memoObject)
|
memoCard(memoObject, cellWidth)
|
||||||
}
|
}
|
||||||
.navigationTitle(horizontalSizeClass == .compact ? "Memos" : "")
|
.navigationTitle(horizontalSizeClass == .compact ? "Memos" : "")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@@ -130,8 +130,8 @@ struct MemosView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func memoCard(_ memoObject: MemoObject) -> some View {
|
func memoCard(_ memoObject: MemoObject, _ cellWidth: CGFloat) -> some View {
|
||||||
MemoCard(memoObject: memoObject) { card in
|
MemoCard(memoObject: memoObject, cellWidth: cellWidth) { card in
|
||||||
card
|
card
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import SwiftUI
|
|||||||
|
|
||||||
struct MemoCard<Preview: View, Detail: View>: View {
|
struct MemoCard<Preview: View, Detail: View>: View {
|
||||||
let memoObject: MemoObject
|
let memoObject: MemoObject
|
||||||
|
let cellWidth: CGFloat
|
||||||
let modifyPreview: ((MemoPreview) -> Preview)?
|
let modifyPreview: ((MemoPreview) -> Preview)?
|
||||||
let details: () -> Detail
|
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.memoObject = memoObject
|
||||||
|
self.cellWidth = cellWidth
|
||||||
self.modifyPreview = modifyPreview
|
self.modifyPreview = modifyPreview
|
||||||
self.details = details
|
self.details = details
|
||||||
}
|
}
|
||||||
@@ -21,9 +23,9 @@ struct MemoCard<Preview: View, Detail: View>: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
if let modifyPreview {
|
if let modifyPreview {
|
||||||
modifyPreview(MemoPreview())
|
modifyPreview(MemoPreview(preview: memoObject.preview, cellWidth: cellWidth))
|
||||||
} else {
|
} else {
|
||||||
MemoPreview()
|
MemoPreview(preview: memoObject.preview, cellWidth: cellWidth)
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(memoObject.title)
|
Text(memoObject.title)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ struct MemoGrid<Card: View>: View {
|
|||||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
let memoObjects: FetchedResults<MemoObject>
|
let memoObjects: FetchedResults<MemoObject>
|
||||||
let placeholder: Placeholder.Info
|
let placeholder: Placeholder.Info
|
||||||
@ViewBuilder let card: (MemoObject) -> Card
|
@ViewBuilder let card: (MemoObject, CGFloat) -> Card
|
||||||
|
|
||||||
var cellWidth: CGFloat {
|
var maxCellWidth: CGFloat {
|
||||||
if horizontalSizeClass == .compact {
|
if horizontalSizeClass == .compact {
|
||||||
return 180
|
return 180
|
||||||
}
|
}
|
||||||
@@ -21,21 +21,27 @@ struct MemoGrid<Card: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if memoObjects.isEmpty {
|
Group {
|
||||||
Placeholder(info: placeholder)
|
if memoObjects.isEmpty {
|
||||||
} else {
|
Placeholder(info: placeholder)
|
||||||
GeometryReader { proxy in
|
} else {
|
||||||
let count = Int(proxy.size.width / cellWidth)
|
GeometryReader { proxy in
|
||||||
let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 15), count: count)
|
let spacing: CGFloat = 15
|
||||||
ScrollView {
|
let padding: CGFloat = 20
|
||||||
LazyVGrid(columns: columns, spacing: 15) {
|
let count = Int(proxy.size.width / maxCellWidth)
|
||||||
ForEach(memoObjects) { memoObject in
|
let cellWidth = (proxy.size.width - spacing * CGFloat(count - 2) - padding * 2.0) / CGFloat(count)
|
||||||
card(memoObject)
|
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 {
|
struct MemoPreview: View {
|
||||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
|
|
||||||
|
let preview: Data?
|
||||||
|
let cellWidth: CGFloat
|
||||||
var cellHeight: CGFloat {
|
var cellHeight: CGFloat {
|
||||||
if horizontalSizeClass == .compact {
|
if horizontalSizeClass == .compact {
|
||||||
return 120
|
return 120
|
||||||
@@ -18,8 +20,17 @@ struct MemoPreview: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Rectangle()
|
Group {
|
||||||
.frame(height: cellHeight)
|
if let preview, let previewImage = UIImage(data: preview) {
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
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
|
} set: { _ in
|
||||||
deletedMemo = nil
|
deletedMemo = nil
|
||||||
}
|
}
|
||||||
MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in
|
MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject, cellWidth in
|
||||||
memoCard(memoObject)
|
memoCard(memoObject, cellWidth)
|
||||||
}
|
}
|
||||||
.navigationTitle(horizontalSizeClass == .compact ? "Trash" : "")
|
.navigationTitle(horizontalSizeClass == .compact ? "Trash" : "")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.searchable(text: $query, placement: .toolbar, prompt: Text("Search"))
|
.searchable(text: $query, placement: .toolbar, prompt: Text("Search"))
|
||||||
@@ -87,8 +87,8 @@ struct TrashView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func memoCard(_ memoObject: MemoObject) -> some View {
|
func memoCard(_ memoObject: MemoObject, _ cellWidth: CGFloat) -> some View {
|
||||||
MemoCard(memoObject: memoObject) { card in
|
MemoCard(memoObject: memoObject, cellWidth: cellWidth) { card in
|
||||||
card
|
card
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ struct Sidebar: View {
|
|||||||
.listStyle(.sidebar)
|
.listStyle(.sidebar)
|
||||||
.navigationTitle(horizontalSizeClass == .compact ? "Memola" : "")
|
.navigationTitle(horizontalSizeClass == .compact ? "Memola" : "")
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color(uiColor: .secondarySystemFill))
|
.background(Color(uiColor: .secondarySystemBackground))
|
||||||
.navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250)
|
.navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250)
|
||||||
.navigationBarTitleDisplayMode(horizontalSizeClass == .compact ? .automatic : .inline)
|
.navigationBarTitleDisplayMode(horizontalSizeClass == .compact ? .automatic : .inline)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,21 +148,8 @@ struct Toolbar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeMemo() {
|
func closeMemo() {
|
||||||
withAnimation {
|
canvas.save(for: memo) {
|
||||||
canvas.state = .closing
|
dismiss()
|
||||||
}
|
|
||||||
withPersistenceSync(\.viewContext) { context in
|
|
||||||
try context.saveIfNeeded()
|
|
||||||
}
|
|
||||||
withPersistence(\.backgroundContext) { context in
|
|
||||||
try context.saveIfNeeded()
|
|
||||||
context.refreshAllObjects()
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
withAnimation {
|
|
||||||
canvas.state = .closed
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ final class MemoObject: NSManagedObject, Identifiable {
|
|||||||
@NSManaged var deletedAt: Date?
|
@NSManaged var deletedAt: Date?
|
||||||
@NSManaged var isFavorite: Bool
|
@NSManaged var isFavorite: Bool
|
||||||
@NSManaged var isTrash: Bool
|
@NSManaged var isTrash: Bool
|
||||||
|
@NSManaged var preview: Data?
|
||||||
@NSManaged var tool: ToolObject
|
@NSManaged var tool: ToolObject
|
||||||
@NSManaged var canvas: CanvasObject
|
@NSManaged var canvas: CanvasObject
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="isFavorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="isFavorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="isTrash" 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="title" attributeType="String"/>
|
||||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
|
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="CanvasObject" inverseName="memo" inverseEntity="CanvasObject"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user