1
0

Path.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. import Foundation // for sin and cos
  2. public enum StrokeCap: Sendable {
  3. /// The stroke ends square exactly at the last point.
  4. case butt
  5. /// The stroke ends with a semicircle.
  6. case round
  7. /// The stroke ends square half of the stroke width past the last point.
  8. case square
  9. }
  10. public enum StrokeJoin: Sendable {
  11. /// Corners are sharp, unless they are longer than `limit` times half the stroke width,
  12. /// in which case they are beveled.
  13. case miter(limit: Double)
  14. /// Corners are rounded.
  15. case round
  16. /// Corners are beveled.
  17. case bevel
  18. }
  19. public struct StrokeStyle: Sendable {
  20. public var width: Double
  21. public var cap: StrokeCap
  22. public var join: StrokeJoin
  23. public init(width: Double, cap: StrokeCap = .butt, join: StrokeJoin = .miter(limit: 10.0)) {
  24. self.width = width
  25. self.cap = cap
  26. self.join = join
  27. }
  28. }
  29. /// An enum describing how a path is shaded.
  30. public enum FillRule: Sendable {
  31. /// A region is shaded if it is enclosed an odd number of times.
  32. case evenOdd
  33. /// A region is shaded if it is enclosed at all.
  34. ///
  35. /// This is also known as the "non-zero" rule.
  36. case winding
  37. }
  38. /// A type representing an affine transformation on a 2-D point.
  39. ///
  40. /// Performing an affine transform consists of multiplying the matrix ``linearTransform``
  41. /// by the point as a column vector, then adding ``translation``.
  42. public struct AffineTransform: Equatable, Sendable, CustomDebugStringConvertible {
  43. /// The linear transformation. This is a 2x2 matrix stored in row-major order.
  44. ///
  45. /// The four properties (`x`, `y`, `z`, `w`) correspond to the 2x2 matrix as follows:
  46. /// ```
  47. /// [ x y ]
  48. /// [ z w ]
  49. /// ```
  50. /// - Remark: The matrices in some graphics frameworks, such as WinUI's `Matrix` and
  51. /// CoreGraphics' `CGAffineTransform`, take the transpose of this matrix. The reason for
  52. /// this difference is left- vs right-multiplication; the values are identical.
  53. public var linearTransform: SIMD4<Double>
  54. /// The translation applied after the linear transformation.
  55. public var translation: SIMD2<Double>
  56. public init(linearTransform: SIMD4<Double>, translation: SIMD2<Double>) {
  57. self.linearTransform = linearTransform
  58. self.translation = translation
  59. }
  60. public static func translation(x: Double, y: Double) -> AffineTransform {
  61. AffineTransform(
  62. linearTransform: SIMD4(x: 1.0, y: 0.0, z: 0.0, w: 1.0),
  63. translation: SIMD2(x: x, y: y)
  64. )
  65. }
  66. public static func scaling(by factor: Double) -> AffineTransform {
  67. AffineTransform(
  68. linearTransform: SIMD4(x: factor, y: 0.0, z: 0.0, w: factor),
  69. translation: .zero
  70. )
  71. }
  72. public static func rotation(radians: Double, center: SIMD2<Double>) -> AffineTransform {
  73. let sine = sin(radians)
  74. let cosine = cos(radians)
  75. return AffineTransform(
  76. linearTransform: SIMD4(x: cosine, y: -sine, z: sine, w: cosine),
  77. translation: SIMD2(
  78. x: -center.x * cosine + center.y * sine + center.x,
  79. y: -center.x * sine - center.y * cosine + center.y
  80. )
  81. )
  82. }
  83. public static func rotation(degrees: Double, center: SIMD2<Double>) -> AffineTransform {
  84. rotation(radians: degrees * (.pi / 180.0), center: center)
  85. }
  86. public static let identity = AffineTransform(
  87. linearTransform: SIMD4(x: 1.0, y: 0.0, z: 0.0, w: 1.0),
  88. translation: .zero
  89. )
  90. public func inverted() -> AffineTransform? {
  91. let determinant =
  92. linearTransform.x * linearTransform.w - linearTransform.y * linearTransform.z
  93. if determinant == 0.0 {
  94. return nil
  95. }
  96. return AffineTransform(
  97. linearTransform: SIMD4(
  98. x: linearTransform.w,
  99. y: -linearTransform.y,
  100. z: -linearTransform.z,
  101. w: linearTransform.x
  102. ) / determinant,
  103. translation: SIMD2(
  104. x: (linearTransform.y * translation.y - linearTransform.w * translation.x),
  105. y: (linearTransform.z * translation.x - linearTransform.x * translation.y)
  106. ) / determinant
  107. )
  108. }
  109. public func followedBy(_ other: AffineTransform) -> AffineTransform {
  110. // Composing two transformations is equivalent to forming the 3x3 matrix shown by
  111. // `debugDescription`, then multiplying `other * self` (the left matrix is applied
  112. // after the right matrix).
  113. return AffineTransform(
  114. linearTransform: SIMD4(
  115. x: other.linearTransform.x * linearTransform.x + other.linearTransform.y
  116. * linearTransform.z,
  117. y: other.linearTransform.x * linearTransform.y + other.linearTransform.y
  118. * linearTransform.w,
  119. z: other.linearTransform.z * linearTransform.x + other.linearTransform.w
  120. * linearTransform.z,
  121. w: other.linearTransform.z * linearTransform.y + other.linearTransform.w
  122. * linearTransform.w
  123. ),
  124. translation: SIMD2(
  125. x: other.linearTransform.x * translation.x + other.linearTransform.y * translation.y
  126. + other.translation.x,
  127. y: other.linearTransform.z * translation.x + other.linearTransform.w * translation.y
  128. + other.translation.y
  129. )
  130. )
  131. }
  132. public var debugDescription: String {
  133. let numberFormat = "%.5g"
  134. let a = String(format: numberFormat, linearTransform.x)
  135. let b = String(format: numberFormat, linearTransform.y)
  136. let c = String(format: numberFormat, linearTransform.z)
  137. let d = String(format: numberFormat, linearTransform.w)
  138. let tx = String(format: numberFormat, translation.x)
  139. let ty = String(format: numberFormat, translation.y)
  140. let zero = String(format: numberFormat, 0.0)
  141. let one = String(format: numberFormat, 1.0)
  142. let maxLength = [a, b, c, d, tx, ty, zero, one].map(\.count).max()!
  143. func pad(_ s: String) -> String {
  144. String(repeating: " ", count: maxLength - s.count) + s
  145. }
  146. return """
  147. [ \(pad(a)) \(pad(b)) \(pad(tx)) ]
  148. [ \(pad(c)) \(pad(d)) \(pad(ty)) ]
  149. [ \(pad(zero)) \(pad(zero)) \(pad(one)) ]
  150. """
  151. }
  152. }
  153. public struct Path: Sendable {
  154. /// A rectangle in 2D space.
  155. ///
  156. /// This type is inspired by `CGRect`.
  157. public struct Rect: Equatable, Sendable {
  158. /// The rectangle's origin (its top-leading corner), as an x/y vector.
  159. public var origin: SIMD2<Double>
  160. /// The rectangle's size, as a width/height vector.
  161. public var size: SIMD2<Double>
  162. /// Creates a ``Path/Rect`` instance.
  163. ///
  164. /// - Parameters:
  165. /// - origin: The rectangle's origin (its top-leading corner), as an
  166. /// x/y vector.
  167. /// - size: The rectangle's size, as a width/height vector.
  168. public init(origin: SIMD2<Double>, size: SIMD2<Double>) {
  169. self.origin = origin
  170. self.size = size
  171. }
  172. /// The X position of the rectangle's leading edge.
  173. public var x: Double { origin.x }
  174. /// The Y position of the rectangle's top edge.
  175. public var y: Double { origin.y }
  176. /// The rectangle's width.
  177. public var width: Double { size.x }
  178. /// The rectangle's height.
  179. public var height: Double { size.y }
  180. /// The position of the rectangle's center.
  181. public var center: SIMD2<Double> { size * 0.5 + origin }
  182. /// The X position of the rectangle's trailing edge.
  183. public var maxX: Double { size.x + origin.x }
  184. /// The Y position of the rectangle's bottom edge.
  185. public var maxY: Double { size.y + origin.y }
  186. /// Creates a ``Path/Rect`` instance.
  187. ///
  188. /// - Parameters:
  189. /// - x: The X position of the rectangle's leading edge.
  190. /// - y: The Y position of the rectangle's top edge.
  191. /// - width: The rectangle's width.
  192. /// - height: The rectangle's height.
  193. public init(x: Double, y: Double, width: Double, height: Double) {
  194. origin = SIMD2(x: x, y: y)
  195. size = SIMD2(x: width, y: height)
  196. }
  197. }
  198. /// The types of actions that can be performed on a path.
  199. ///
  200. /// The first action's starting point is (0, 0).
  201. public enum Action: Equatable, Sendable {
  202. /// Moves to the specified point without drawing anything.
  203. case moveTo(SIMD2<Double>)
  204. /// Draws a line from the path's current point to the specified point.
  205. case lineTo(SIMD2<Double>)
  206. /// Draws an order-2 curve from the path's current point, bending
  207. /// towards `control`, and ending at `endPoint`.
  208. ///
  209. /// After this, the path's current point will be `endPoint`.
  210. case quadCurve(control: SIMD2<Double>, end: SIMD2<Double>)
  211. /// Draws an order-3 curve starting at the path's current point, bending
  212. /// towards `control1` and `control2`, and ending at `endPoint`.
  213. ///
  214. /// After this, the path's current point will be `endPoint`.
  215. case cubicCurve(
  216. control1: SIMD2<Double>,
  217. control2: SIMD2<Double>,
  218. end: SIMD2<Double>
  219. )
  220. /// Draws a rectangle.
  221. case rectangle(Rect)
  222. /// Draws a circle with the specified `center` and `radius`.
  223. case circle(center: SIMD2<Double>, radius: Double)
  224. /// Draws an arc segment with the specified `center` and `radius`,
  225. /// starting at `startAngle` and ending at `endAngle`.
  226. ///
  227. /// `startAngle` and `endAngle` are measured in radians clockwise from
  228. /// the trailing direction, and must be between 0 and 2π, inclusive.
  229. ///
  230. /// If `clockwise` is `true`, the arc is drawn in a clockwise direction;
  231. /// otherwise, it is drawn counterclockwise.
  232. case arc(
  233. center: SIMD2<Double>,
  234. radius: Double,
  235. startAngle: Double,
  236. endAngle: Double,
  237. clockwise: Bool
  238. )
  239. /// Applies a transform to the currently drawn path.
  240. case transform(AffineTransform)
  241. /// Performs each action in order.
  242. ///
  243. /// ``transform(_:)`` actions inside of a `subpath` do not affect
  244. /// the outer path.
  245. case subpath([Action])
  246. }
  247. /// A list of every action that has been performed on this path.
  248. ///
  249. /// This property is meant for backends implementing paths. If the backend
  250. /// has a similar path type built-in (such as `UIBezierPath` or
  251. /// `GskPathBuilder`), constructing the path should consist of looping over
  252. /// this array and calling the method that corresponds to each action.
  253. public private(set) var actions: [Action] = []
  254. /// The fill rule for this path.
  255. public private(set) var fillRule: FillRule = .evenOdd
  256. /// The stroke style for this path.
  257. public private(set) var strokeStyle = StrokeStyle(width: 1.0)
  258. /// Creates an empty ``Path`` instance.
  259. public init() {}
  260. /// Move the path's current point to the given point.
  261. ///
  262. /// This does not draw a line segment. For that, see ``addLine(to:)``.
  263. ///
  264. /// If ``addLine(to:)``, ``addQuadCurve(control:to:)``,
  265. /// ``addCubicCurve(control1:control2:to:)``, or
  266. /// ``addArc(center:radius:startAngle:endAngle:clockwise:)`` is called on an
  267. /// empty path without calling this method first, the start point is
  268. /// implicitly (0, 0).
  269. ///
  270. /// - Parameter point: The point to move to.
  271. /// - Returns: The updated path.
  272. public consuming func move(to point: SIMD2<Double>) -> Path {
  273. actions.append(.moveTo(point))
  274. return self
  275. }
  276. /// Add a line segment from the current point to the given point.
  277. ///
  278. /// After this, the path's current point will be the endpoint of this line
  279. /// segment.
  280. ///
  281. /// - Parameter point: The point to draw the line to.
  282. /// - Returns: The updated path.
  283. public consuming func addLine(to point: SIMD2<Double>) -> Path {
  284. actions.append(.lineTo(point))
  285. return self
  286. }
  287. /// Add a quadratic Bézier curve to the path.
  288. ///
  289. /// This creates an order-2 curve starting at the path's current point,
  290. /// bending towards `control`, and ending at `endPoint`. After this, the
  291. /// path's current point will be `endPoint`.
  292. ///
  293. /// - Parameters:
  294. /// - control: The control point.
  295. /// - endPoint: The end point.
  296. /// - Returns: The updated path.
  297. public consuming func addQuadCurve(
  298. control: SIMD2<Double>,
  299. to endPoint: SIMD2<Double>
  300. ) -> Path {
  301. actions.append(.quadCurve(control: control, end: endPoint))
  302. return self
  303. }
  304. /// Add a cubic Bézier curve to the path.
  305. ///
  306. /// This creates an order-3 curve starting at the path's current point,
  307. /// bending towards `control1` and `control2`, and ending at `endPoint`.
  308. /// After this, the path's current point will be `endPoint`.
  309. ///
  310. /// - Parameters:
  311. /// - control1: The first control point.
  312. /// - control2: The second control point.
  313. /// - endPoint: The end point.
  314. /// - Returns: The updated path.
  315. public consuming func addCubicCurve(
  316. control1: SIMD2<Double>,
  317. control2: SIMD2<Double>,
  318. to endPoint: SIMD2<Double>
  319. ) -> Path {
  320. actions.append(.cubicCurve(control1: control1, control2: control2, end: endPoint))
  321. return self
  322. }
  323. /// Adds a rectangle to the path.
  324. ///
  325. /// - Parameter rect: The rectangle to add.
  326. /// - Returns: The updated path.
  327. public consuming func addRectangle(_ rect: Rect) -> Path {
  328. actions.append(.rectangle(rect))
  329. return self
  330. }
  331. /// Adds a circle to the path.
  332. ///
  333. /// - Parameters:
  334. /// - center: The circle's center.
  335. /// - radius: The circle's radius.
  336. /// - Returns: The updated path.
  337. public consuming func addCircle(center: SIMD2<Double>, radius: Double) -> Path {
  338. actions.append(.circle(center: center, radius: radius))
  339. return self
  340. }
  341. /// Add an arc segment to the path.
  342. ///
  343. /// After this, the path's current point will be the endpoint implied by
  344. /// `center`, `radius`, and `endAngle`.
  345. ///
  346. /// - Parameters:
  347. /// - center: The location of the center of the circle.
  348. /// - radius: The radius of the circle.
  349. /// - startAngle: The angle of the start of the arc, measured in radians
  350. /// clockwise from the trailing direction. Must be between 0 and 2π,
  351. /// inclusive.
  352. /// - endAngle: The angle of the end of the arc, measured in radians
  353. /// clockwise from the trailing direction. Must be between 0 and 2π,
  354. /// inclusive.
  355. /// - clockwise: `true` if the arc is to be drawn clockwise, `false` if
  356. /// the arc is to be drawn counter-clockwise. Used to determine which of
  357. /// the two possible arcs to draw between the given start and end
  358. /// angles.
  359. /// - Returns: The updated path.
  360. public consuming func addArc(
  361. center: SIMD2<Double>,
  362. radius: Double,
  363. startAngle: Double,
  364. endAngle: Double,
  365. clockwise: Bool
  366. ) -> Path {
  367. assert((0.0...(2.0 * .pi)).contains(startAngle) && (0.0...(2.0 * .pi)).contains(endAngle))
  368. actions.append(
  369. .arc(
  370. center: center,
  371. radius: radius,
  372. startAngle: startAngle,
  373. endAngle: endAngle,
  374. clockwise: clockwise
  375. )
  376. )
  377. return self
  378. }
  379. /// Apply the given transform to the segments in the path so far.
  380. ///
  381. /// While this may adjust the path's current point, it does not otherwise
  382. /// affect segments that are added to the path after this method call.
  383. ///
  384. /// - Parameter transform: The transform to apply.
  385. /// - Returns: The updated path.
  386. public consuming func applyTransform(_ transform: AffineTransform) -> Path {
  387. actions.append(.transform(transform))
  388. return self
  389. }
  390. /// Add the entirety of another path as part of this path.
  391. ///
  392. /// This can be necessary to section off transforms, as transforms applied
  393. /// to `subpath` will not affect this path.
  394. ///
  395. /// The fill rule and preferred stroke style of the subpath are ignored.
  396. ///
  397. /// - Parameter subpath: The subpath to add.
  398. /// - Returns: The updated path.
  399. public consuming func addSubpath(_ subpath: Path) -> Path {
  400. actions.append(.subpath(subpath.actions))
  401. return self
  402. }
  403. /// Set the default stroke style for the path.
  404. ///
  405. /// This is not necessarily respected; it can be overridden by
  406. /// ``Shape/stroke(_:style:)``, and is lost when the path is passed to
  407. /// ``addSubpath(_:)``.
  408. ///
  409. /// - Parameter style: The stroke style to set.
  410. /// - Returns: The updated path.
  411. public consuming func stroke(style: StrokeStyle) -> Path {
  412. strokeStyle = style
  413. return self
  414. }
  415. /// Set the fill rule for the path.
  416. ///
  417. /// - Parameter rule: The fill rule to set.
  418. /// - Returns: The updated path.
  419. public consuming func fillRule(_ rule: FillRule) -> Path {
  420. fillRule = rule
  421. return self
  422. }
  423. }
  424. extension Path {
  425. /// Conditionally modifies a path.
  426. ///
  427. /// - Parameters:
  428. /// - condition: The condition to check.
  429. /// - ifTrue: The action to perform if `condition` is `true`. Receives
  430. /// a copy of the path.
  431. /// - ifFalse: The action to perform if `condition` is `false`. Receives
  432. /// a copy of the path. Defaults to leaving the path unchanged.
  433. /// - Returns: The updated path.
  434. @inlinable
  435. public consuming func `if`(
  436. _ condition: Bool,
  437. then ifTrue: (consuming Path) throws -> Path,
  438. else ifFalse: (consuming Path) throws -> Path = { $0 }
  439. ) rethrows -> Path {
  440. if condition {
  441. try ifTrue(self)
  442. } else {
  443. try ifFalse(self)
  444. }
  445. }
  446. }