feat: add grid option

This commit is contained in:
dscyrescotti
2024-06-25 20:26:58 +07:00
parent 32af33f617
commit de71b567ac
16 changed files with 279 additions and 41 deletions
@@ -0,0 +1,13 @@
//
// LineGridVertex.swift
// Memola
//
// Created by Dscyre Scotti on 6/25/24.
//
import MetalKit
import Foundation
struct LineGridVertex {
var position: vector_float4
}
@@ -1,5 +1,5 @@
//
// GridVertex.swift
// PointGridVertex.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
@@ -8,12 +8,12 @@
import MetalKit
import Foundation
struct GridVertex {
struct PointGridVertex {
var position: vector_float4
var pointSize: Float = 10
}
extension GridVertex {
extension PointGridVertex {
init(x: CGFloat, y: CGFloat) {
self.position = [x.float, y.float, 0, 1]
}
@@ -0,0 +1,45 @@
//
// LineGridContext.swift
// Memola
//
// Created by Dscyre Scotti on 6/25/24.
//
import MetalKit
import Foundation
class LineGridContext {
var vertices: [LineGridVertex] = []
var vertexCount: Int = 0
var vertexBuffer: MTLBuffer?
init() {
generateVertices()
}
func generateVertices() {
let steps = stride(from: -10, through: 110, by: 0.25)
for y in steps {
vertices.append(LineGridVertex(position: [-10, Float(y), 0, 0]))
vertices.append(LineGridVertex(position: [110, Float(y), 0, 0]))
}
for x in steps {
vertices.append(LineGridVertex(position: [Float(x), -10, 0, 0]))
vertices.append(LineGridVertex(position: [Float(x), 110, 0, 0]))
}
vertexCount = vertices.count
}
}
extension LineGridContext: Drawable {
func prepare(device: MTLDevice) {
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<LineGridVertex>.stride, options: [])
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
guard vertexCount > .zero else { return }
prepare(device: device)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.drawPrimitives(type: .line, vertexStart: 0, vertexCount: vertexCount)
}
}
@@ -1,5 +1,5 @@
//
// GridContext.swift
// PointGridContext.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
@@ -8,8 +8,8 @@
import MetalKit
import Foundation
class GridContext {
var vertices: [GridVertex] = []
class PointGridContext {
var vertices: [PointGridVertex] = []
var vertexCount: Int = 0
var vertexBuffer: MTLBuffer?
@@ -21,16 +21,16 @@ class GridContext {
let steps = stride(from: -10, through: 110, by: 0.25)
for y in steps {
for x in steps {
vertices.append(GridVertex(x: CGFloat(x), y: CGFloat(y)))
vertices.append(PointGridVertex(x: CGFloat(x), y: CGFloat(y)))
}
}
vertexCount = vertices.count
}
}
extension GridContext: Drawable {
extension PointGridContext: Drawable {
func prepare(device: MTLDevice) {
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<GridVertex>.stride, options: [])
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PointGridVertex>.stride, options: [])
}
func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
+33 -4
View File
@@ -13,8 +13,10 @@ import Foundation
final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
let size: CGSize
let canvasID: NSManagedObjectID
var object: CanvasObject?
let gridContext = GridContext()
let pointGridContext = PointGridContext()
let lineGridContext = LineGridContext()
var graphicContext = GraphicContext()
let viewPortContext = ViewPortContext()
@@ -31,11 +33,14 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable {
@Published var zoomScale: CGFloat = .zero
@Published var locksCanvas: Bool = false
@Published var gridMode: GridMode = .point
let zoomPublisher = PassthroughSubject<CGFloat, Never>()
init(size: CGSize, canvasID: NSManagedObjectID) {
init(size: CGSize, canvasID: NSManagedObjectID, gridMode: Int16) {
self.size = size
self.canvasID = canvasID
self.gridMode = GridMode(rawValue: gridMode) ?? .point
}
var hasValidStroke: Bool {
@@ -56,6 +61,7 @@ extension Canvas {
guard let canvas = context.object(with: canvasID) as? CanvasObject else {
return
}
self?.object = canvas
let graphicContext = canvas.graphicContext
self?.graphicContext.object = graphicContext
self?.graphicContext.loadStrokes(bounds)
@@ -103,6 +109,18 @@ extension Canvas {
}
}
// MARK: - Grid Mode
extension Canvas {
func setGridMode(_ gridMode: GridMode) {
guard self.gridMode != gridMode else { return }
self.gridMode = gridMode
withPersistence(\.backgroundContext) { [weak object] context in
object?.gridMode = gridMode.rawValue
try context.saveIfNeeded()
}
}
}
// MARK: - Stroke
extension Canvas {
func beginTouch(at point: CGPoint, pen: Pen) -> any Stroke {
@@ -135,7 +153,7 @@ extension Canvas {
// MARK: - Rendering
extension Canvas {
func renderGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
func renderPointGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
var uniforms = GridUniforms(
ratio: size.width.float / 100,
zoom: zoomScale.float,
@@ -143,7 +161,18 @@ extension Canvas {
)
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<GridUniforms>.size)
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
gridContext.draw(device: device, renderEncoder: renderEncoder)
pointGridContext.draw(device: device, renderEncoder: renderEncoder)
}
func renderLineGrid(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
var uniforms = GridUniforms(
ratio: size.width.float / 100,
zoom: zoomScale.float,
transform: transform
)
uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout<GridUniforms>.size)
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
lineGridContext.draw(device: device, renderEncoder: renderEncoder)
}
func renderGraphic(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) {
+25 -4
View File
@@ -9,14 +9,35 @@ import MetalKit
import Foundation
struct PipelineStates {
static func createGridPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
static func createPointGridPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
let device = renderer.device
let library = renderer.library
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_grid")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_grid")
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_point_grid")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_point_grid")
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
pipelineDescriptor.label = "Grid Pipeline State"
pipelineDescriptor.label = "Point Grid Pipeline State"
let attachment = pipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.rgbBlendOperation = .add
attachment?.sourceRGBBlendFactor = .one
attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha
attachment?.alphaBlendOperation = .add
attachment?.sourceAlphaBlendFactor = .sourceAlpha
attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
return try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
}
static func createLineGridPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? {
let device = renderer.device
let library = renderer.library
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_line_grid")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_line_grid")
pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat
pipelineDescriptor.label = "Line Grid Pipeline State"
let attachment = pipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
+38
View File
@@ -0,0 +1,38 @@
//
// GridMode.swift
// Memola
//
// Created by Dscyre Scotti on 6/25/24.
//
import Foundation
enum GridMode: Int16, Equatable {
case none
case point
case line
var title: String {
switch self {
case .none:
return "No Grid"
case .point:
return "Point Grid"
case .line:
return "Line Grid"
}
}
var icon: String {
switch self {
case .none:
return "square.slash"
case .point:
return "circle.grid.3x3.fill"
case .line:
return "squareshape.split.3x3"
}
}
static let all: [GridMode] = [.none, .point, line]
}
@@ -12,7 +12,8 @@ class ViewPortRenderPass: RenderPass {
var label: String { "View Port Render Pass"}
var descriptor: MTLRenderPassDescriptor?
var gridPipelineState: MTLRenderPipelineState?
var pointGridPipelineState: MTLRenderPipelineState?
var lineGridPipelineState: MTLRenderPipelineState?
var viewPortPipelineState: MTLRenderPipelineState?
var viewPortUpdatePipelineState: MTLRenderPipelineState?
@@ -22,7 +23,8 @@ class ViewPortRenderPass: RenderPass {
weak var view: MTKView?
init(renderer: Renderer) {
gridPipelineState = PipelineStates.createGridPipelineState(from: renderer)
pointGridPipelineState = PipelineStates.createPointGridPipelineState(from: renderer)
lineGridPipelineState = PipelineStates.createLineGridPipelineState(from: renderer)
viewPortPipelineState = PipelineStates.createViewPortPipelineState(from: renderer)
viewPortUpdatePipelineState = PipelineStates.createViewPortPipelineState(from: renderer, isUpdate: true)
}
@@ -38,9 +40,18 @@ class ViewPortRenderPass: RenderPass {
}
renderEncoder.label = "View Port Render Encoder"
guard let gridPipelineState else { return }
renderEncoder.setRenderPipelineState(gridPipelineState)
canvas.renderGrid(device: renderer.device, renderEncoder: renderEncoder)
switch canvas.gridMode {
case .none:
break
case .point:
guard let pointGridPipelineState else { return }
renderEncoder.setRenderPipelineState(pointGridPipelineState)
canvas.renderPointGrid(device: renderer.device, renderEncoder: renderEncoder)
case .line:
guard let lineGridPipelineState else { return }
renderEncoder.setRenderPipelineState(lineGridPipelineState)
canvas.renderLineGrid(device: renderer.device, renderEncoder: renderEncoder)
}
if renderer.updatesViewPort {
guard let viewPortUpdatePipelineState else {
+28 -7
View File
@@ -8,33 +8,37 @@
#include <metal_stdlib>
using namespace metal;
struct Vertex {
struct PointVertex {
float4 position [[position]];
float pointSize [[point_size]];
};
struct LineVertex {
float4 position [[position]];
};
struct Uniforms {
float ratio;
float zoom;
float4x4 transform;
};
vertex Vertex vertex_grid(
constant Vertex *vertices [[buffer(0)]],
vertex PointVertex vertex_point_grid(
constant PointVertex *vertices [[buffer(0)]],
constant Uniforms &uniforms [[buffer(11)]],
uint vertexId [[vertex_id]]
) {
Vertex _vertex = vertices[vertexId];
PointVertex _vertex = vertices[vertexId];
float x = _vertex.position.x * uniforms.ratio;
float y = _vertex.position.y * uniforms.ratio;
float4 position = float4(x, y, 0, 1);
_vertex.position = uniforms.transform * position;
_vertex.pointSize = 10 * uniforms.zoom / 12;
_vertex.pointSize = 8 * uniforms.zoom / 12;
return _vertex;
}
fragment float4 fragment_grid(
Vertex _vertex [[stage_in]],
fragment float4 fragment_point_grid(
PointVertex _vertex [[stage_in]],
float2 pointCoord [[point_coord]]
) {
float dist = length(pointCoord - float2(0.5));
@@ -42,3 +46,20 @@ fragment float4 fragment_grid(
color.a = 1.0 - smoothstep(0.4, 0.5, dist);
return color;
}
vertex LineVertex vertex_line_grid(
constant LineVertex *vertices [[buffer(0)]],
constant Uniforms &uniforms [[buffer(11)]],
uint vertexId [[vertex_id]]
) {
LineVertex _vertex = vertices[vertexId];
float x = _vertex.position.x * uniforms.ratio;
float y = _vertex.position.y * uniforms.ratio;
float4 position = float4(x, y, 0, 1);
_vertex.position = uniforms.transform * position;
return _vertex;
}
fragment float4 fragment_line_grid() {
return float4(0.752, 0.752, 0.752, 1);
}
@@ -161,18 +161,22 @@ extension CanvasViewController {
self?.canvasStateChanged(state)
}
.store(in: &cancellables)
canvas.zoomPublisher
.sink { [weak self] zoomScale in
self?.zoomChanged(zoomScale)
}
.store(in: &cancellables)
canvas.$locksCanvas
.sink { [weak self] state in
self?.lockModeChanged(state)
}
.store(in: &cancellables)
canvas.$gridMode
.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)
.sink { [weak self] mode in
self?.gridModeChanged(mode)
}
.store(in: &cancellables)
tool.$selectedPen
.sink { [weak self] pen in
@@ -364,6 +368,13 @@ extension CanvasViewController {
func lockModeChanged(_ state: Bool) {
scrollView.pinchGestureRecognizer?.isEnabled = !state
}
func gridModeChanged(_ mode: GridMode) {
drawingView.disableUserInteraction()
renderer.resize(on: renderView, to: renderView.drawableSize)
renderView.draw()
drawingView.enableUserInteraction()
}
}
extension CanvasViewController {
+1 -1
View File
@@ -23,7 +23,7 @@ struct MemoView: View {
self.memo = memo
self.title = memo.title
self._tool = StateObject(wrappedValue: Tool(object: memo.tool))
self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID))
self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID, gridMode: memo.canvas.gridMode))
}
var body: some View {
+27 -1
View File
@@ -50,8 +50,9 @@ struct Toolbar: View {
if !canvas.locksCanvas {
elementTool
}
Group {
HStack(spacing: 5) {
if !canvas.locksCanvas {
gridModeControl
historyControl
}
}
@@ -263,6 +264,31 @@ struct Toolbar: View {
.transition(.move(edge: .top).combined(with: .blurReplace))
}
var gridModeControl: some View {
Menu {
ForEach(GridMode.all, id: \.self) { mode in
Button {
canvas.setGridMode(mode)
} label: {
Label {
Text(mode.title)
} icon: {
Image(systemName: mode.icon)
}
.font(.headline)
}
}
} label: {
Image(systemName: canvas.gridMode.icon)
.contentShape(.circle)
.frame(width: size, height: size)
.background(.regularMaterial)
.clipShape(.rect(cornerRadius: 8))
}
.hoverEffect(.lift)
.contentTransition(.symbolEffect(.replace))
}
func openCamera() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
+1
View File
@@ -71,6 +71,7 @@ struct MemosView: View {
let canvasObject = CanvasObject(context: managedObjectContext)
canvasObject.width = 8_000
canvasObject.height = 8_000
canvasObject.gridMode = 1
let toolObject = ToolObject(\.viewContext)
toolObject.selection = 0
@@ -12,6 +12,7 @@ import Foundation
final class CanvasObject: NSManagedObject {
@NSManaged var width: CGFloat
@NSManaged var height: CGFloat
@NSManaged var gridMode: Int16
@NSManaged var memo: MemoObject?
@NSManaged var graphicContext: GraphicContextObject
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23B74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="CanvasObject" representedClassName="CanvasObject" syncable="YES">
<attribute name="gridMode" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/>
<attribute name="width" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES" customClassName="CGFloat"/>
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GraphicContextObject" inverseName="canvas" inverseEntity="GraphicContextObject"/>