210 lines
7.2 KiB
Swift
210 lines
7.2 KiB
Swift
import SwiftUI
|
|
|
|
enum DrawingMode: String, CaseIterable, Identifiable {
|
|
case draw
|
|
case erase
|
|
|
|
var id: String {
|
|
rawValue
|
|
}
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .draw:
|
|
return "Draw"
|
|
case .erase:
|
|
return "Erase"
|
|
}
|
|
}
|
|
|
|
var symbolName: String {
|
|
switch self {
|
|
case .draw:
|
|
return "rectangle"
|
|
case .erase:
|
|
return "eraser"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ShikakuGridView: View {
|
|
@ObservedObject var game: ShikakuGame
|
|
let mode: DrawingMode
|
|
let side: CGFloat
|
|
|
|
@State private var dragStart: GridPoint?
|
|
@State private var dragEnd: GridPoint?
|
|
@State private var lastErasedCell: GridPoint?
|
|
|
|
var body: some View {
|
|
Canvas { context, _ in
|
|
drawBoard(in: CGRect(x: 0, y: 0, width: side, height: side), context: &context)
|
|
}
|
|
.frame(width: side, height: side)
|
|
.contentShape(Rectangle())
|
|
.gesture(boardGesture(side: side))
|
|
.accessibilityLabel("Shikaku grid")
|
|
.accessibilityValue("\(game.size) by \(game.size)")
|
|
}
|
|
|
|
private func boardGesture(side: CGFloat) -> some Gesture {
|
|
DragGesture(minimumDistance: 0)
|
|
.onChanged { value in
|
|
guard let cell = cell(at: value.location, side: side) else {
|
|
return
|
|
}
|
|
|
|
switch mode {
|
|
case .draw:
|
|
if dragStart == nil {
|
|
dragStart = cell
|
|
}
|
|
dragEnd = cell
|
|
case .erase:
|
|
guard lastErasedCell != cell else {
|
|
return
|
|
}
|
|
game.removeRect(at: cell)
|
|
lastErasedCell = cell
|
|
dragStart = nil
|
|
dragEnd = nil
|
|
}
|
|
}
|
|
.onEnded { value in
|
|
defer {
|
|
dragStart = nil
|
|
dragEnd = nil
|
|
lastErasedCell = nil
|
|
}
|
|
|
|
guard mode == .draw,
|
|
let start = dragStart,
|
|
let end = cell(at: value.location, side: side) ?? dragEnd else {
|
|
return
|
|
}
|
|
|
|
game.placeRect(
|
|
PlayerRect(
|
|
startRow: start.row,
|
|
startCol: start.col,
|
|
endRow: end.row,
|
|
endCol: end.col
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private func drawBoard(in rect: CGRect, context: inout GraphicsContext) {
|
|
let cellSide = rect.width / CGFloat(game.size)
|
|
let boardRect = CGRect(origin: .zero, size: rect.size)
|
|
|
|
context.fill(Path(boardRect), with: .color(ShikakuTheme.paper))
|
|
|
|
for (index, playerRect) in game.playerRects.enumerated() {
|
|
draw(playerRect, index: index, cellSide: cellSide, context: &context)
|
|
}
|
|
|
|
if let start = dragStart, let end = dragEnd, mode == .draw {
|
|
let preview = PlayerRect(startRow: start.row, startCol: start.col, endRow: end.row, endCol: end.col)
|
|
drawPreview(preview, cellSide: cellSide, context: &context)
|
|
}
|
|
|
|
drawGridLines(cellSide: cellSide, boardRect: boardRect, context: &context)
|
|
drawClues(cellSide: cellSide, context: &context)
|
|
}
|
|
|
|
private func draw(_ rect: PlayerRect, index: Int, cellSide: CGFloat, context: inout GraphicsContext) {
|
|
let pathRect = cgRect(for: rect.bounds, cellSide: cellSide).insetBy(dx: 2, dy: 2)
|
|
let path = Path(roundedRect: pathRect, cornerRadius: 6)
|
|
let hasError = game.errorRectIDs.contains(rect.id)
|
|
|
|
if hasError {
|
|
context.fill(path, with: .color(ShikakuTheme.errorFill))
|
|
context.stroke(path, with: .color(ShikakuTheme.errorStroke), lineWidth: 2.5)
|
|
} else {
|
|
let colorIndex = index % ShikakuTheme.rectFills.count
|
|
context.fill(path, with: .color(ShikakuTheme.rectFills[colorIndex]))
|
|
context.stroke(path, with: .color(ShikakuTheme.rectStrokes[colorIndex]), lineWidth: 2.5)
|
|
}
|
|
}
|
|
|
|
private func drawPreview(_ rect: PlayerRect, cellSide: CGFloat, context: inout GraphicsContext) {
|
|
let pathRect = cgRect(for: rect.bounds, cellSide: cellSide).insetBy(dx: 2, dy: 2)
|
|
let path = Path(roundedRect: pathRect, cornerRadius: 6)
|
|
let isInvalid = rect.area == 1
|
|
|
|
context.fill(path, with: .color(isInvalid ? ShikakuTheme.errorFill : ShikakuTheme.dragFill))
|
|
context.stroke(path, with: .color(isInvalid ? ShikakuTheme.errorStroke : ShikakuTheme.dragStroke), lineWidth: 2)
|
|
|
|
let fontSize = max(10, min(26, cellSide * 0.38))
|
|
let center = CGPoint(x: pathRect.midX, y: pathRect.midY)
|
|
let label = Text("\(rect.area)")
|
|
.font(.system(size: fontSize, weight: .bold, design: .rounded))
|
|
|
|
context.draw(
|
|
label.foregroundStyle(Color.black.opacity(0.55)),
|
|
at: CGPoint(x: center.x + 1, y: center.y + 1),
|
|
anchor: .center
|
|
)
|
|
context.draw(
|
|
label.foregroundStyle(Color.white.opacity(0.95)),
|
|
at: center,
|
|
anchor: .center
|
|
)
|
|
}
|
|
|
|
private func drawGridLines(cellSide: CGFloat, boardRect: CGRect, context: inout GraphicsContext) {
|
|
var gridPath = Path()
|
|
|
|
for index in 0...game.size {
|
|
let offset = CGFloat(index) * cellSide
|
|
gridPath.move(to: CGPoint(x: offset, y: 0))
|
|
gridPath.addLine(to: CGPoint(x: offset, y: boardRect.maxY))
|
|
gridPath.move(to: CGPoint(x: 0, y: offset))
|
|
gridPath.addLine(to: CGPoint(x: boardRect.maxX, y: offset))
|
|
}
|
|
|
|
context.stroke(gridPath, with: .color(ShikakuTheme.gridLine), lineWidth: 0.8)
|
|
context.stroke(Path(boardRect), with: .color(ShikakuTheme.gridBorder), lineWidth: 3)
|
|
}
|
|
|
|
private func drawClues(cellSide: CGFloat, context: inout GraphicsContext) {
|
|
let fontSize = max(14, min(24, cellSide * 0.36))
|
|
|
|
for (cell, value) in game.puzzle.clues {
|
|
let point = CGPoint(
|
|
x: (CGFloat(cell.col) + 0.5) * cellSide,
|
|
y: (CGFloat(cell.row) + 0.5) * cellSide
|
|
)
|
|
let text = Text("\(value)")
|
|
.font(.system(size: fontSize, weight: .bold, design: .rounded))
|
|
.foregroundStyle(ShikakuTheme.clue)
|
|
|
|
context.draw(text, at: point, anchor: .center)
|
|
}
|
|
}
|
|
|
|
private func cgRect(for bounds: CellBounds, cellSide: CGFloat) -> CGRect {
|
|
CGRect(
|
|
x: CGFloat(bounds.startCol) * cellSide,
|
|
y: CGFloat(bounds.startRow) * cellSide,
|
|
width: CGFloat(bounds.width) * cellSide,
|
|
height: CGFloat(bounds.height) * cellSide
|
|
)
|
|
}
|
|
|
|
private func cell(at location: CGPoint, side: CGFloat) -> GridPoint? {
|
|
guard location.x >= 0,
|
|
location.y >= 0,
|
|
location.x <= side,
|
|
location.y <= side else {
|
|
return nil
|
|
}
|
|
|
|
let cellSide = side / CGFloat(game.size)
|
|
let row = min(Int(location.y / cellSide), game.size - 1)
|
|
let col = min(Int(location.x / cellSide), game.size - 1)
|
|
return GridPoint(row: row, col: col)
|
|
}
|
|
}
|