mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-20 08:34:05 +01:00
Merge branch 'feature/quad-entity' into feature/memory
# Conflicts: # Memola/Canvas/Core/Canvas.swift # Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift # Memola/Canvas/Geometries/Stroke/Stroke.swift
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; };
|
||||
EC771E602BEB6EE50053CC68 /* QuadValueTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */; };
|
||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
|
||||
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
|
||||
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; };
|
||||
@@ -63,16 +62,15 @@
|
||||
ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; };
|
||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; };
|
||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; };
|
||||
ECA738F82BE612EB00A4542E /* StrokeQuad.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F72BE612EB00A4542E /* StrokeQuad.swift */; };
|
||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; };
|
||||
ECA739012BE61D9C00A4542E /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FF2BE61D9C00A4542E /* MemolaModel.xcdatamodeld */; };
|
||||
ECA739052BE61E3100A4542E /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739042BE61E3100A4542E /* Memo.swift */; };
|
||||
ECA739082BE623F300A4542E /* PenToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenToolView.swift */; };
|
||||
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
|
||||
EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadValueTransformer.swift; sourceTree = "<group>"; };
|
||||
EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = "<group>"; };
|
||||
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -128,11 +126,11 @@
|
||||
ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = "<group>"; };
|
||||
ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = "<group>"; };
|
||||
ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = "<group>"; };
|
||||
ECA738F72BE612EB00A4542E /* StrokeQuad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeQuad.swift; sourceTree = "<group>"; };
|
||||
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
|
||||
ECA739002BE61D9C00A4542E /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
|
||||
ECA739042BE61E3100A4542E /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = "<group>"; };
|
||||
ECA739072BE623F300A4542E /* PenToolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenToolView.swift; sourceTree = "<group>"; };
|
||||
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -162,14 +160,6 @@
|
||||
path = ViewController;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC771E5C2BEB37FC0053CC68 /* Transformers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC771E5F2BEB6EE50053CC68 /* QuadValueTransformer.swift */,
|
||||
);
|
||||
path = Transformers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC7F6BDF2BE5E6E300A34A7B = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -304,8 +294,8 @@
|
||||
ECA738982BE6015700A4542E /* Primitives */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738F72BE612EB00A4542E /* StrokeQuad.swift */,
|
||||
EC4538882BEBCAE000A86FEC /* Quad.swift */,
|
||||
ECEC01A72BEE11BA006DA24C /* QuadShape.swift */,
|
||||
);
|
||||
path = Primitives;
|
||||
sourceTree = "<group>";
|
||||
@@ -463,7 +453,6 @@
|
||||
ECA738FA2BE61B1700A4542E /* Persistence */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC771E5C2BEB37FC0053CC68 /* Transformers */,
|
||||
ECA739022BE61DE700A4542E /* Core */,
|
||||
);
|
||||
path = Persistence;
|
||||
@@ -572,7 +561,6 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EC771E602BEB6EE50053CC68 /* QuadValueTransformer.swift in Sources */,
|
||||
ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */,
|
||||
ECA738E42BE6110800A4542E /* Drawable.swift in Sources */,
|
||||
ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */,
|
||||
@@ -594,6 +582,7 @@
|
||||
ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */,
|
||||
ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */,
|
||||
ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */,
|
||||
ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */,
|
||||
ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */,
|
||||
ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */,
|
||||
ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */,
|
||||
@@ -628,7 +617,6 @@
|
||||
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
|
||||
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
|
||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||
ECA738F82BE612EB00A4542E /* StrokeQuad.swift in Sources */,
|
||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ struct MemolaApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MemosView()
|
||||
.environment(\.managedObjectContext, Persistence.shared.viewContext)
|
||||
.environment(\.managedObjectContext, Persistence.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,7 @@ final class GraphicContext: NSManagedObject {
|
||||
strokes.remove(stroke)
|
||||
stroke.graphicContext = nil
|
||||
previousStroke = nil
|
||||
do {
|
||||
try Persistence.shared.viewContext.save()
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
}
|
||||
Persistence.saveIfNeeded()
|
||||
}
|
||||
|
||||
func redoGraphic(for event: HistoryEvent) {
|
||||
@@ -58,11 +54,7 @@ final class GraphicContext: NSManagedObject {
|
||||
stroke.graphicContext = self
|
||||
previousStroke = nil
|
||||
}
|
||||
do {
|
||||
try Persistence.shared.viewContext.save()
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
}
|
||||
Persistence.saveIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,13 +76,13 @@ extension GraphicContext: Drawable {
|
||||
|
||||
extension GraphicContext {
|
||||
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
|
||||
let stroke = Stroke(context: Persistence.shared.viewContext)
|
||||
let stroke = Stroke(context: Persistence.context)
|
||||
stroke.id = UUID()
|
||||
stroke.color = pen.color
|
||||
stroke.style = pen.strokeStyle.rawValue
|
||||
stroke.thickness = pen.thickness
|
||||
stroke.createdAt = .now
|
||||
stroke.strokeQuads = []
|
||||
stroke.quads = []
|
||||
stroke.graphicContext = self
|
||||
strokes.add(stroke)
|
||||
currentStroke = stroke
|
||||
@@ -109,12 +101,7 @@ extension GraphicContext {
|
||||
func endStroke(at point: CGPoint) {
|
||||
guard currentPoint != nil, let currentStroke else { return }
|
||||
currentStroke.finish(at: point)
|
||||
currentStroke.saveQuads()
|
||||
do {
|
||||
try Persistence.shared.viewContext.save()
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
}
|
||||
Persistence.saveIfNeeded()
|
||||
previousStroke = currentStroke
|
||||
self.currentStroke = nil
|
||||
self.currentPoint = nil
|
||||
@@ -122,14 +109,11 @@ extension GraphicContext {
|
||||
|
||||
func cancelStroke() {
|
||||
if let stroke = strokes.lastObject as? Stroke {
|
||||
do {
|
||||
let viewContext = Persistence.shared.viewContext
|
||||
Persistence.performe { context in
|
||||
strokes.remove(stroke)
|
||||
viewContext.delete(stroke)
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
context.delete(stroke)
|
||||
}
|
||||
Persistence.saveIfNeeded()
|
||||
}
|
||||
currentStroke = nil
|
||||
currentPoint = nil
|
||||
|
||||
@@ -43,26 +43,15 @@ final class Canvas: NSManagedObject, Identifiable {
|
||||
extension Canvas {
|
||||
func load() {
|
||||
let start = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
|
||||
Task(priority: .high) { [start] in
|
||||
await MainActor.run {
|
||||
state = .loading
|
||||
objectWillChange.send()
|
||||
}
|
||||
await withTaskGroup(of: Void.self) { taskGroup in
|
||||
for stroke in graphicContext.strokes {
|
||||
guard let stroke = stroke as? Stroke else { continue }
|
||||
taskGroup.addTask {
|
||||
stroke.loadVertices()
|
||||
}
|
||||
}
|
||||
}
|
||||
let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
|
||||
NSLog("[Memola] - Loaded from \(start) to \(end)")
|
||||
await MainActor.run {
|
||||
state = .loaded
|
||||
objectWillChange.send()
|
||||
for stroke in graphicContext.strokes {
|
||||
if let stroke = stroke as? Stroke {
|
||||
stroke.loadVertices()
|
||||
NSLog("[Memola] - \(stroke.quads.count) quads")
|
||||
}
|
||||
}
|
||||
let end = Date().formatted(.dateTime.minute().second().secondFraction(.fractional(5)))
|
||||
NSLog("[Memola] - Loaded from \(start) to \(end)")
|
||||
state = .loaded
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,37 +5,38 @@
|
||||
// Created by Dscyre Scotti on 5/8/24.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
struct Quad: Codable {
|
||||
var origin: CGPoint
|
||||
var color: [CGFloat]
|
||||
var size: CGFloat
|
||||
var rotation: CGFloat
|
||||
var shape: QuadShape
|
||||
@objc(Quad)
|
||||
class Quad: NSManagedObject {
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var originX: CGFloat
|
||||
@NSManaged var originY: CGFloat
|
||||
@NSManaged var size: CGFloat
|
||||
@NSManaged var rotation: CGFloat
|
||||
@NSManaged var shape: Int16
|
||||
@NSManaged var stroke: Stroke?
|
||||
|
||||
init(origin: CGPoint, size: CGFloat, color: [CGFloat], rotation: CGFloat, shape: QuadShape = .rounded) {
|
||||
self.origin = origin
|
||||
self.size = size
|
||||
self.color = color
|
||||
self.rotation = rotation
|
||||
self.shape = shape
|
||||
}
|
||||
|
||||
func generateVertices() -> [QuadVertex] {
|
||||
switch shape {
|
||||
case .rounded:
|
||||
generateRoundedQuad()
|
||||
case .squared:
|
||||
generateSquaredQuad()
|
||||
case let .calligraphic(vFactor, hFactor):
|
||||
generateCalligraphicQuad(vFactor: vFactor, hFactor: hFactor)
|
||||
case let .trapezoid(topFactor, bottomFactor, heightFactor):
|
||||
generateTrapezoidQuad(topFactor: topFactor, bottomFactor: bottomFactor, heightFactor: heightFactor)
|
||||
var origin: CGPoint {
|
||||
get { CGPoint(x: originX, y: originY) }
|
||||
set {
|
||||
originX = newValue.x
|
||||
originY = newValue.y
|
||||
}
|
||||
}
|
||||
|
||||
func generateRoundedQuad() -> [QuadVertex] {
|
||||
func generateVertices(_ color: [CGFloat]) -> [QuadVertex] {
|
||||
guard let shape = QuadShape.init(rawValue: shape) else { return [] }
|
||||
switch shape {
|
||||
case .rounded:
|
||||
return generateRoundedQuad(color)
|
||||
case .squared:
|
||||
return generateSquaredQuad(color)
|
||||
}
|
||||
}
|
||||
|
||||
func generateRoundedQuad(_ color: [CGFloat]) -> [QuadVertex] {
|
||||
let halfSize = size * 0.5
|
||||
return [
|
||||
QuadVertex(x: origin.x - halfSize, y: origin.y - halfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
@@ -47,7 +48,7 @@ struct Quad: Codable {
|
||||
]
|
||||
}
|
||||
|
||||
func generateSquaredQuad() -> [QuadVertex] {
|
||||
func generateSquaredQuad(_ color: [CGFloat]) -> [QuadVertex] {
|
||||
let vHalfSize = size * 0.5
|
||||
let hHalfSize = size * 0.15
|
||||
return [
|
||||
@@ -59,38 +60,4 @@ struct Quad: Codable {
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||
]
|
||||
}
|
||||
|
||||
func generateCalligraphicQuad(vFactor: CGFloat, hFactor: CGFloat) -> [QuadVertex] {
|
||||
let vHalfSize = size * vFactor * 0.5
|
||||
let hHalfSize = size * hFactor * 0.5
|
||||
return [
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||
]
|
||||
}
|
||||
|
||||
func generateTrapezoidQuad(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat) -> [QuadVertex] {
|
||||
let vHalfSize = size * heightFactor * 0.5
|
||||
let hTopHalfSize = size * topFactor * 0.5
|
||||
let hBottomHalfSize = size * bottomFactor * 0.5
|
||||
return [
|
||||
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 0, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y - vHalfSize, textCoord: CGPoint(x: 1, y: 0), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x - hTopHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 0, y: 1), color: color, origin: origin, rotation: rotation),
|
||||
QuadVertex(x: origin.x + hBottomHalfSize, y: origin.y + vHalfSize, textCoord: CGPoint(x: 1, y: 1), color: color, origin: origin, rotation: rotation)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
enum QuadShape: Codable {
|
||||
case rounded
|
||||
case squared
|
||||
case calligraphic(CGFloat, CGFloat)
|
||||
case trapezoid(topFactor: CGFloat, bottomFactor: CGFloat, heightFactor: CGFloat)
|
||||
}
|
||||
|
||||
13
Memola/Canvas/Geometries/Primitives/QuadShape.swift
Normal file
13
Memola/Canvas/Geometries/Primitives/QuadShape.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// QuadShape.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/10/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum QuadShape: Int16 {
|
||||
case rounded
|
||||
case squared
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// StrokeQuad.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/4/24.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import Foundation
|
||||
|
||||
final class StrokeQuad: NSObject, Codable {
|
||||
var quad: Quad
|
||||
|
||||
init(quad: Quad) {
|
||||
self.quad = quad
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||
let control = CGPoint.middle(p1: start, p2: end)
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
case 3:
|
||||
discardVertices(upto: stroke.vertexIndex, on: stroke)
|
||||
discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke)
|
||||
let index = stroke.keyPoints.count - 1
|
||||
var start = stroke.keyPoints[index - 2]
|
||||
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||
@@ -62,7 +62,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||
}
|
||||
|
||||
private func smoothOutPath(on stroke: Stroke) {
|
||||
discardVertices(upto: stroke.vertexIndex, on: stroke)
|
||||
discardVertices(upto: stroke.vertexIndex, quadIndex: stroke.quadIndex, on: stroke)
|
||||
adjustPreviousKeyPoint(on: stroke)
|
||||
switch stroke.keyPoints.count {
|
||||
case 4:
|
||||
@@ -79,6 +79,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||
addCurve(from: start, to: end, by: control, on: stroke)
|
||||
}
|
||||
stroke.quadIndex = stroke.quads.count - 1
|
||||
stroke.vertexIndex = stroke.vertices.endIndex - 1
|
||||
}
|
||||
|
||||
@@ -105,9 +106,8 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||
case .random:
|
||||
rotation = CGFloat.random(in: 0...360) * .pi / 180
|
||||
}
|
||||
let quad = Quad(origin: point, size: stroke.thickness, color: stroke.color, rotation: rotation)
|
||||
stroke._quads.append(quad)
|
||||
stroke.vertices.append(contentsOf: quad.generateVertices())
|
||||
let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded)
|
||||
stroke.vertices.append(contentsOf: quad.generateVertices(stroke.color))
|
||||
stroke.vertexCount = stroke.vertices.endIndex
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||
let factor: CGFloat
|
||||
switch configuration.granularity {
|
||||
case .automatic:
|
||||
factor = min(5, 1 / (stroke.thickness * 10 / 500))
|
||||
factor = min(5, 1 / (stroke.thickness * 1 / 50))
|
||||
case .fixed:
|
||||
factor = 1 / (stroke.thickness * stroke.penStyle.anyPenStyle.stepRate)
|
||||
case .none:
|
||||
@@ -145,15 +145,28 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private func discardVertices(upto index: Int, on stroke: Stroke) {
|
||||
private func discardVertices(upto index: Int, quadIndex: Int, on stroke: Stroke) {
|
||||
if index < 0 {
|
||||
stroke.vertices.removeAll()
|
||||
stroke._quads.removeAll()
|
||||
discardQuads(from: quadIndex + 1, on: stroke)
|
||||
} else {
|
||||
let count = stroke.vertices.endIndex
|
||||
let dropCount = count - (max(0, index) + 1)
|
||||
stroke.vertices.removeLast(dropCount)
|
||||
stroke._quads.removeLast(dropCount / 6)
|
||||
discardQuads(from: quadIndex + 1, on: stroke)
|
||||
}
|
||||
}
|
||||
|
||||
private func discardQuads(from start: Int, on stroke: Stroke) {
|
||||
let quads = stroke.quads.array
|
||||
Persistence.performe { context in
|
||||
for index in start..<quads.count {
|
||||
if let quad = quads[index] as? Quad {
|
||||
quad.stroke = nil
|
||||
context.delete(quad)
|
||||
stroke.quads.remove(quad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ final class Stroke: NSManagedObject {
|
||||
@NSManaged var style: Int16
|
||||
@NSManaged var createdAt: Date
|
||||
@NSManaged var thickness: CGFloat
|
||||
@NSManaged var strokeQuads: Array<StrokeQuad>
|
||||
@NSManaged var quads: NSMutableOrderedSet
|
||||
@NSManaged var graphicContext: GraphicContext?
|
||||
|
||||
var angle: CGFloat = 0
|
||||
@@ -31,7 +31,6 @@ final class Stroke: NSManagedObject {
|
||||
var thicknessFactor: CGFloat = 0.7
|
||||
|
||||
var vertices: [QuadVertex] = []
|
||||
var _quads: [Quad] = []
|
||||
var vertexBuffer: MTLBuffer?
|
||||
var vertexCount: Int = 0
|
||||
|
||||
@@ -59,14 +58,21 @@ final class Stroke: NSManagedObject {
|
||||
}
|
||||
|
||||
func loadVertices() {
|
||||
vertices = strokeQuads
|
||||
.flatMap { $0.quad.generateVertices() }
|
||||
vertices = quads
|
||||
.flatMap { ($0 as? Quad)?.generateVertices(color) ?? [] }
|
||||
vertexCount = vertices.endIndex
|
||||
}
|
||||
|
||||
func saveQuads() {
|
||||
strokeQuads = _quads.map(StrokeQuad.init)
|
||||
_quads.removeAll()
|
||||
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad {
|
||||
let quad = Quad(context: Persistence.context)
|
||||
quad.id = UUID()
|
||||
quad.origin = point
|
||||
quad.rotation = rotation
|
||||
quad.size = thickness
|
||||
quad.shape = shape.rawValue
|
||||
quads.add(quad)
|
||||
quad.stroke = self
|
||||
return quad
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +85,9 @@ extension Stroke: Drawable {
|
||||
}
|
||||
|
||||
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
|
||||
if isEmpty {
|
||||
loadVertices()
|
||||
}
|
||||
guard !isEmpty else { return }
|
||||
prepare(device: device)
|
||||
renderEncoder.setFragmentTexture(texture, index: 0)
|
||||
|
||||
@@ -49,6 +49,15 @@ class History: ObservableObject {
|
||||
|
||||
func resetRedo() {
|
||||
redoCache = redoStack
|
||||
for event in redoStack {
|
||||
switch event {
|
||||
case .stroke(let stroke):
|
||||
Persistence.performe { context in
|
||||
context.delete(stroke)
|
||||
}
|
||||
}
|
||||
}
|
||||
Persistence.saveIfNeeded()
|
||||
redoStack.removeAll()
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,10 @@ class CanvasViewController: UIViewController {
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
Persistence.shared.viewContext.refresh(canvas, mergeChanges: false)
|
||||
history.resetRedo()
|
||||
Persistence.performe { context in
|
||||
context.refresh(canvas, mergeChanges: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ class Persistence {
|
||||
|
||||
static let shared: Persistence = Persistence()
|
||||
|
||||
private init() {
|
||||
QuadValueTransformer.register()
|
||||
}
|
||||
private init() { }
|
||||
|
||||
lazy var viewContext: NSManagedObjectContext = {
|
||||
static var context: NSManagedObjectContext = {
|
||||
shared.persistentContainer.viewContext
|
||||
}()
|
||||
|
||||
private lazy var viewContext: NSManagedObjectContext = {
|
||||
persistentContainer.viewContext
|
||||
}()
|
||||
|
||||
@@ -65,4 +67,18 @@ class Persistence {
|
||||
fatalError("[Memola]: \(error.localizedDescription)")
|
||||
}
|
||||
}()
|
||||
|
||||
static func performe(_ action: (NSManagedObjectContext) -> Void) {
|
||||
action(shared.viewContext)
|
||||
}
|
||||
|
||||
static func saveIfNeeded() {
|
||||
if context.hasChanges {
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// QuadValueTransformer.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/8/24.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(QuadValueTransformer)
|
||||
class QuadValueTransformer: ValueTransformer {
|
||||
static let name = NSValueTransformerName(rawValue: String(describing: QuadValueTransformer.self))
|
||||
|
||||
override class func transformedValueClass() -> AnyClass {
|
||||
StrokeQuad.self
|
||||
}
|
||||
|
||||
override func transformedValue(_ value: Any?) -> Any? {
|
||||
guard let quads = value as? [StrokeQuad] else {
|
||||
assertionFailure("[Memola] - Failed to transform `[Quad]` to `Data`")
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let data = try JSONEncoder().encode(quads.map(\.quad))
|
||||
return data
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
assertionFailure("[Memola] - Failed to transform `Quad` to `Data`")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func reverseTransformedValue(_ value: Any?) -> Any? {
|
||||
guard let data = value as? Data else {
|
||||
assertionFailure("[Memola] - Failed to transform `Data` to `Quad`")
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let quads = try JSONDecoder().decode([Quad].self, from: data)
|
||||
return quads.map(StrokeQuad.init)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
assertionFailure("[Memola] - Failed to transform `Data` to `Quad`")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func register() {
|
||||
let transformer = QuadValueTransformer()
|
||||
ValueTransformer.setValueTransformer(transformer, forName: name)
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,22 @@
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="canvas" maxCount="1" deletionRule="Cascade" destinationEntity="Canvas" inverseName="memo" inverseEntity="Canvas"/>
|
||||
</entity>
|
||||
<entity name="Quad" representedClassName="Quad" syncable="YES">
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="originX" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="originY" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rotation" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="shape" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="size" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Stroke" inverseName="quads" inverseEntity="Stroke"/>
|
||||
</entity>
|
||||
<entity name="Stroke" representedClassName="Stroke" syncable="YES">
|
||||
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="strokeQuads" attributeType="Transformable" valueTransformerName="QuadValueTransformer" customClassName="[StrokeQuad]"/>
|
||||
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContext" inverseName="strokes" inverseEntity="GraphicContext"/>
|
||||
<relationship name="quads" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="Quad" inverseName="stroke" inverseEntity="Quad"/>
|
||||
</entity>
|
||||
</model>
|
||||
Reference in New Issue
Block a user