mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-07-08 05:45:17 +02:00
Merge pull request #26 from dscyrescotti/feature/canvas-loading
Optimize canvas loading time
This commit is contained in:
@@ -60,17 +60,40 @@ final class GraphicContext: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension GraphicContext {
|
extension GraphicContext {
|
||||||
func loadStrokes() {
|
func loadStrokes(_ bounds: CGRect) {
|
||||||
guard let object else { return }
|
guard let object else { return }
|
||||||
|
let queue = OperationQueue()
|
||||||
|
queue.qualityOfService = .userInteractive
|
||||||
self.strokes = object.strokes.compactMap { stroke -> Stroke? in
|
self.strokes = object.strokes.compactMap { stroke -> Stroke? in
|
||||||
guard let stroke = stroke as? StrokeObject else { return nil }
|
guard let stroke = stroke as? StrokeObject else { return nil }
|
||||||
let _stroke = Stroke(object: stroke)
|
let _stroke = Stroke(object: stroke)
|
||||||
_stroke.loadQuads()
|
if _stroke.isVisible(in: bounds) {
|
||||||
withPersistence(\.backgroundContext) { [stroke] context in
|
let id = stroke.objectID
|
||||||
context.refresh(stroke, mergeChanges: false)
|
queue.addOperation {
|
||||||
|
withPersistenceSync(\.newBackgroundContext) { [_stroke] context in
|
||||||
|
guard let stroke = try? context.existingObject(with: id) as? StrokeObject else { return }
|
||||||
|
_stroke.loadQuads(from: stroke)
|
||||||
|
}
|
||||||
|
withPersistence(\.backgroundContext) { [stroke] context in
|
||||||
|
context.refresh(stroke, mergeChanges: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withPersistence(\.backgroundContext) { [stroke] context in
|
||||||
|
_stroke.loadQuads()
|
||||||
|
context.refresh(stroke, mergeChanges: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _stroke
|
return _stroke
|
||||||
}
|
}
|
||||||
|
queue.waitUntilAllOperationsAreFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadQuads(_ bounds: CGRect) {
|
||||||
|
for stroke in self.strokes {
|
||||||
|
guard stroke.isVisible(in: bounds), stroke.quads.isEmpty else { continue }
|
||||||
|
stroke.loadQuads()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +116,7 @@ extension GraphicContext: Drawable {
|
|||||||
extension GraphicContext {
|
extension GraphicContext {
|
||||||
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
|
func beginStroke(at point: CGPoint, pen: Pen) -> Stroke {
|
||||||
let stroke = Stroke(
|
let stroke = Stroke(
|
||||||
|
bounds: [point.x - pen.thickness, point.y - pen.thickness, point.x + pen.thickness, point.y + pen.thickness],
|
||||||
color: pen.color,
|
color: pen.color,
|
||||||
style: pen.strokeStyle.rawValue,
|
style: pen.strokeStyle.rawValue,
|
||||||
createdAt: .now,
|
createdAt: .now,
|
||||||
@@ -100,6 +124,7 @@ extension GraphicContext {
|
|||||||
)
|
)
|
||||||
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in
|
withPersistence(\.backgroundContext) { [graphicContext = object, _stroke = stroke] context in
|
||||||
let stroke = StrokeObject(\.backgroundContext)
|
let stroke = StrokeObject(\.backgroundContext)
|
||||||
|
stroke.bounds = _stroke.bounds
|
||||||
stroke.color = _stroke.color
|
stroke.color = _stroke.color
|
||||||
stroke.style = _stroke.style
|
stroke.style = _stroke.style
|
||||||
stroke.thickness = _stroke.thickness
|
stroke.thickness = _stroke.thickness
|
||||||
@@ -118,7 +143,9 @@ extension GraphicContext {
|
|||||||
|
|
||||||
func appendStroke(with point: CGPoint) {
|
func appendStroke(with point: CGPoint) {
|
||||||
guard let currentStroke else { return }
|
guard let currentStroke else { return }
|
||||||
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.anyPenStyle.stepRate else { return }
|
guard let currentPoint, point.distance(to: currentPoint) > currentStroke.thickness * currentStroke.penStyle.anyPenStyle.stepRate else {
|
||||||
|
return
|
||||||
|
}
|
||||||
currentStroke.append(to: point)
|
currentStroke.append(to: point)
|
||||||
self.currentPoint = point
|
self.currentPoint = point
|
||||||
}
|
}
|
||||||
@@ -126,10 +153,11 @@ extension GraphicContext {
|
|||||||
func endStroke(at point: CGPoint) {
|
func endStroke(at point: CGPoint) {
|
||||||
guard currentPoint != nil, let currentStroke else { return }
|
guard currentPoint != nil, let currentStroke else { return }
|
||||||
currentStroke.finish(at: point)
|
currentStroke.finish(at: point)
|
||||||
let saveIndex = currentStroke.batchIndex
|
let batchIndex = currentStroke.batchIndex
|
||||||
let quads = Array(currentStroke.quads[saveIndex..<currentStroke.quads.count])
|
let quads = Array(currentStroke.quads[batchIndex..<currentStroke.quads.count])
|
||||||
withPersistence(\.backgroundContext) { [currentStroke, quads] context in
|
withPersistence(\.backgroundContext) { [currentStroke, quads] context in
|
||||||
currentStroke.saveQuads(for: quads)
|
currentStroke.saveQuads(for: quads)
|
||||||
|
currentStroke.object?.bounds = currentStroke.bounds
|
||||||
try context.saveIfNeeded()
|
try context.saveIfNeeded()
|
||||||
if let stroke = currentStroke.object {
|
if let stroke = currentStroke.object {
|
||||||
context.refresh(stroke, mergeChanges: false)
|
context.refresh(stroke, mergeChanges: false)
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
|
|||||||
var graphicContext = GraphicContext()
|
var graphicContext = GraphicContext()
|
||||||
let viewPortContext = ViewPortContext()
|
let viewPortContext = ViewPortContext()
|
||||||
|
|
||||||
let maximumZoomScale: CGFloat = 28
|
let maximumZoomScale: CGFloat = 35
|
||||||
let minimumZoomScale: CGFloat = 3.1
|
let minimumZoomScale: CGFloat = 5
|
||||||
|
let defaultZoomScale: CGFloat = 20
|
||||||
|
|
||||||
var transform: simd_float4x4 = .init()
|
var transform: simd_float4x4 = .init()
|
||||||
var clipBounds: CGRect = .zero
|
var clipBounds: CGRect = .zero
|
||||||
var zoomScale: CGFloat = .zero
|
var zoomScale: CGFloat = .zero
|
||||||
|
var bounds: CGRect = .zero
|
||||||
var uniformsBuffer: MTLBuffer?
|
var uniformsBuffer: MTLBuffer?
|
||||||
|
|
||||||
init(size: CGSize, canvasID: NSManagedObjectID) {
|
init(size: CGSize, canvasID: NSManagedObjectID) {
|
||||||
@@ -44,7 +46,7 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
|
|||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
extension Canvas {
|
extension Canvas {
|
||||||
func load() {
|
func load() {
|
||||||
withPersistence(\.backgroundContext) { [weak self, canvasID] context in
|
withPersistence(\.backgroundContext) { [weak self, canvasID, bounds] context in
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.state = .loading
|
self?.state = .loading
|
||||||
}
|
}
|
||||||
@@ -53,13 +55,19 @@ extension Canvas {
|
|||||||
}
|
}
|
||||||
let graphicContext = canvas.graphicContext
|
let graphicContext = canvas.graphicContext
|
||||||
self?.graphicContext.object = graphicContext
|
self?.graphicContext.object = graphicContext
|
||||||
self?.graphicContext.loadStrokes()
|
self?.graphicContext.loadStrokes(bounds)
|
||||||
context.refresh(canvas, mergeChanges: false)
|
context.refresh(canvas, mergeChanges: false)
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.state = .loaded
|
self?.state = .loaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadStrokes(_ bounds: CGRect) {
|
||||||
|
withPersistence(\.backgroundContext) { [weak self, bounds] context in
|
||||||
|
self?.graphicContext.loadQuads(bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Dimension
|
// MARK: - Dimension
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func append(to point: CGPoint, on stroke: Stroke) {
|
func append(to point: CGPoint, on stroke: Stroke) {
|
||||||
guard stroke.keyPoints.count > 0 else {
|
guard stroke.keyPoints.endIndex > 0 else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stroke.keyPoints.append(point)
|
stroke.keyPoints.append(point)
|
||||||
switch stroke.keyPoints.count {
|
switch stroke.keyPoints.endIndex {
|
||||||
case 2:
|
case 2:
|
||||||
let start = stroke.keyPoints[0]
|
let start = stroke.keyPoints[0]
|
||||||
let end = stroke.keyPoints[1]
|
let end = stroke.keyPoints[1]
|
||||||
@@ -28,7 +28,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
case 3:
|
case 3:
|
||||||
stroke.removeQuads(from: stroke.quadIndex + 1)
|
stroke.removeQuads(from: stroke.quadIndex + 1)
|
||||||
let index = stroke.keyPoints.count - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
var start = stroke.keyPoints[index - 2]
|
var start = stroke.keyPoints[index - 2]
|
||||||
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
var control = CGPoint.middle(p1: start, p2: end)
|
var control = CGPoint.middle(p1: start, p2: end)
|
||||||
@@ -39,7 +39,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
default:
|
default:
|
||||||
smoothOutPath(on: stroke)
|
smoothOutPath(on: stroke)
|
||||||
let index = stroke.keyPoints.count - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
let control = stroke.keyPoints[index - 1]
|
let control = stroke.keyPoints[index - 1]
|
||||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||||
@@ -48,12 +48,12 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func finish(at point: CGPoint, on stroke: Stroke) {
|
func finish(at point: CGPoint, on stroke: Stroke) {
|
||||||
switch stroke.keyPoints.count {
|
switch stroke.keyPoints.endIndex {
|
||||||
case 0...1:
|
case 0...1:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
append(to: point, on: stroke)
|
append(to: point, on: stroke)
|
||||||
let index = stroke.keyPoints.count - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
let end = stroke.keyPoints[index]
|
let end = stroke.keyPoints[index]
|
||||||
let control = CGPoint.middle(p1: start, p2: end)
|
let control = CGPoint.middle(p1: start, p2: end)
|
||||||
@@ -64,37 +64,34 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
private func smoothOutPath(on stroke: Stroke) {
|
private func smoothOutPath(on stroke: Stroke) {
|
||||||
stroke.removeQuads(from: stroke.quadIndex + 1)
|
stroke.removeQuads(from: stroke.quadIndex + 1)
|
||||||
adjustPreviousKeyPoint(on: stroke)
|
adjustPreviousKeyPoint(on: stroke)
|
||||||
switch stroke.keyPoints.count {
|
switch stroke.keyPoints.endIndex {
|
||||||
case 4:
|
case 4:
|
||||||
let index = stroke.keyPoints.count - 2
|
let index = stroke.keyPoints.endIndex - 2
|
||||||
let start = stroke.keyPoints[index - 2]
|
let start = stroke.keyPoints[index - 2]
|
||||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
let end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
let control = CGPoint.middle(p1: start, p2: end)
|
let control = CGPoint.middle(p1: start, p2: end)
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
let index = stroke.keyPoints.count - 2
|
let index = stroke.keyPoints.endIndex - 2
|
||||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
let control = stroke.keyPoints[index - 1]
|
let control = stroke.keyPoints[index - 1]
|
||||||
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
}
|
}
|
||||||
stroke.quadIndex = stroke.quads.count - 1
|
stroke.quadIndex = stroke.quads.endIndex - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private func adjustPreviousKeyPoint(on stroke: Stroke) {
|
private func adjustPreviousKeyPoint(on stroke: Stroke) {
|
||||||
let index = stroke.keyPoints.count - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
let prev = stroke.keyPoints[index - 1]
|
let prev = stroke.keyPoints[index - 2]
|
||||||
|
let mid = stroke.keyPoints[index - 1]
|
||||||
let current = stroke.keyPoints[index]
|
let current = stroke.keyPoints[index]
|
||||||
let averageX = (prev.x + current.x) / 2
|
let averageX = (prev.x + current.x + mid.x) / 3
|
||||||
let averageY = (prev.y + current.y) / 2
|
let averageY = (prev.y + current.y + mid.y) / 3
|
||||||
let point = CGPoint(x: averageX, y: averageY)
|
let point = CGPoint(x: averageX, y: averageY)
|
||||||
if index != 0 {
|
stroke.keyPoints[index] = point
|
||||||
stroke.keyPoints[index] = point
|
stroke.keyPoints[index - 1] = point
|
||||||
}
|
|
||||||
if index - 1 != 0 {
|
|
||||||
stroke.keyPoints[index - 1] = point
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addPoint(_ point: CGPoint, on stroke: Stroke) {
|
private func addPoint(_ point: CGPoint, on stroke: Stroke) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Foundation
|
|||||||
|
|
||||||
final class Stroke: @unchecked Sendable {
|
final class Stroke: @unchecked Sendable {
|
||||||
var object: StrokeObject?
|
var object: StrokeObject?
|
||||||
|
var bounds: [CGFloat]
|
||||||
var color: [CGFloat]
|
var color: [CGFloat]
|
||||||
var style: Int16
|
var style: Int16
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
@@ -19,6 +20,7 @@ final class Stroke: @unchecked Sendable {
|
|||||||
|
|
||||||
init(object: StrokeObject) {
|
init(object: StrokeObject) {
|
||||||
self.object = object
|
self.object = object
|
||||||
|
self.bounds = object.bounds
|
||||||
self.color = object.color
|
self.color = object.color
|
||||||
self.style = object.style
|
self.style = object.style
|
||||||
self.createdAt = object.createdAt
|
self.createdAt = object.createdAt
|
||||||
@@ -27,12 +29,14 @@ final class Stroke: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
bounds: [CGFloat],
|
||||||
color: [CGFloat],
|
color: [CGFloat],
|
||||||
style: Int16,
|
style: Int16,
|
||||||
createdAt: Date,
|
createdAt: Date,
|
||||||
thickness: CGFloat,
|
thickness: CGFloat,
|
||||||
quads: [Quad] = []
|
quads: [Quad] = []
|
||||||
) {
|
) {
|
||||||
|
self.bounds = bounds
|
||||||
self.color = color
|
self.color = color
|
||||||
self.style = style
|
self.style = style
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
@@ -59,6 +63,17 @@ final class Stroke: @unchecked Sendable {
|
|||||||
var isEraserPenStyle: Bool {
|
var isEraserPenStyle: Bool {
|
||||||
penStyle == .eraser
|
penStyle == .eraser
|
||||||
}
|
}
|
||||||
|
var strokeBounds: CGRect {
|
||||||
|
let x = bounds[0]
|
||||||
|
let y = bounds[1]
|
||||||
|
let width = bounds[2] - x
|
||||||
|
let height = bounds[3] - y
|
||||||
|
return CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isVisible(in bounds: CGRect) -> Bool {
|
||||||
|
bounds.contains(strokeBounds) || bounds.intersects(strokeBounds)
|
||||||
|
}
|
||||||
|
|
||||||
func begin(at point: CGPoint) {
|
func begin(at point: CGPoint) {
|
||||||
penStyle.anyPenStyle.generator.begin(at: point, on: self)
|
penStyle.anyPenStyle.generator.begin(at: point, on: self)
|
||||||
@@ -83,6 +98,13 @@ extension Stroke {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadQuads(from object: StrokeObject) {
|
||||||
|
quads = object.quads.compactMap { quad in
|
||||||
|
guard let quad = quad as? QuadObject else { return nil }
|
||||||
|
return Quad(object: quad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad {
|
func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad {
|
||||||
let quad = Quad(
|
let quad = Quad(
|
||||||
origin: point,
|
origin: point,
|
||||||
@@ -106,6 +128,8 @@ extension Stroke {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveQuads(for quads: [Quad]) {
|
func saveQuads(for quads: [Quad]) {
|
||||||
|
var topLeft: CGPoint = CGPoint(x: bounds[0], y: bounds[1])
|
||||||
|
var bottomRight: CGPoint = CGPoint(x: bounds[2], y: bounds[3])
|
||||||
for _quad in quads {
|
for _quad in quads {
|
||||||
let quad = QuadObject(\.backgroundContext)
|
let quad = QuadObject(\.backgroundContext)
|
||||||
quad.originX = _quad.originX.cgFloat
|
quad.originX = _quad.originX.cgFloat
|
||||||
@@ -116,7 +140,12 @@ extension Stroke {
|
|||||||
quad.color = _quad.getColor()
|
quad.color = _quad.getColor()
|
||||||
quad.stroke = object
|
quad.stroke = object
|
||||||
object?.quads.add(quad)
|
object?.quads.add(quad)
|
||||||
|
topLeft.x = min(quad.originX, topLeft.x)
|
||||||
|
topLeft.y = min(quad.originY, topLeft.y)
|
||||||
|
bottomRight.x = max(quad.originX, bottomRight.x)
|
||||||
|
bottomRight.y = max(quad.originY, bottomRight.y)
|
||||||
}
|
}
|
||||||
|
bounds = [topLeft.x, topLeft.y, bottomRight.x, bottomRight.y]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class GraphicRenderPass: RenderPass {
|
|||||||
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
if graphicContext.previousStroke === stroke || graphicContext.currentStroke === stroke {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
guard stroke.isVisible(in: canvas.bounds) else { continue }
|
||||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
clearsTexture = false
|
clearsTexture = false
|
||||||
if stroke.isEraserPenStyle {
|
if stroke.isEraserPenStyle {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class StrokeRenderPass: RenderPass {
|
|||||||
computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
|
computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
|
||||||
computeEncoder.endEncoding()
|
computeEncoder.endEncoding()
|
||||||
quadCommandBuffer.commit()
|
quadCommandBuffer.commit()
|
||||||
|
quadCommandBuffer.waitUntilCompleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) {
|
private func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ struct EraserPenStyle: PenStyle {
|
|||||||
|
|
||||||
var textureName: String = "point-texture"
|
var textureName: String = "point-texture"
|
||||||
|
|
||||||
var thinkness: (min: CGFloat, max: CGFloat) = (1, 120)
|
var thinkness: (min: CGFloat, max: CGFloat) = (0.5, 120)
|
||||||
|
|
||||||
var color: [CGFloat] = [1, 1, 1, 0]
|
var color: [CGFloat] = [1, 1, 1, 0]
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ struct MarkerPenStyle: PenStyle {
|
|||||||
|
|
||||||
var textureName: String = "point-texture"
|
var textureName: String = "point-texture"
|
||||||
|
|
||||||
var thinkness: (min: CGFloat, max: CGFloat) = (1, 120)
|
var thinkness: (min: CGFloat, max: CGFloat) = (0.5, 120)
|
||||||
|
|
||||||
var color: [CGFloat] = [1, 0.38, 0.38, 1]
|
var color: [CGFloat] = [1, 0.38, 0.38, 1]
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ class CanvasViewController: UIViewController {
|
|||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
configureViews()
|
configureViews()
|
||||||
configureListeners()
|
configureListeners()
|
||||||
|
|
||||||
loadMemo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
resizeDocumentView()
|
resizeDocumentView()
|
||||||
|
updateDocumentBounds()
|
||||||
|
loadMemo()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
@@ -119,11 +119,11 @@ extension CanvasViewController {
|
|||||||
let newFrame = CGRect(x: 0, y: 0, width: width, height: height)
|
let newFrame = CGRect(x: 0, y: 0, width: width, height: height)
|
||||||
drawingView.frame = newFrame
|
drawingView.frame = newFrame
|
||||||
|
|
||||||
scrollView.setZoomScale(canvas.minimumZoomScale, animated: true)
|
scrollView.setZoomScale(canvas.defaultZoomScale, animated: true)
|
||||||
centerDocumentView(to: newSize)
|
centerDocumentView(to: newSize)
|
||||||
|
|
||||||
let offsetX = (newFrame.width * canvas.minimumZoomScale - view.frame.width) / 2
|
let offsetX = (newFrame.width * canvas.defaultZoomScale - view.frame.width) / 2
|
||||||
let offsetY = (newFrame.height * canvas.minimumZoomScale - view.frame.height) / 2
|
let offsetY = (newFrame.height * canvas.defaultZoomScale - view.frame.height) / 2
|
||||||
|
|
||||||
let point = CGPoint(x: offsetX, y: offsetY)
|
let point = CGPoint(x: offsetX, y: offsetY)
|
||||||
scrollView.setContentOffset(point, animated: true)
|
scrollView.setContentOffset(point, animated: true)
|
||||||
@@ -138,6 +138,20 @@ extension CanvasViewController {
|
|||||||
let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0
|
let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0
|
||||||
self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
|
self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateDocumentBounds() {
|
||||||
|
var bounds = scrollView.bounds.muliply(by: drawingView.ratio / scrollView.zoomScale)
|
||||||
|
let xDelta = bounds.minX * 0.05
|
||||||
|
let yDelta = bounds.minY * 0.05
|
||||||
|
bounds.origin.x -= xDelta
|
||||||
|
bounds.origin.y -= yDelta
|
||||||
|
bounds.size.width += xDelta * 2
|
||||||
|
bounds.size.height += yDelta * 2
|
||||||
|
canvas.bounds = bounds
|
||||||
|
if canvas.state == .loaded {
|
||||||
|
canvas.loadStrokes(bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CanvasViewController {
|
extension CanvasViewController {
|
||||||
@@ -208,6 +222,7 @@ extension CanvasViewController: UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||||
|
updateDocumentBounds()
|
||||||
centerDocumentView()
|
centerDocumentView()
|
||||||
magnificationEnded()
|
magnificationEnded()
|
||||||
}
|
}
|
||||||
@@ -234,6 +249,7 @@ extension CanvasViewController: UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
|
func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
|
||||||
|
updateDocumentBounds()
|
||||||
draggingEnded()
|
draggingEnded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DrawingView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateDrawableSize(with size: CGSize) {
|
func updateDrawableSize(with size: CGSize) {
|
||||||
renderView.drawableSize = size.multiply(by: 2.0)
|
renderView.drawableSize = size.multiply(by: 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
|||||||
@@ -16,4 +16,8 @@ extension CGRect {
|
|||||||
t = t.scaledBy(x: rect.width, y: rect.height)
|
t = t.scaledBy(x: rect.width, y: rect.height)
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func muliply(by factor: CGFloat) -> CGRect {
|
||||||
|
CGRect(origin: origin.muliply(by: factor), size: size.multiply(by: factor))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ struct MemosView: View {
|
|||||||
memoObject.updatedAt = .now
|
memoObject.updatedAt = .now
|
||||||
|
|
||||||
let canvasObject = CanvasObject(context: managedObjectContext)
|
let canvasObject = CanvasObject(context: managedObjectContext)
|
||||||
canvasObject.width = 4_000
|
canvasObject.width = 8_000
|
||||||
canvasObject.height = 4_000
|
canvasObject.height = 8_000
|
||||||
|
|
||||||
let graphicContextObject = GraphicContextObject(context: managedObjectContext)
|
let graphicContextObject = GraphicContextObject(context: managedObjectContext)
|
||||||
graphicContextObject.strokes = []
|
graphicContextObject.strokes = []
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ final class Persistence {
|
|||||||
return context
|
return context
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var newBackgroundContext: NSManagedObjectContext {
|
||||||
|
let context = persistentContainer.newBackgroundContext()
|
||||||
|
context.undoManager = nil
|
||||||
|
context.automaticallyMergesChangesFromParent = true
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
lazy var persistentContainer: NSPersistentContainer = {
|
lazy var persistentContainer: NSPersistentContainer = {
|
||||||
let persistentStore = NSPersistentStoreDescription()
|
let persistentStore = NSPersistentStoreDescription()
|
||||||
persistentStore.shouldMigrateStoreAutomatically = true
|
persistentStore.shouldMigrateStoreAutomatically = true
|
||||||
@@ -83,3 +90,14 @@ func withPersistence(_ keypath: KeyPath<Persistence, NSManagedObjectContext>, _
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withPersistenceSync(_ keypath: KeyPath<Persistence, NSManagedObjectContext>, _ task: @escaping (NSManagedObjectContext) throws -> Void) {
|
||||||
|
let context = Persistence.shared[keyPath: keypath]
|
||||||
|
context.performAndWait {
|
||||||
|
do {
|
||||||
|
try task(context)
|
||||||
|
} catch {
|
||||||
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Foundation
|
|||||||
|
|
||||||
@objc(StrokeObject)
|
@objc(StrokeObject)
|
||||||
final class StrokeObject: NSManagedObject {
|
final class StrokeObject: NSManagedObject {
|
||||||
|
@NSManaged var bounds: [CGFloat]
|
||||||
@NSManaged var color: [CGFloat]
|
@NSManaged var color: [CGFloat]
|
||||||
@NSManaged var style: Int16
|
@NSManaged var style: Int16
|
||||||
@NSManaged var createdAt: Date
|
@NSManaged var createdAt: Date
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="quads" inverseEntity="StrokeObject"/>
|
<relationship name="stroke" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StrokeObject" inverseName="quads" inverseEntity="StrokeObject"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="StrokeObject" representedClassName="StrokeObject" syncable="YES">
|
<entity name="StrokeObject" representedClassName="StrokeObject" syncable="YES">
|
||||||
|
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="style" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user