Cyborg/DrawingCommand.swift (502 lines of code) (raw):
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreGraphics
import Foundation
enum DrawingCommand: Equatable {
case move(CGPoint)
case moveAbsolute(CGPoint)
case curve(CGPoint, CGPoint, CGPoint)
case curveAbsolute(CGPoint, CGPoint, CGPoint)
case line(CGPoint)
case lineAbsolute(CGPoint)
case horizontal(CGFloat)
case horizontalAbsolute(CGFloat)
case vertical(CGFloat)
case verticalAbsolute(CGFloat)
case smoothCurve(CGPoint, CGPoint)
case smoothCurveAbsolute(CGPoint, CGPoint)
case quadratic(CGPoint, CGPoint)
case quadraticAbsolute(CGPoint, CGPoint)
case smoothQuadratic(CGPoint)
case smoothQuadraticAbsolute(CGPoint)
case closePath
case closePathAbsolute
case arc(CGPoint, CGFloat, CGFloat, CGFloat, CGPoint)
case arcAbsolute(CGPoint, CGFloat, CGFloat, CGFloat, CGPoint)
func apply(to path: CGMutablePath, using prior: PriorContext, in size: CGSize) -> PriorContext {
switch self {
case .move(let point):
let next = point.times(size).add(prior.point)
path.move(to: next)
return next.asPriorContext
case .moveAbsolute(let point):
let next = point.times(size)
path.move(to: next)
return next.asPriorContext
case .curve(let control1, let control2, let end):
let intoAbsolute = prior.point
let control1 = control1.times(size).add(intoAbsolute)
let control2 = control2.times(size).add(intoAbsolute)
let end = end.times(size).add(intoAbsolute)
path.addCurve(to: end, control1: control1, control2: control2)
return .lastAndControlPoint(end, control2.reflected(across: end))
case .curveAbsolute(let control1, let control2, let end):
let control1 = control1.times(size)
let control2 = control2.times(size)
let end = end.times(size)
path.addCurve(to: end, control1: control1, control2: control2)
return .lastAndControlPoint(end, control2.reflected(across: end))
case .line(let point):
let point = point.times(size).add(prior.point)
path.addLine(to: point)
return point.asPriorContext
case .lineAbsolute(let point):
let point = point.times(size)
path.addLine(to: point)
return point.asPriorContext
case .horizontal(let magnitude):
let last = prior.point
let next = CGPoint(x: magnitude * size.width + last.x, y: last.y)
path.addLine(to: next)
return next.asPriorContext
case .horizontalAbsolute(let magnitude):
let last = prior.point
let next = CGPoint(x: magnitude * size.width, y: last.y)
path.addLine(to: next)
return next.asPriorContext
case .vertical(let magnitude):
let last = prior.point
let next = CGPoint(x: last.x, y: magnitude * size.height + last.y)
path.addLine(to: next)
return next.asPriorContext
case .verticalAbsolute(let magnitude):
let last = prior.point
let next = CGPoint(x: last.x, y: magnitude * size.height)
path.addLine(to: next)
return next.asPriorContext
case .smoothCurve(let control2, let end):
let (last, control1) = prior.pointAndControlPoint
let end = end.times(size).add(last)
let control2 = control2.times(size).add(last)
path.addCurve(to: end, control1: control1, control2: control2)
return .lastAndControlPoint(end,
control2.reflected(across: end))
case .smoothCurveAbsolute(let control2, let end):
let (_, control1) = prior.pointAndControlPoint
let end = end.times(size)
let control2 = control2.times(size)
path.addCurve(to: end, control1: control1, control2: control2)
return .lastAndControlPoint(end,
control2.reflected(across: end))
case .quadratic(let control1, let end):
let last = prior.point
let end = end.times(size).add(last)
let control1 = control1.times(size).add(last)
path.addQuadCurve(to: end, control: control1)
return .lastAndControlPoint(end,
control1.reflected(across: end))
case .quadraticAbsolute(let control1, let end):
let end = end.times(size)
let control1 = control1.times(size)
path.addQuadCurve(to: end, control: control1)
return .lastAndControlPoint(end,
control1.reflected(across: end))
case .arc(let radius, let rotation, let largeArcFlag, let sweepFlag, let endPoint):
return applyArc(to: path,
in: size,
radius: radius,
rotation: rotation,
largeArcFlag: largeArcFlag,
sweepFlag: sweepFlag,
endPoint: endPoint,
prior: prior,
isRelative: true)
case .arcAbsolute(let radius, let rotation, let largeArcFlag, let sweepFlag, let endPoint):
return applyArc(to: path,
in: size,
radius: radius,
rotation: rotation,
largeArcFlag: largeArcFlag,
sweepFlag: sweepFlag,
endPoint: endPoint,
prior: prior,
isRelative: false)
case .smoothQuadratic(let end):
let (last, control) = prior.pointAndControlPoint
let end = end.times(size).add(last)
path.addQuadCurve(to: end,
control: control)
return .lastAndControlPoint(end,
control.reflected(across: end))
case .smoothQuadraticAbsolute(let end):
let (_, control) = prior.pointAndControlPoint
let end = end.times(size)
path.addQuadCurve(to: end,
control: control)
return .lastAndControlPoint(end,
control.reflected(across: end))
case .closePath, .closePathAbsolute:
path.closeSubpath()
return path.currentPoint.asPriorContext
}
}
}
enum PriorContext: Equatable {
case last(CGPoint)
case lastAndControlPoint(CGPoint, CGPoint)
var point: CGPoint {
switch self {
case .last(let point): return point
case .lastAndControlPoint(let point, _): return point
}
}
var pointAndControlPoint: (point: CGPoint, controlPoint: CGPoint) {
switch self {
// per the spec, if there is no last control point, the
// control point is coincident to the last point
case .last(let point): return (point, point)
case .lastAndControlPoint(let point, let controlPoint): return (point, controlPoint)
}
}
static let zero: PriorContext = .last(.zero)
static func == (lhs: PriorContext, rhs: PriorContext) -> Bool {
switch (lhs, rhs) {
case (.last(let lhs), .last(let rhs)): return lhs == rhs
case (.lastAndControlPoint(let lhsPoint, let lhsControl),
.lastAndControlPoint(let rhsPoint, let rhsControl)): return lhsPoint == rhsPoint && lhsControl == rhsControl
default: return false
}
}
}
extension CGPoint {
var asPriorContext: PriorContext {
return .last(self)
}
}
typealias PathSegment = [DrawingCommand]
func parse<T>(command: XMLString,
followedBy: @escaping Parser<T>,
convertToPathCommandsWith convert: @escaping (T) -> PathSegment) -> Parser<PathSegment> {
return { stream, index in
literal(command)(stream, index)
.chain(into: stream) { stream, index in
followedBy(stream, index)
.map { result, index in
.ok(convert(result), index)
}
}
}
}
func parseCurve() -> Parser<PathSegment> {
return parse(command: .c,
followedBy: 3.coordinatePairs(),
convertToPathCommandsWith: { (points: [[CGPoint]]) -> PathSegment in
points.map { points in
let control1 = points[0],
control2 = points[1],
end = points[2]
return .curve(control1, control2, end)
}
})
}
func parseAbsoluteCurve() -> Parser<PathSegment> {
return parse(command: .C,
followedBy: 3.coordinatePairs(),
convertToPathCommandsWith: { (points: [[CGPoint]]) -> PathSegment in
points.map { points in
let control1 = points[0],
control2 = points[1],
end = points[2]
return .curveAbsolute(control1, control2, end)
}
})
}
func parseMoveAbsolute() -> Parser<PathSegment> {
return parse(command: .M,
followedBy: 1.coordinatePairs(),
convertToPathCommandsWith: { (points) -> PathSegment in
points.map { points in
.moveAbsolute(points[0])
}
})
}
func parseMove() -> Parser<PathSegment> {
return parse(command: .m,
followedBy: 1.coordinatePairs(),
convertToPathCommandsWith: { (points) -> PathSegment in
points.map { points in
.move(points[0])
}
})
}
func parseLine() -> Parser<PathSegment> {
return parse(command: .l,
followedBy: oneOrMore(of: coordinatePair()),
convertToPathCommandsWith: { (points: [CGPoint]) -> PathSegment in
points.map { point in
.line(point)
}
})
}
func parseLineAbsolute() -> Parser<PathSegment> {
return parse(command: .L,
followedBy: oneOrMore(of: coordinatePair()),
convertToPathCommandsWith: { (points: [CGPoint]) -> PathSegment in
points.map { point in
.lineAbsolute(point)
}
})
}
func parseClosePath() -> Parser<PathSegment> {
return parse(command: .z,
followedBy: empty(),
convertToPathCommandsWith: {
[.closePath]
})
}
func parseClosePathAbsolute() -> Parser<PathSegment> {
return parse(command: .Z,
followedBy: empty(),
convertToPathCommandsWith: {
[.closePathAbsolute]
})
}
func parseHorizontal() -> Parser<PathSegment> {
return parse(command: .h,
followedBy: numbers(),
convertToPathCommandsWith: { (numbers: [CGFloat]) -> PathSegment in
numbers.map { magnitude in
.horizontal(magnitude)
}
})
}
func parseHorizontalAbsolute() -> Parser<PathSegment> {
return parse(command: .H,
followedBy: numbers(),
convertToPathCommandsWith: { (numbers: [CGFloat]) -> PathSegment in
numbers.map { magnitude in
.horizontalAbsolute(magnitude)
}
})
}
func parseVertical() -> Parser<PathSegment> {
return parse(command: .v,
followedBy: numbers(),
convertToPathCommandsWith: { (numbers: [CGFloat]) -> PathSegment in
numbers.map { magnitude in
.vertical(magnitude)
}
})
}
func parseVerticalAbsolute() -> Parser<PathSegment> {
return parse(command: .V,
followedBy: numbers(),
convertToPathCommandsWith: { (numbers: [CGFloat]) -> PathSegment in
numbers.map { magnitude in
.verticalAbsolute(magnitude)
}
})
}
func parseSmoothCurve() -> Parser<PathSegment> {
return parse(command: .s,
followedBy: 2.coordinatePairs(),
convertToPathCommandsWith: { (points: [[CGPoint]]) -> PathSegment in
points.map { pair in
let end = pair[1],
controlPoint = pair[0]
return .smoothCurve(controlPoint, end)
}
})
}
func parseSmoothQuadraticCurveAbsolute() -> Parser<PathSegment> {
parse(command: .T,
followedBy: oneOrMore(of: coordinatePair()),
convertToPathCommandsWith: { (points: [CGPoint]) -> PathSegment in
points.map { point in
.smoothQuadraticAbsolute(point)
}
})
}
func parseSmoothQuadraticCurve() -> Parser<PathSegment> {
parse(command: .t,
followedBy: oneOrMore(of: coordinatePair()),
convertToPathCommandsWith: { (points: [CGPoint]) -> PathSegment in
points.map { point in
.smoothQuadratic(point)
}
})
}
func parseSmoothCurveAbsolute() -> Parser<PathSegment> {
return parse(command: .S,
followedBy: 2.coordinatePairs(),
convertToPathCommandsWith: { (points: [[CGPoint]]) -> PathSegment in
points.map { pair in
let end = pair[1],
controlPoint = pair[0]
return .smoothCurveAbsolute(controlPoint, end)
}
})
}
func parseQuadratic() -> Parser<PathSegment> {
return parse(command: .q,
followedBy: 2.coordinatePairs(),
convertToPathCommandsWith: { (points: [[CGPoint]]) -> PathSegment in
points.map { pair in
let end = pair[1],
controlPoint1 = pair[0]
return .quadratic(controlPoint1, end)
}
})
}
func parseQuadraticAbsolute() -> Parser<PathSegment> {
return parse(command: .Q,
followedBy: 2.coordinatePairs(),
convertToPathCommandsWith: { (points: [[CGPoint]]) -> PathSegment in
points.map { pair in
let end = pair[1],
controlPoint1 = pair[0]
return .quadraticAbsolute(controlPoint1, end)
}
})
}
func parseArc() -> Parser<PathSegment> {
return parse(command: .a,
followedBy: oneOrMore(of: arcParser),
convertToPathCommandsWith: { (result) -> PathSegment in
result.map { result in
let (radius, rotation, largeArcFlag, sweepFlag, endPoint) = result
return .arc(radius, rotation, largeArcFlag, sweepFlag, endPoint)
}
})
}
func parseArcAbsolute() -> Parser<PathSegment> {
return parse(command: .A,
followedBy: oneOrMore(of: arcParser),
convertToPathCommandsWith: { (result) -> PathSegment in
result.map { result in
let (radius, rotation, largeArcFlag, sweepFlag, endPoint) = result
return .arcAbsolute(radius, rotation, largeArcFlag, sweepFlag, endPoint)
}
})
}
func arcParser(_ string: XMLString, _ index: Int32) -> ParseResult<(CGPoint, CGFloat, CGFloat, CGFloat, CGPoint)> {
switch coordinatePair()(string, index) {
case .ok(let radius, let index):
switch number(from: string, at: index) {
case .ok(let rotation, let index):
switch number(from: string, at: index) {
case .ok(let arcFlagNumber, let index):
switch number(from: string, at: index) {
case .ok(let sweepFlagNumber, let index):
switch coordinatePair()(string, index) {
case .ok(let endPoint, let index):
return .ok((radius, rotation, arcFlagNumber, sweepFlagNumber, endPoint), index)
case .error(let error):
return .error(error)
}
case .error(let error):
return .error(error)
}
case .error(let error):
return .error(error)
}
case .error(let error):
return .error(error)
}
case .error(let error):
return .error(error)
}
}
let allDrawingCommands: [Parser<PathSegment>] = [
parseCurve(),
parseAbsoluteCurve(),
parseLine(),
parseLineAbsolute(),
parseMove(),
parseMoveAbsolute(),
parseHorizontal(),
parseHorizontalAbsolute(),
parseVertical(),
parseVerticalAbsolute(),
parseQuadratic(),
parseQuadraticAbsolute(),
parseSmoothCurve(),
parseSmoothCurveAbsolute(),
parseClosePath(),
parseClosePathAbsolute(),
parseArc(),
parseArcAbsolute(),
parseSmoothQuadraticCurve(),
parseSmoothQuadraticCurveAbsolute(),
]
extension CGPoint {
func add(_ rhs: CGPoint) -> CGPoint {
.init(x: x + rhs.x, y: y + rhs.y)
}
func subtract(_ rhs: CGPoint) -> CGPoint {
.init(x: x - rhs.x, y: y - rhs.y)
}
func times(_ x1: CGFloat, _ y1: CGFloat) -> CGPoint {
.init(x: x * x1, y: y * y1)
}
func times(_ size: CGSize) -> CGPoint {
.init(x: x * size.width, y: y * size.height)
}
func times(_ other: CGFloat) -> CGPoint {
.init(x: x * other, y: y * other)
}
func times(_ other: CGPoint) -> CGPoint {
times(other.x, other.y)
}
func dot(_ other: CGPoint) -> CGFloat {
(x * other.x) + (y * other.y)
}
func reflected(across current: CGPoint) -> CGPoint {
let newX = current.x * 2 - x
let newY = current.y * 2 - y
return CGPoint(x: newX, y: newY)
}
var magnitude: CGFloat {
return sqrt(pow(x, 2) + pow(y, 2))
}
func angle(with other: CGPoint) -> CGFloat {
let sign: CGFloat = ((x * other.y - y * other.x) < 0) ? -1 : 1
return acos(max(-1.0, min(1.0, dot(other) / (magnitude * other.magnitude)))) * sign
}
func isWithinAPointOf(_ other: CGPoint) -> Bool {
abs(x - other.x) < 1.0 &&
abs(y - other.y) < 1.0
}
}
extension CGFloat {
func times(_ size: CGSize) -> CGFloat {
let magnitude = sqrt(pow(size.height, 2) + pow(size.width, 2))
return self * magnitude
}
}
extension Int {
func coordinatePairs() -> Parser<[[CGPoint]]> {
return { stream, index in
var floats = [CGFloat]()
floats.reserveCapacity(self * 2)
var found = 0
var next = index
while case .ok(let value, let index) = number(from: stream, at: next) {
floats.insert(value, at: found)
next = index
found += 1
}
if found % 2 == 0 && (found / 2) % self == 0 {
let numberOfCommandsFound = (found / 2) / self
var results = [[CGPoint]](repeating: [CGPoint](repeating: .zero, count: self), count: numberOfCommandsFound)
for i in 0..<numberOfCommandsFound {
for j in 0..<self {
results[i][j] = CGPoint(x: floats[(i * self + j) * 2],
y: floats[(i * self + j) * 2 + 1])
}
}
return .ok(results, next)
} else {
return .error(.tooFewNumbers(expected: self * 2,
found: found,
.init(index: next, stream: stream)))
}
}
}
}