Skip to content

Commit 7312a6e

Browse files
committed
Enhance cross-platform graphics polyfills
- Add proper imports for WASI (WASILibc) and Linux (Glibc) - Implement MBezierPath with full functionality for WASI/Linux platforms - Add missing math functions for non-Apple platforms - Fix mutable path variable in SVG path reader - Simplify CGAffineTransform serialization
1 parent 380db15 commit 7312a6e

3 files changed

Lines changed: 221 additions & 38 deletions

File tree

Source/Parser/SVG/SVGPathReader.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
// Created by Alisa Mylnikova on 23/07/2020.
66
//
77

8-
#if os(WASI) || os(Linux)
9-
8+
#if os(WASI)
9+
import WASILibc
10+
#elseif os(Linux)
11+
import Glibc
1012
#elseif os(OSX)
1113
import AppKit
1214
public typealias MBezierPath = NSBezierPath
@@ -335,7 +337,11 @@ extension SVGPath {
335337
return CGFloat > 0.5 ? true : false
336338
}
337339

340+
#if os(WASI) || os(Linux)
341+
var bezierPath = MBezierPath()
342+
#else
338343
let bezierPath = MBezierPath()
344+
#endif
339345

340346
var currentPoint: CGPoint?
341347
var cubicPoint: CGPoint?
@@ -560,7 +566,7 @@ extension SVGPath {
560566
bezierPath.addArc(withCenter: CGPoint(x: cx, y: cy), radius: CGFloat(w / 2), startAngle: extent, endAngle: end, clockwise: arcAngle >= 0)
561567
} else {
562568
let maxSize = CGFloat(max(w, h))
563-
let path = MBezierPath(arcCenter: CGPoint.zero, radius: maxSize / 2, startAngle: extent, endAngle: end, clockwise: arcAngle >= 0)
569+
var path = MBezierPath(arcCenter: CGPoint.zero, radius: maxSize / 2, startAngle: extent, endAngle: end, clockwise: arcAngle >= 0)
564570

565571
var transform = CGAffineTransform(translationX: cx, y: cy)
566572
transform = transform.rotated(by: CGFloat(rotation))

Source/Polyfills/CoreGraphicsPolyfills.swift

Lines changed: 211 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,31 @@ import Foundation
99

1010
public typealias CGFloat = Foundation.CGFloat
1111
public typealias CGSize = Foundation.CGSize
12+
public typealias CGPoint = Foundation.CGPoint
1213

1314
#if os(WASI) || os(Linux)
15+
import Glibc
16+
17+
public func sqrt(_ x: CGFloat) -> CGFloat { return x.squareRoot() }
18+
public func copysign(_ x: CGFloat, _ y: CGFloat) -> CGFloat {
19+
let magnitude = x >= 0 ? x : -x
20+
return y >= 0 ? magnitude : -magnitude
21+
}
22+
public func acos(_ x: CGFloat) -> CGFloat {
23+
return Glibc.acos(x)
24+
}
25+
26+
public func cos(_ x: CGFloat) -> CGFloat {
27+
return Glibc.cos(x)
28+
}
29+
30+
public func sin(_ x: CGFloat) -> CGFloat {
31+
return Glibc.sin(x)
32+
}
33+
1434
private let KAPPA: CGFloat = 0.5522847498 // 4 *(sqrt(2) -1)/3
1535

16-
public struct CGAffineTransform: Equatable {
36+
public struct CGAffineTransform: Equatable {
1737
public var a, b, c, d, tx, ty: CGFloat
1838

1939
public init(a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat) {
@@ -89,33 +109,33 @@ public struct CGAffineTransform: Equatable {
89109
var minY = CGFloat.infinity
90110
var maxX = -CGFloat.infinity
91111
var maxY = -CGFloat.infinity
92-
112+
93113
for element in elements {
94114
switch element {
95115
case .moveToPoint(let point),
96-
.addLineToPoint(let point):
116+
.addLineToPoint(let point):
97117
minX = min(minX, point.x)
98118
minY = min(minY, point.y)
99119
maxX = max(maxX, point.x)
100120
maxY = max(maxY, point.y)
101-
121+
102122
case .addQuadCurveToPoint(let control, let point):
103123
minX = min(minX, control.x, point.x)
104124
minY = min(minY, control.y, point.y)
105125
maxX = max(maxX, control.x, point.x)
106126
maxY = max(maxY, control.y, point.y)
107-
127+
108128
case .addCurveToPoint(let control1, let control2, let point):
109129
minX = min(minX, control1.x, control2.x, point.x)
110130
minY = min(minY, control1.y, control2.y, point.y)
111131
maxX = max(maxX, control1.x, control2.x, point.x)
112132
maxY = max(maxY, control1.y, control2.y, point.y)
113-
133+
114134
case .closeSubpath:
115135
break
116136
}
117137
}
118-
138+
119139
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
120140
}
121141

@@ -243,24 +263,24 @@ public struct CGAffineTransform: Equatable {
243263
}
244264

245265
extension CGPoint {
246-
266+
247267
@inline(__always)
248268
public func applying(_ t: CGAffineTransform) -> CGPoint {
249-
return CGPoint(x: t.a * x + t.c * y + t.tx,
250-
y: t.b * x + t.d * y + t.ty)
269+
return CGPoint(
270+
x: t.a * x + t.c * y + t.tx,
271+
y: t.b * x + t.d * y + t.ty)
251272
}
252273
}
253274

254-
255275
extension CGAffineTransform {
256276
public var isIdentity: Bool {
257277
self == CGAffineTransform.identity
258278
}
259-
279+
260280
public static var identity: CGAffineTransform {
261281
CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0)
262282
}
263-
283+
264284
public init(translationX tx: CGFloat, y ty: CGFloat) {
265285
self.init(a: 1, b: 0, c: 0, d: 1, tx: tx, ty: ty)
266286
}
@@ -272,11 +292,11 @@ public struct CGAffineTransform: Equatable {
272292
public init(rotationAngle angle: CGFloat) {
273293
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
274294
}
275-
295+
276296
public func translatedBy(x: CGFloat, y: CGFloat) -> CGAffineTransform {
277297
return self.concatenating(CGAffineTransform(translationX: x, y: y))
278298
}
279-
299+
280300
public func concatenating(_ t: CGAffineTransform) -> CGAffineTransform {
281301
return CGAffineTransform(
282302
a: a * t.a + c * t.b,
@@ -287,20 +307,188 @@ public struct CGAffineTransform: Equatable {
287307
ty: b * t.tx + d * t.ty + ty
288308
)
289309
}
290-
310+
291311
public func scaledBy(x: CGFloat, y: CGFloat) -> CGAffineTransform {
292312
return self.concatenating(CGAffineTransform(scaleX: x, y: y))
293313
}
294-
314+
295315
public func rotated(by angle: CGFloat) -> CGAffineTransform {
296316
return self.concatenating(CGAffineTransform(rotationAngle: angle))
297317
}
298318
}
319+
320+
public struct MBezierPath {
321+
public var cgPath: CGPath
322+
323+
public init() {
324+
self.cgPath = CGPath()
325+
}
326+
327+
public init?(rect: CGRect) {
328+
self.cgPath = CGPath()
329+
self.cgPath.addRect(rect)
330+
}
331+
332+
public init?(ovalIn rect: CGRect) {
333+
self.cgPath = CGPath()
334+
self.cgPath.addEllipse(in: rect)
335+
}
336+
337+
public init(
338+
arcCenter: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat,
339+
clockwise: Bool
340+
) {
341+
self.cgPath = CGPath()
342+
MBezierPath.addArcTo(
343+
path: &self.cgPath, center: arcCenter, radius: radius, startAngle: startAngle,
344+
endAngle: endAngle, clockwise: clockwise)
345+
}
346+
347+
public mutating func move(to point: CGPoint) {
348+
cgPath.move(to: point)
349+
}
350+
351+
public mutating func addLine(to point: CGPoint) {
352+
cgPath.addLine(to: point)
353+
}
354+
355+
public mutating func addCurve(
356+
to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint
357+
) {
358+
cgPath.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2)
359+
}
360+
361+
public mutating func addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint) {
362+
cgPath.addQuadCurve(to: endPoint, control: controlPoint)
363+
}
364+
365+
public mutating func addArc(
366+
withCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat,
367+
clockwise: Bool
368+
) {
369+
MBezierPath.addArcTo(
370+
path: &self.cgPath, center: center, radius: radius, startAngle: startAngle,
371+
endAngle: endAngle, clockwise: clockwise)
372+
}
373+
374+
public mutating func append(_ path: MBezierPath) {
375+
cgPath.elements.append(contentsOf: path.cgPath.elements)
376+
}
377+
378+
public mutating func close() {
379+
cgPath.closeSubpath()
380+
}
381+
382+
public mutating func apply(_ transform: CGAffineTransform) {
383+
var newElements: [CGPath.Element] = []
384+
for element in cgPath.elements {
385+
switch element {
386+
case .moveToPoint(let point):
387+
newElements.append(.moveToPoint(point.applying(transform)))
388+
case .addLineToPoint(let point):
389+
newElements.append(.addLineToPoint(point.applying(transform)))
390+
case .addQuadCurveToPoint(let control, let point):
391+
newElements.append(
392+
.addQuadCurveToPoint(control.applying(transform), point.applying(transform))
393+
)
394+
case .addCurveToPoint(let control1, let control2, let point):
395+
newElements.append(
396+
.addCurveToPoint(
397+
control1.applying(transform), control2.applying(transform),
398+
point.applying(transform)))
399+
case .closeSubpath:
400+
newElements.append(.closeSubpath)
401+
}
402+
}
403+
self.cgPath.elements = newElements
404+
}
405+
406+
public var isEmpty: Bool {
407+
return cgPath.elements.isEmpty
408+
}
409+
410+
public var bounds: CGRect {
411+
return cgPath.boundingBoxOfPath
412+
}
413+
414+
static func addArcTo(
415+
path: inout CGPath, center: CGPoint, radius: CGFloat, startAngle: CGFloat,
416+
endAngle: CGFloat, clockwise: Bool
417+
) {
418+
var deltaAngle: CGFloat
419+
if clockwise {
420+
deltaAngle = endAngle - startAngle
421+
while deltaAngle < 0 { deltaAngle += 2 * .pi }
422+
} else { // Counter-clockwise
423+
deltaAngle = endAngle - startAngle
424+
while deltaAngle > 0 { deltaAngle -= 2 * .pi }
425+
}
426+
427+
if abs(deltaAngle) < 1e-6 { return } // Essentially no arc
428+
429+
let numSegments = Swift.max(1, Int(ceil(abs(deltaAngle) / (.pi / 2.0)))) // Max 90deg segments
430+
let segmentAngleSweep = deltaAngle / CGFloat(numSegments)
431+
432+
var currentAngle = startAngle
433+
434+
let initialPoint = CGPoint(
435+
x: center.x + radius * cos(currentAngle), y: center.y + radius * sin(currentAngle))
436+
437+
if path.elements.isEmpty || (path.elements.last?.isCloseSubpath ?? false) {
438+
path.move(to: initialPoint)
439+
} else if let lastElement = path.elements.last, let lastPoint = lastElement.lastPoint,
440+
lastPoint != initialPoint
441+
{
442+
path.addLine(to: initialPoint)
443+
}
444+
445+
for _ in 0..<numSegments {
446+
let nextAngle = currentAngle + segmentAngleSweep
447+
let L = radius * (4.0 / 3.0) * tan(abs(segmentAngleSweep) / 4.0)
448+
449+
let cp1_centerRelative = CGPoint(
450+
x: radius * cos(currentAngle) - L * sin(currentAngle),
451+
y: radius * sin(currentAngle) + L * cos(currentAngle)
452+
)
453+
let cp2_centerRelative = CGPoint(
454+
x: radius * cos(nextAngle) + L * sin(nextAngle),
455+
y: radius * sin(nextAngle) - L * cos(nextAngle)
456+
)
457+
458+
let endPoint_abs = CGPoint(
459+
x: center.x + radius * cos(nextAngle), y: center.y + radius * sin(nextAngle))
460+
let cp1_abs = CGPoint(
461+
x: center.x + cp1_centerRelative.x, y: center.y + cp1_centerRelative.y)
462+
let cp2_abs = CGPoint(
463+
x: center.x + cp2_centerRelative.x, y: center.y + cp2_centerRelative.y)
464+
465+
path.addCurve(to: endPoint_abs, control1: cp1_abs, control2: cp2_abs)
466+
currentAngle = nextAngle
467+
}
468+
}
469+
}
470+
471+
extension CGPath.Element {
472+
var lastPoint: CGPoint? {
473+
switch self {
474+
case .moveToPoint(let p): return p
475+
case .addLineToPoint(let p): return p
476+
case .addQuadCurveToPoint(_, let p): return p
477+
case .addCurveToPoint(_, _, let p): return p
478+
case .closeSubpath: return nil
479+
}
480+
}
481+
var isCloseSubpath: Bool {
482+
if case .closeSubpath = self { return true }
483+
return false
484+
}
485+
}
486+
299487
#else
300-
import CoreGraphics
301-
public typealias CGLineJoin = CoreGraphics.CGLineJoin
302-
public typealias CGLineCap = CoreGraphics.CGLineCap
303-
public typealias CGPathFillRule = CoreGraphics.CGPathFillRule
304-
public typealias CGPath = CoreGraphics.CGPath
305-
public typealias CGAffineTransform = CoreGraphics.CGAffineTransform
488+
import CoreGraphics
489+
public typealias CGLineJoin = CoreGraphics.CGLineJoin
490+
public typealias CGLineCap = CoreGraphics.CGLineCap
491+
public typealias CGPathFillRule = CoreGraphics.CGPathFillRule
492+
public typealias CGPath = CoreGraphics.CGPath
493+
public typealias CGAffineTransform = CoreGraphics.CGAffineTransform
306494
#endif

Source/Serialization/Serializations.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,7 @@ extension Double: SerializableAtom {
4545
extension CGAffineTransform: SerializableAtom {
4646

4747
func serialize() -> String {
48-
let formatter = NumberFormatter()
49-
formatter.minimumFractionDigits = 0
50-
formatter.maximumFractionDigits = 10
51-
52-
let nums = [a, b, c, d, tx, ty]
53-
54-
var result = ""
55-
for num in nums {
56-
result += formatter.string(from: num as NSNumber) ?? "n/a"
57-
result += ", "
58-
}
59-
return "[\(result.dropLast(2))]"
48+
"[\(a), \(b), \(c), \(d), \(tx), \(ty)]"
6049
}
6150
}
6251

0 commit comments

Comments
 (0)