forked from nicklockwood/ShapeScript
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEuclid+Extensions.swift
More file actions
204 lines (189 loc) · 5.78 KB
/
Euclid+Extensions.swift
File metadata and controls
204 lines (189 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//
// Euclid+Extensions.swift
// ShapeScript Lib
//
// Created by Nick Lockwood on 19/10/2021.
// Copyright © 2021 Nick Lockwood. All rights reserved.
//
import Euclid
public extension Collection where Element == Path {
/// Collective bounds for all paths
var bounds: Bounds {
reduce(into: .empty) { $0.formUnion($1.bounds) }
}
}
public extension Color {
init?(hexString: String) {
var string = hexString
if hexString.hasPrefix("#") {
string = String(string.dropFirst())
}
switch string.count {
case 3:
string += "f"
fallthrough
case 4:
let chars = Array(string)
let red = chars[0]
let green = chars[1]
let blue = chars[2]
let alpha = chars[3]
string = "\(red)\(red)\(green)\(green)\(blue)\(blue)\(alpha)\(alpha)"
case 6:
string += "ff"
case 8:
break
default:
return nil
}
guard let rgba = Double("0x" + string).flatMap({
UInt32(exactly: $0)
}) else {
return nil
}
let red = Double((rgba & 0xFF00_0000) >> 24) / 255
let green = Double((rgba & 0x00FF_0000) >> 16) / 255
let blue = Double((rgba & 0x0000_FF00) >> 8) / 255
let alpha = Double((rgba & 0x0000_00FF) >> 0) / 255
self.init(unchecked: [red, green, blue, alpha])
}
}
extension Color {
init(unchecked components: [Double]) {
if let color = Color(components) {
self = color
} else {
assertionFailure()
self = .clear
}
}
}
extension Angle {
var halfturns: Double {
radians / .pi
}
init(halfturns: Double) {
self.init(radians: halfturns * .pi)
}
static func halfturns(_ halfturns: Double) -> Angle {
self.init(halfturns: halfturns)
}
}
extension Rotation {
var rollYawPitchInHalfTurns: [Double] {
[roll.radians / .pi, yaw.radians / .pi, pitch.radians / .pi]
}
init?(rollYawPitchInHalfTurns: [Double]) {
var roll = 0.0, yaw = 0.0, pitch = 0.0
switch rollYawPitchInHalfTurns.count {
case 3:
pitch = rollYawPitchInHalfTurns[2]
fallthrough
case 2:
yaw = rollYawPitchInHalfTurns[1]
fallthrough
case 1:
roll = rollYawPitchInHalfTurns[0]
case 0:
break
default:
return nil
}
self.init(
roll: .radians(roll * .pi),
yaw: .radians(yaw * .pi),
pitch: .radians(pitch * .pi)
)
}
init(unchecked rollYawPitchInHalfTurns: [Double]) {
if let rotation = Rotation(rollYawPitchInHalfTurns: rollYawPitchInHalfTurns) {
self = rotation
} else {
assertionFailure()
self = .identity
}
}
}
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
#if canImport(CoreText)
private extension NSAttributedString {
convenience init(string: String, font: String?, color: Color?, linespacing: Double?) {
var fontName = (font ?? "Helvetica") as CFString
#if canImport(CoreGraphics)
fontName = CGFont(fontName)?.postScriptName ?? fontName
#endif
let font = CTFontCreateWithName(fontName as CFString, 1, nil)
var attributes = [NSAttributedString.Key.font: font as Any]
#if canImport(AppKit) || canImport(UIKit)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = CGFloat(linespacing ?? 0)
attributes[.paragraphStyle] = paragraphStyle
attributes[.foregroundColor] = color.map(OSColor.init)
#endif
self.init(string: string, attributes: attributes)
}
}
#endif
extension Path {
/// Does path contain vertex colors
var hasColors: Bool {
points.contains(where: { $0.color != nil })
}
/// Create an array of text paths
static func text(
_ text: [TextValue],
width: Double? = nil,
detail: Int = 2
) -> [Path] {
#if canImport(CoreText)
let attributedString = NSMutableAttributedString()
for text in text {
var string = text.string
if attributedString.length > 0 {
string = "\n\(string)"
}
attributedString.append(NSAttributedString(
string: string,
font: text.font,
color: text.color,
linespacing: text.linespacing
))
}
return Path.text(attributedString, width: width, detail: detail)
#else
// TODO: throw error when CoreText not available
return []
#endif
}
/// Increase path detail in proportion to twist angle
func withDetail(_ detail: Int, twist: Angle) -> Path {
let subpaths = self.subpaths
guard subpaths.count == 1 else {
return Path(subpaths: subpaths.map {
$0.withDetail(detail, twist: twist)
})
}
guard var prev = points.first else {
return self
}
let total = length
let maxStep = Angle.twoPi / max(1, Double(detail / 2))
var split = false
let path = Path([prev] + points.dropFirst()
.flatMap { point -> [PathPoint] in
defer { prev = point }
let length = (point.position - prev.position).length
let step = twist * (length / total)
if step >= maxStep {
split = true
return [prev.lerp(point, 0.5).curved(), point]
}
return [point]
})
return split ? path.withDetail(detail, twist: twist) : path
}
}