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:
dscyrescotti
2024-05-10 18:17:07 +07:00
14 changed files with 141 additions and 212 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ struct MemolaApp: App {
var body: some Scene {
WindowGroup {
MemosView()
.environment(\.managedObjectContext, Persistence.shared.viewContext)
.environment(\.managedObjectContext, Persistence.context)
}
}
}
+8 -24
View File
@@ -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
+7 -18
View File
@@ -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
}
}
+27 -60
View File
@@ -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)
}
@@ -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
View File
@@ -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)
+9
View File
@@ -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)
}
}
}
+20 -4
View File
@@ -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>