feat: load only visible strokes

This commit is contained in:
dscyrescotti
2024-05-14 23:53:24 +07:00
parent c1b9baa354
commit 8ee010b77a
8 changed files with 87 additions and 15 deletions
+23 -6
View File
@@ -60,18 +60,32 @@ 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 }
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 _stroke.loadQuads()
context.refresh(stroke, mergeChanges: false) 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
} }
} }
func loadQuads(_ bounds: CGRect) {
for stroke in self.strokes {
guard stroke.isVisible(in: bounds), stroke.quads.isEmpty else { continue }
stroke.loadQuads()
}
}
} }
extension GraphicContext: Drawable { extension GraphicContext: Drawable {
@@ -93,6 +107,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 +115,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
@@ -126,10 +142,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)
+14 -4
View File
@@ -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 = 30
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,8 +46,9 @@ 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
NSLog(Date().formatted(.dateTime.minute().second().secondFraction(.fractional(2))))
self?.state = .loading self?.state = .loading
} }
guard let canvas = context.object(with: canvasID) as? CanvasObject else { guard let canvas = context.object(with: canvasID) as? CanvasObject else {
@@ -53,13 +56,20 @@ 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
NSLog(Date().formatted(.dateTime.minute().second().secondFraction(.fractional(2))))
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
@@ -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)
}
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)
@@ -106,6 +121,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 +133,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 {
@@ -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()
} }
} }
+4
View File
@@ -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))
}
} }
@@ -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"/>