CellPatterns.swift (4105B)
1 import SwiftUI 2 3 /// The ordered palette of passive cross-reference textures. A puzzle can 4 /// have several independent cross-reference groups; each is assigned the 5 /// next pattern in this sequence (wrapping if there are more groups than 6 /// patterns) so distinct links read differently at a glance. Monochrome 7 /// by design — the texture layers over any cursor/author colour without 8 /// competing with the app's colour-coded state. 9 /// 10 /// Cell content is painted by `PuzzleCellsLayer` (a single `Canvas`); this 11 /// type and `CrossRefLines` are the drawing primitives it reuses. 12 enum CrossRefPattern: CaseIterable, Sendable { 13 case diagonalDown 14 case diagonalUp 15 case crosshatch 16 case horizontal 17 case vertical 18 } 19 20 /// Parallel lines across a cell at one of four orientations. The line 21 /// family is anchored to a grid-wide lattice derived from the cell's 22 /// position rather than its own origin, so adjacent cells draw the same 23 /// family and the hatching meets exactly at every border. Runs are 24 /// generated past the edges and clipped by the caller. 25 struct CrossRefLines: Shape { 26 enum Slope { case down, up, horizontal, vertical } 27 var slope: Slope 28 /// This cell's grid position and the grid's inter-cell spacing. The 29 /// lattice phase is computed from these so every cell, at whatever 30 /// size the layout currently gives it, lands on the same global 31 /// family of lines. 32 var row: Int 33 var col: Int 34 var spacing: CGFloat 35 var spacingFraction: CGFloat = 0.24 36 37 func path(in rect: CGRect) -> Path { 38 var path = Path() 39 let side = min(rect.width, rect.height) 40 let step = max(2, side * spacingFraction) 41 // Cells are square and uniformly pitched, so one pitch value 42 // covers both axes. Computed from the live rect, so it tracks 43 // any change in cell size automatically. 44 let pitch = side + spacing 45 let gx = CGFloat(col) * pitch 46 let gy = CGFloat(row) * pitch 47 48 switch slope { 49 case .down: 50 // Lines of constant (globalX − globalY); snap the generator 51 // so that quantity is a multiple of `step` grid-wide. 52 let lower = rect.minX - rect.height 53 var x = firstAligned(at: lower, congruentTo: rect.minX - (gx - gy), step: step) 54 while x <= rect.maxX { 55 path.move(to: CGPoint(x: x, y: rect.minY)) 56 path.addLine(to: CGPoint(x: x + rect.height, y: rect.maxY)) 57 x += step 58 } 59 case .up: 60 // Lines of constant (globalX + globalY). 61 let lower = rect.minX - rect.height 62 var x = firstAligned(at: lower, congruentTo: lower - (gx + gy), step: step) 63 while x <= rect.maxX { 64 path.move(to: CGPoint(x: x, y: rect.maxY)) 65 path.addLine(to: CGPoint(x: x + rect.height, y: rect.minY)) 66 x += step 67 } 68 case .horizontal: 69 var y = firstAligned(at: rect.minY, congruentTo: rect.minY - gy, step: step) 70 while y < rect.maxY { 71 path.move(to: CGPoint(x: rect.minX, y: y)) 72 path.addLine(to: CGPoint(x: rect.maxX, y: y)) 73 y += step 74 } 75 case .vertical: 76 var x = firstAligned(at: rect.minX, congruentTo: rect.minX - gx, step: step) 77 while x < rect.maxX { 78 path.move(to: CGPoint(x: x, y: rect.minY)) 79 path.addLine(to: CGPoint(x: x, y: rect.maxY)) 80 x += step 81 } 82 } 83 return path 84 } 85 86 /// Smallest value ≥ `lower` that is congruent to `target` modulo 87 /// `step`. Starting each run here puts every cell's lines on one 88 /// shared global lattice, so they line up across cell borders. 89 private func firstAligned( 90 at lower: CGFloat, 91 congruentTo target: CGFloat, 92 step: CGFloat 93 ) -> CGFloat { 94 let delta = (target - lower).truncatingRemainder(dividingBy: step) 95 let offset = delta >= 0 ? delta : delta + step 96 return lower + offset 97 } 98 }