diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c4674c3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test-linux: + name: Test on Linux + runs-on: ubuntu-latest + container: + image: swift:5.9 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run tests + run: swift test --enable-test-discovery + + test-macos: + name: Test on macOS + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run tests + run: swift test --enable-test-discovery + \ No newline at end of file diff --git a/Package.swift b/Package.swift index 7f5e8f7..6ddc86c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 import PackageDescription let package = Package( name: "SVGView", platforms: [ - .macOS(.v11), + .macOS(.v14), .iOS(.v14), .watchOS(.v7) ], @@ -18,8 +18,11 @@ let package = Package( targets: [ .target( name: "SVGView", - path: "Source", - exclude: ["Info.plist"] + path: "Source" + ), + .testTarget( + name: "CoreGraphicsPolyfillTests", + dependencies: ["SVGView"] ) ], swiftLanguageVersions: [.v5] diff --git a/Source/CoreGraphicsPolyfill.swift b/Source/CoreGraphicsPolyfill.swift new file mode 100644 index 0000000..aa7de7e --- /dev/null +++ b/Source/CoreGraphicsPolyfill.swift @@ -0,0 +1,471 @@ +// +// CoreGraphicsPolyfills.swift +// SVGView +// +// Created by khoi on 10/5/25. +// + +import Foundation + +#if os(WASI) || os(Linux) + private let KAPPA: CGFloat = 0.5522847498 // 4 *(sqrt(2) -1)/3 + + public struct CGAffineTransform: Equatable { + public var a, b, c, d, tx, ty: CGFloat + + public init(a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat) { + + self.a = a + self.b = b + self.c = c + self.d = d + self.tx = tx + self.ty = ty + } + } + + public enum CGLineJoin: UInt32 { + + case miter + case round + case bevel + + public init() { self = .miter } + } + + public enum CGLineCap: UInt32 { + + case butt + case round + case square + + public init() { self = .butt } + } + + /// A graphics path is a mathematical description of a series of shapes or lines. + public class CGPath { + + public typealias Element = PathElement + + public var elements: [Element] + + public init(elements: [Element] = []) { + + self.elements = elements + } + } + + // MARK: - Supporting Types + + /// A path element. + public enum PathElement { + + /// The path element that starts a new subpath. The element holds a single point for the destination. + case moveToPoint(CGPoint) + + /// The path element that adds a line from the current point to a new point. + /// The element holds a single point for the destination. + case addLineToPoint(CGPoint) + + /// The path element that adds a quadratic curve from the current point to the specified point. + /// The element holds a control point and a destination point. + case addQuadCurveToPoint(CGPoint, CGPoint) + + /// The path element that adds a cubic curve from the current point to the specified point. + /// The element holds two control points and a destination point. + case addCurveToPoint(CGPoint, CGPoint, CGPoint) + + /// The path element that closes and completes a subpath. The element does not contain any points. + case closeSubpath + } + + extension CGPath { + + public var boundingBoxOfPath: CGRect { + var minX = CGFloat.infinity + var minY = CGFloat.infinity + var maxX = -CGFloat.infinity + var maxY = -CGFloat.infinity + + for element in elements { + switch element { + case .moveToPoint(let point), + .addLineToPoint(let point): + minX = min(minX, point.x) + minY = min(minY, point.y) + maxX = max(maxX, point.x) + maxY = max(maxY, point.y) + + case .addQuadCurveToPoint(let control, let point): + minX = min(minX, control.x, point.x) + minY = min(minY, control.y, point.y) + maxX = max(maxX, control.x, point.x) + maxY = max(maxY, control.y, point.y) + + case .addCurveToPoint(let control1, let control2, let point): + minX = min(minX, control1.x, control2.x, point.x) + minY = min(minY, control1.y, control2.y, point.y) + maxX = max(maxX, control1.x, control2.x, point.x) + maxY = max(maxY, control1.y, control2.y, point.y) + + case .closeSubpath: + break + } + } + + return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) + } + + public func addRect(_ rect: CGRect) { + + let newElements: [Element] = [ + .moveToPoint(CGPoint(x: rect.minX, y: rect.minY)), + .addLineToPoint(CGPoint(x: rect.maxX, y: rect.minY)), + .addLineToPoint(CGPoint(x: rect.maxX, y: rect.maxY)), + .addLineToPoint(CGPoint(x: rect.minX, y: rect.maxY)), + .closeSubpath, + ] + + elements.append(contentsOf: newElements) + } + + public func addEllipse(in rect: CGRect) { + + var p = CGPoint() + var p1 = CGPoint() + var p2 = CGPoint() + + let hdiff = rect.width / 2 * KAPPA + let vdiff = rect.height / 2 * KAPPA + + p = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height) + elements.append(.moveToPoint(p)) + + p = CGPoint(x: rect.origin.x, y: rect.origin.y + rect.height / 2) + p1 = CGPoint(x: rect.origin.x + rect.width / 2 - hdiff, y: rect.origin.y + rect.height) + p2 = CGPoint(x: rect.origin.x, y: rect.origin.y + rect.height / 2 + vdiff) + elements.append(.addCurveToPoint(p1, p2, p)) + + p = CGPoint(x: rect.origin.x + rect.size.width / 2, y: rect.origin.y) + p1 = CGPoint(x: rect.origin.x, y: rect.origin.y + rect.size.height / 2 - vdiff) + p2 = CGPoint(x: rect.origin.x + rect.size.width / 2 - hdiff, y: rect.origin.y) + elements.append(.addCurveToPoint(p1, p2, p)) + + p = CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height / 2) + p1 = CGPoint(x: rect.origin.x + rect.size.width / 2 + hdiff, y: rect.origin.y) + p2 = CGPoint( + x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height / 2 - vdiff) + elements.append(.addCurveToPoint(p1, p2, p)) + + p = CGPoint(x: rect.origin.x + rect.size.width / 2, y: rect.origin.y + rect.size.height) + p1 = CGPoint( + x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height / 2 + vdiff) + p2 = CGPoint( + x: rect.origin.x + rect.size.width / 2 + hdiff, y: rect.origin.y + rect.size.height) + elements.append(.addCurveToPoint(p1, p2, p)) + } + + public func move(to point: CGPoint) { + + elements.append(.moveToPoint(point)) + } + + public func addLine(to point: CGPoint) { + + elements.append(.addLineToPoint(point)) + } + + public func addCurve(to endPoint: CGPoint, control1: CGPoint, control2: CGPoint) { + + elements.append(.addCurveToPoint(control1, control2, endPoint)) + } + + public func addQuadCurve(to endPoint: CGPoint, control: CGPoint) { + + elements.append(.addQuadCurveToPoint(control, endPoint)) + } + + public func closeSubpath() { + + elements.append(.closeSubpath) + } + } + + public struct CGPathElement { + + public var type: CGPathElementType + + public var points: (CGPoint, CGPoint, CGPoint) + + public init(type: CGPathElementType, points: (CGPoint, CGPoint, CGPoint)) { + + self.type = type + self.points = points + } + } + + /// Rules for determining which regions are interior to a path. + /// + /// When filling a path, regions that a fill rule defines as interior to the path are painted. + /// When clipping with a path, regions interior to the path remain visible after clipping. + public enum CGPathFillRule: Int { + + /// A rule that considers a region to be interior to a path based on the number of times it is enclosed by path elements. + case evenOdd + + /// A rule that considers a region to be interior to a path if the winding number for that region is nonzero. + case winding + } + + /// The type of element found in a path. + public enum CGPathElementType { + + /// The path element that starts a new subpath. The element holds a single point for the destination. + case moveToPoint + + /// The path element that adds a line from the current point to a new point. + /// The element holds a single point for the destination. + case addLineToPoint + + /// The path element that adds a quadratic curve from the current point to the specified point. + /// The element holds a control point and a destination point. + case addQuadCurveToPoint + + /// The path element that adds a cubic curve from the current point to the specified point. + /// The element holds two control points and a destination point. + case addCurveToPoint + + /// The path element that closes and completes a subpath. The element does not contain any points. + case closeSubpath + } + + extension CGPoint { + + @inline(__always) + public func applying(_ t: CGAffineTransform) -> CGPoint { + return CGPoint( + x: t.a * x + t.c * y + t.tx, + y: t.b * x + t.d * y + t.ty) + } + } + + extension CGAffineTransform { + public var isIdentity: Bool { + self == CGAffineTransform.identity + } + + public static var identity: CGAffineTransform { + CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0) + } + + public init(translationX tx: CGFloat, y ty: CGFloat) { + self.init(a: 1, b: 0, c: 0, d: 1, tx: tx, ty: ty) + } + + public init(scaleX sx: CGFloat, y sy: CGFloat) { + self.init(a: sx, b: 0, c: 0, d: sy, tx: 0, ty: 0) + } + + public init(rotationAngle angle: CGFloat) { + self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0) + } + + public func translatedBy(x: CGFloat, y: CGFloat) -> CGAffineTransform { + return self.concatenating(CGAffineTransform(translationX: x, y: y)) + } + + public func concatenating(_ t: CGAffineTransform) -> CGAffineTransform { + return CGAffineTransform( + a: a * t.a + c * t.b, + b: b * t.a + d * t.b, + c: a * t.c + c * t.d, + d: b * t.c + d * t.d, + tx: a * t.tx + c * t.ty + tx, + ty: b * t.tx + d * t.ty + ty + ) + } + + public func scaledBy(x: CGFloat, y: CGFloat) -> CGAffineTransform { + return self.concatenating(CGAffineTransform(scaleX: x, y: y)) + } + + public func rotated(by angle: CGFloat) -> CGAffineTransform { + return self.concatenating(CGAffineTransform(rotationAngle: angle)) + } + } + + public class MBezierPath { + public var cgPath: CGPath + + public init() { + self.cgPath = CGPath() + } + + public init?(rect: CGRect) { + self.cgPath = CGPath() + self.cgPath.addRect(rect) + } + + public init?(ovalIn rect: CGRect) { + self.cgPath = CGPath() + self.cgPath.addEllipse(in: rect) + } + + public init( + arcCenter: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, + clockwise: Bool + ) { + self.cgPath = CGPath() + MBezierPath.addArcTo( + path: self.cgPath, center: arcCenter, radius: radius, startAngle: startAngle, + endAngle: endAngle, clockwise: clockwise) + } + + public func move(to point: CGPoint) { + cgPath.move(to: point) + } + + public func addLine(to point: CGPoint) { + cgPath.addLine(to: point) + } + + public func addCurve( + to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint + ) { + cgPath.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) + } + + public func addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint) { + cgPath.addQuadCurve(to: endPoint, control: controlPoint) + } + + public func addArc( + withCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, + clockwise: Bool + ) { + MBezierPath.addArcTo( + path: self.cgPath, center: center, radius: radius, startAngle: startAngle, + endAngle: endAngle, clockwise: clockwise) + } + + public func append(_ path: MBezierPath) { + cgPath.elements.append(contentsOf: path.cgPath.elements) + } + + public func close() { + cgPath.closeSubpath() + } + + public func apply(_ transform: CGAffineTransform) { + var newElements: [CGPath.Element] = [] + for element in cgPath.elements { + switch element { + case .moveToPoint(let point): + newElements.append(.moveToPoint(point.applying(transform))) + case .addLineToPoint(let point): + newElements.append(.addLineToPoint(point.applying(transform))) + case .addQuadCurveToPoint(let control, let point): + newElements.append( + .addQuadCurveToPoint(control.applying(transform), point.applying(transform)) + ) + case .addCurveToPoint(let control1, let control2, let point): + newElements.append( + .addCurveToPoint( + control1.applying(transform), control2.applying(transform), + point.applying(transform))) + case .closeSubpath: + newElements.append(.closeSubpath) + } + } + self.cgPath.elements = newElements + } + + public var isEmpty: Bool { + return cgPath.elements.isEmpty + } + + public var bounds: CGRect { + return cgPath.boundingBoxOfPath + } + + static func addArcTo( + path: CGPath, center: CGPoint, radius: CGFloat, startAngle: CGFloat, + endAngle: CGFloat, clockwise: Bool + ) { + var deltaAngle: CGFloat + if clockwise { + deltaAngle = endAngle - startAngle + while deltaAngle < 0 { deltaAngle += 2 * .pi } + } else { // Counter-clockwise + deltaAngle = endAngle - startAngle + while deltaAngle > 0 { deltaAngle -= 2 * .pi } + } + + if abs(deltaAngle) < 1e-6 { return } // Essentially no arc + + let numSegments = Swift.max(1, Int(ceil(abs(deltaAngle) / (.pi / 2.0)))) // Max 90deg segments + let segmentAngleSweep = deltaAngle / CGFloat(numSegments) + + var currentAngle = startAngle + + let initialPoint = CGPoint( + x: center.x + radius * cos(currentAngle), y: center.y + radius * sin(currentAngle)) + + if path.elements.isEmpty || (path.elements.last?.isCloseSubpath ?? false) { + path.move(to: initialPoint) + } else if let lastElement = path.elements.last, let lastPoint = lastElement.lastPoint, + lastPoint != initialPoint + { + path.addLine(to: initialPoint) + } + + for _ in 0.. some View { SVGDataImageView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGDataImage: ObservableObject {} struct SVGDataImageView: View { #if os(OSX) @@ -55,3 +66,4 @@ struct SVGDataImageView: View { .applyNodeAttributes(model: model) } } +#endif diff --git a/Source/Model/Images/SVGURLImage.swift b/Source/Model/Images/SVGURLImage.swift index 4071e91..bca987a 100644 --- a/Source/Model/Images/SVGURLImage.swift +++ b/Source/Model/Images/SVGURLImage.swift @@ -5,9 +5,13 @@ // Created by Alisa Mylnikova on 22/09/2021. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif -public class SVGURLImage: SVGImage, ObservableObject { +public class SVGURLImage: SVGImage { public let src: String public let data: Data? @@ -23,11 +27,15 @@ public class SVGURLImage: SVGImage, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGUrlImageView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGURLImage: ObservableObject {} struct SVGUrlImageView: View { @ObservedObject var model: SVGURLImage @@ -56,4 +64,5 @@ struct SVGUrlImageView: View { .applyNodeAttributes(model: model) } } +#endif diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift index a9ea4ae..406839a 100644 --- a/Source/Model/Nodes/SVGGroup.swift +++ b/Source/Model/Nodes/SVGGroup.swift @@ -1,10 +1,16 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif -public class SVGGroup: SVGNode, ObservableObject { - +public class SVGGroup: SVGNode { + #if os(WASI) || os(Linux) + public var contents: [SVGNode] = [] + #else @Published public var contents: [SVGNode] = [] - + #endif public init(contents: [SVGNode], transform: CGAffineTransform = .identity, opaque: Bool = true, opacity: Double = 1, clip: SVGUserSpaceNode? = nil, mask: SVGNode? = nil) { super.init(transform: transform, opaque: opaque, opacity: opacity, clip: clip, mask: mask) self.contents = contents @@ -31,11 +37,15 @@ public class SVGGroup: SVGNode, ObservableObject { serializer.add("contents", contents) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGGroupView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGGroup: ObservableObject {} struct SVGGroupView: View { @ObservedObject var model: SVGGroup @@ -52,4 +62,5 @@ struct SVGGroupView: View { .applyNodeAttributes(model: model) } } +#endif diff --git a/Source/Model/Nodes/SVGImage.swift b/Source/Model/Nodes/SVGImage.swift index 65b2c1a..da2d974 100644 --- a/Source/Model/Nodes/SVGImage.swift +++ b/Source/Model/Nodes/SVGImage.swift @@ -5,15 +5,26 @@ // Created by Alisa Mylnikova on 03/06/2021. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGImage: SVGNode { +#if os(WASI) || os(Linux) + public var x: CGFloat + public var y: CGFloat + public var width: CGFloat + public var height: CGFloat +#else @Published public var x: CGFloat @Published public var y: CGFloat @Published public var width: CGFloat @Published public var height: CGFloat +#endif public init(x: CGFloat = 0, y: CGFloat = 0, width: CGFloat = 0, height: CGFloat = 0) { self.x = x diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift index fecfd06..214b486 100644 --- a/Source/Model/Nodes/SVGNode.swift +++ b/Source/Model/Nodes/SVGNode.swift @@ -1,16 +1,28 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGNode: SerializableElement { +#if os(WASI) || os(Linux) + public var transform: CGAffineTransform = CGAffineTransform.identity + public var opaque: Bool + public var opacity: Double + public var clip: SVGNode? + public var mask: SVGNode? + public var id: String? +#else @Published public var transform: CGAffineTransform = CGAffineTransform.identity @Published public var opaque: Bool @Published public var opacity: Double @Published public var clip: SVGNode? @Published public var mask: SVGNode? @Published public var id: String? +#endif - var gestures = [AnyGesture<()>]() public init(transform: CGAffineTransform = .identity, opaque: Bool = true, opacity: Double = 1, clip: SVGNode? = nil, mask: SVGNode? = nil, id: String? = nil) { self.transform = transform @@ -34,6 +46,8 @@ public class SVGNode: SerializableElement { return self.id == id ? self : .none } + #if canImport(SwiftUI) + var gestures = [AnyGesture<()>]() public func onTapGesture(_ count: Int = 1, tapClosure: @escaping ()->()) { let newGesture = TapGesture(count: count).onEnded { tapClosure() @@ -48,6 +62,7 @@ public class SVGNode: SerializableElement { public func removeAllGestures() { gestures.removeAll() } + #endif func serialize(_ serializer: Serializer) { if !transform.isIdentity { @@ -64,6 +79,7 @@ public class SVGNode: SerializableElement { } +#if canImport(SwiftUI) extension SVGNode { @ViewBuilder public func toSwiftUI() -> some View { @@ -103,3 +119,4 @@ extension SVGNode { } } } +#endif diff --git a/Source/Model/Nodes/SVGShape.swift b/Source/Model/Nodes/SVGShape.swift index e32f3df..ea23743 100644 --- a/Source/Model/Nodes/SVGShape.swift +++ b/Source/Model/Nodes/SVGShape.swift @@ -1,10 +1,19 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGShape: SVGNode { +#if os(WASI) || os(Linux) + public var fill: SVGPaint? + public var stroke: SVGStroke? +#else @Published public var fill: SVGPaint? @Published public var stroke: SVGStroke? +#endif override func serialize(_ serializer: Serializer) { fill?.serialize(key: "fill", serializer: serializer) diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift index 930ee46..098cb99 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -1,15 +1,43 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif -public class SVGText: SVGNode, ObservableObject { - +public class SVGText: SVGNode { + public enum Anchor: String, SerializableEnum { + case leading + case center + case trailing + } + + #if os(WASI) || os(Linux) + public var text: String + public var font: SVGFont? + public var fill: SVGPaint? + public var stroke: SVGStroke? + public var textAnchor: Anchor = .leading + #else @Published public var text: String @Published public var font: SVGFont? @Published public var fill: SVGPaint? @Published public var stroke: SVGStroke? - @Published public var textAnchor: HorizontalAlignment = .leading - - public init(text: String, font: SVGFont? = nil, fill: SVGPaint? = SVGColor.black, stroke: SVGStroke? = nil, textAnchor: HorizontalAlignment = .leading, transform: CGAffineTransform = .identity, opaque: Bool = true, opacity: Double = 1, clip: SVGUserSpaceNode? = nil, mask: SVGNode? = nil) { + @Published public var textAnchor: Anchor = .leading + #endif + + public init( + text: String, + font: SVGFont? = nil, + fill: SVGPaint? = SVGColor.black, + stroke: SVGStroke? = nil, + textAnchor: Anchor = .leading, + transform: CGAffineTransform = .identity, + opaque: Bool = true, + opacity: Double = 1, + clip: SVGUserSpaceNode? = nil, + mask: SVGNode? = nil + ) { self.text = text self.font = font self.fill = fill @@ -25,11 +53,28 @@ public class SVGText: SVGNode, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGTextView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGText: ObservableObject {} + +extension SVGText.Anchor { + var horizontalAlignment: HorizontalAlignment { + switch self { + case .leading: + return .leading + case .center: + return .center + case .trailing: + return .trailing + } + } +} struct SVGTextView: View { @ObservedObject var model: SVGText @@ -54,7 +99,9 @@ struct SVGTextView: View { Text(model.text) .font(model.font?.toSwiftUI()) .lineLimit(1) - .alignmentGuide(.leading) { d in d[model.textAnchor] } + .alignmentGuide(.leading) { + d in d[model.textAnchor.horizontalAlignment] + } .alignmentGuide(VerticalAlignment.top) { d in d[VerticalAlignment.firstTextBaseline] } .position(x: 0, y: 0) // just to specify that positioning is global, actual coords are in transform .apply(paint: fill) @@ -62,3 +109,4 @@ struct SVGTextView: View { .frame(alignment: .topLeading) } } +#endif diff --git a/Source/Model/Nodes/SVGUserSpaceNode.swift b/Source/Model/Nodes/SVGUserSpaceNode.swift index 7e6d90a..c48c6c1 100644 --- a/Source/Model/Nodes/SVGUserSpaceNode.swift +++ b/Source/Model/Nodes/SVGUserSpaceNode.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 14/10/2020. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public class SVGUserSpaceNode: SVGNode { @@ -28,11 +32,14 @@ public class SVGUserSpaceNode: SVGNode { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGUserSpaceNodeView(model: self) } + #endif } +#if canImport(SwiftUI) struct SVGUserSpaceNodeView: View { let model: SVGUserSpaceNode @@ -44,3 +51,4 @@ struct SVGUserSpaceNodeView: View { } } } +#endif diff --git a/Source/Model/Nodes/SVGViewport.swift b/Source/Model/Nodes/SVGViewport.swift index 0aa9f7f..8911dce 100644 --- a/Source/Model/Nodes/SVGViewport.swift +++ b/Source/Model/Nodes/SVGViewport.swift @@ -1,8 +1,17 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGViewport: SVGGroup { - + #if os(WASI) || os(Linux) + public var width: SVGLength + public var height: SVGLength + public var viewBox: CGRect? + public var preserveAspectRatio: SVGPreserveAspectRatio + #else @Published public var width: SVGLength { willSet { self.objectWillChange.send() @@ -26,6 +35,7 @@ public class SVGViewport: SVGGroup { self.objectWillChange.send() } } + #endif public init(width: SVGLength, height: SVGLength, viewBox: CGRect? = .none, preserveAspectRatio: SVGPreserveAspectRatio, contents: [SVGNode] = []) { self.width = width @@ -57,6 +67,7 @@ public class SVGViewport: SVGGroup { } +#if canImport(SwiftUI) struct SVGViewportView: View { @ObservedObject var model: SVGViewport @@ -89,3 +100,4 @@ struct SVGViewportView: View { } } +#endif diff --git a/Source/Model/Primitives/SVGColor.swift b/Source/Model/Primitives/SVGColor.swift index 33d1d51..bebfd8b 100644 --- a/Source/Model/Primitives/SVGColor.swift +++ b/Source/Model/Primitives/SVGColor.swift @@ -5,7 +5,11 @@ // Created by Yuriy Strot on 19.01.2021. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public class SVGColor: SVGPaint { @@ -58,7 +62,8 @@ public class SVGColor: SVGPaint { serializer.add(key, "\(prefix)#\(String(format: "%02X%02X%02X", r, g, b))") } } - + + #if canImport(SwiftUI) public func toSwiftUI() -> Color { return Color(red: Double(r) / 0xff, green: Double(g) / 0xff, blue: Double(b) / 0xff).opacity(opacity) } @@ -66,6 +71,7 @@ public class SVGColor: SVGPaint { func apply(view: S, model: SVGShape? = nil) -> some View where S : View { view.foregroundColor(toSwiftUI()) } + #endif public var r: Int { return (value >> 16) & 0xff @@ -101,6 +107,7 @@ public func == (lhs: SVGColor, rhs: SVGColor) -> Bool { return lhs.value == rhs.value } +#if canImport(SwiftUI) extension Color: SerializableAtom { static func by(name: String) -> Color? { @@ -149,6 +156,7 @@ extension Color: SerializableAtom { } } +#endif class SVGColors { diff --git a/Source/Model/Primitives/SVGFont.swift b/Source/Model/Primitives/SVGFont.swift index 77e6c86..888c6de 100644 --- a/Source/Model/Primitives/SVGFont.swift +++ b/Source/Model/Primitives/SVGFont.swift @@ -1,4 +1,8 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public class SVGFont: SerializableBlock { @@ -11,10 +15,12 @@ public class SVGFont: SerializableBlock { self.size = size self.weight = weight } - + + #if canImport(SwiftUI) public func toSwiftUI() -> Font { return Font.custom(name, size: size)//.weight(fontWeight) } + #endif func serialize(_ serializer: Serializer) { serializer.add("name", name, "Serif").add("size", size, 16).add("weight", weight, "normal") diff --git a/Source/Model/Primitives/SVGGradient.swift b/Source/Model/Primitives/SVGGradient.swift index 42cafb5..184557b 100644 --- a/Source/Model/Primitives/SVGGradient.swift +++ b/Source/Model/Primitives/SVGGradient.swift @@ -5,7 +5,11 @@ // Created by Yuriy Strot on 22.02.2021. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public class SVGLinearGradient: SVGGradient { @@ -54,7 +58,8 @@ public class SVGLinearGradient: SVGGradient { stops: stops ) } - + + #if canImport(SwiftUI) public func toSwiftUI(rect: CGRect) -> LinearGradient { let suiStops = stops.map { stop in Gradient.Stop(color: stop.color.toSwiftUI(), location: stop.offset) } let nx1 = userSpace ? (x1 - rect.minX) / rect.size.width : x1 @@ -85,6 +90,7 @@ public class SVGLinearGradient: SVGGradient { .mask(view) ) } + #endif } @@ -107,7 +113,8 @@ public class SVGRadialGradient: SVGGradient { stops: stops ) } - + + #if canImport(SwiftUI) public func toSwiftUI(rect: CGRect) -> RadialGradient { let suiStops = stops.map { stop in Gradient.Stop(color: stop.color.toSwiftUI(), location: stop.offset) } let s = min(rect.size.width, rect.size.height) @@ -132,6 +139,7 @@ public class SVGRadialGradient: SVGGradient { .mask(view) ) } + #endif } diff --git a/Source/Model/Primitives/SVGLength.swift b/Source/Model/Primitives/SVGLength.swift index 1c31259..466afb3 100644 --- a/Source/Model/Primitives/SVGLength.swift +++ b/Source/Model/Primitives/SVGLength.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 13/10/2020. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public enum SVGLength { diff --git a/Source/Model/Primitives/SVGPaint.swift b/Source/Model/Primitives/SVGPaint.swift index 017d8c4..1907613 100644 --- a/Source/Model/Primitives/SVGPaint.swift +++ b/Source/Model/Primitives/SVGPaint.swift @@ -5,8 +5,11 @@ // Created by Yuriy Strot on 19.01.2021. // +#if os(WASI) || os(Linux) import Foundation +#else import SwiftUI +#endif public class SVGPaint { @@ -20,6 +23,7 @@ public class SVGPaint { } +#if canImport(SwiftUI) extension View { @ViewBuilder @@ -41,3 +45,4 @@ extension View { } } +#endif diff --git a/Source/Model/Primitives/SVGPreserveAspectRatio.swift b/Source/Model/Primitives/SVGPreserveAspectRatio.swift index 00904a1..0c6461f 100644 --- a/Source/Model/Primitives/SVGPreserveAspectRatio.swift +++ b/Source/Model/Primitives/SVGPreserveAspectRatio.swift @@ -5,7 +5,11 @@ // Created by Yuriy Strot on 20.01.2021. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public class SVGPreserveAspectRatio { diff --git a/Source/Model/Primitives/SVGStroke.swift b/Source/Model/Primitives/SVGStroke.swift index aab2cda..e813f11 100644 --- a/Source/Model/Primitives/SVGStroke.swift +++ b/Source/Model/Primitives/SVGStroke.swift @@ -1,4 +1,8 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public class SVGStroke: SerializableBlock { @@ -20,6 +24,7 @@ public class SVGStroke: SerializableBlock { self.offset = offset } + #if canImport(SwiftUI) public func toSwiftUI() -> StrokeStyle { StrokeStyle(lineWidth: width, lineCap: cap, @@ -28,6 +33,7 @@ public class SVGStroke: SerializableBlock { dash: dashes, dashPhase: offset) } + #endif func serialize(_ serializer: Serializer) { fill.serialize(key: "fill", serializer: serializer) diff --git a/Source/Model/Shapes/SVGCircle.swift b/Source/Model/Shapes/SVGCircle.swift index 91c54b0..49f8e19 100644 --- a/Source/Model/Shapes/SVGCircle.swift +++ b/Source/Model/Shapes/SVGCircle.swift @@ -1,11 +1,22 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif -public class SVGCircle: SVGShape, ObservableObject { +public class SVGCircle: SVGShape { + #if os(WASI) || os(Linux) + public var cx: CGFloat + public var cy: CGFloat + public var r: CGFloat + #else @Published public var cx: CGFloat @Published public var cy: CGFloat @Published public var r: CGFloat + #endif + public init(cx: CGFloat = 0, cy: CGFloat = 0, r: CGFloat = 0) { self.cx = cx @@ -22,11 +33,15 @@ public class SVGCircle: SVGShape, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGCircleView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGCircle: ObservableObject {} struct SVGCircleView: View { @ObservedObject var model = SVGCircle() @@ -39,3 +54,4 @@ struct SVGCircleView: View { .position(x: model.cx, y: model.cy) } } +#endif diff --git a/Source/Model/Shapes/SVGEllipse.swift b/Source/Model/Shapes/SVGEllipse.swift index 44e90df..efa9e25 100644 --- a/Source/Model/Shapes/SVGEllipse.swift +++ b/Source/Model/Shapes/SVGEllipse.swift @@ -1,13 +1,22 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine - -public class SVGEllipse: SVGShape, ObservableObject { - +#endif + +public class SVGEllipse: SVGShape { + #if os(WASI) || os(Linux) + public var cx: CGFloat + public var cy: CGFloat + public var rx: CGFloat + public var ry: CGFloat + #else @Published public var cx: CGFloat @Published public var cy: CGFloat @Published public var rx: CGFloat @Published public var ry: CGFloat - + #endif public init(cx: CGFloat = 0, cy: CGFloat = 0, rx: CGFloat = 0, ry: CGFloat = 0) { self.cx = cx self.cy = cy @@ -24,11 +33,15 @@ public class SVGEllipse: SVGShape, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGEllipseView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGEllipse: ObservableObject {} struct SVGEllipseView: View { @ObservedObject var model = SVGEllipse() @@ -41,4 +54,5 @@ struct SVGEllipseView: View { .applyShapeAttributes(model: model) } } +#endif diff --git a/Source/Model/Shapes/SVGLine.swift b/Source/Model/Shapes/SVGLine.swift index 2b02c81..825e819 100644 --- a/Source/Model/Shapes/SVGLine.swift +++ b/Source/Model/Shapes/SVGLine.swift @@ -1,12 +1,23 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif -public class SVGLine: SVGShape, ObservableObject { +public class SVGLine: SVGShape { + #if os(WASI) || os(Linux) + public var x1: CGFloat + public var y1: CGFloat + public var x2: CGFloat + public var y2: CGFloat + #else @Published public var x1: CGFloat @Published public var y1: CGFloat @Published public var x2: CGFloat @Published public var y2: CGFloat + #endif public init(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat) { self.x1 = x1 @@ -31,11 +42,15 @@ public class SVGLine: SVGShape, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGLineView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGLine: ObservableObject {} struct SVGLineView: View { @ObservedObject var model = SVGLine() @@ -51,4 +66,5 @@ struct SVGLineView: View { return line } } +#endif diff --git a/Source/Model/Shapes/SVGPath.swift b/Source/Model/Shapes/SVGPath.swift index a37e15a..0e9b86e 100644 --- a/Source/Model/Shapes/SVGPath.swift +++ b/Source/Model/Shapes/SVGPath.swift @@ -1,10 +1,19 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif -public class SVGPath: SVGShape, ObservableObject { +public class SVGPath: SVGShape { + #if os(WASI) || os(Linux) + public var segments: [PathSegment] + public var fillRule: CGPathFillRule + #else @Published public var segments: [PathSegment] @Published public var fillRule: CGPathFillRule + #endif public init(segments: [PathSegment] = [], fillRule: CGPathFillRule = .winding) { self.segments = segments @@ -26,11 +35,15 @@ public class SVGPath: SVGShape, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGPathView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGPath: ObservableObject {} struct SVGPathView: View { @ObservedObject var model = SVGPath() @@ -39,7 +52,9 @@ struct SVGPathView: View { model.toBezierPath().toSwiftUI(model: model, eoFill: model.fillRule == .evenOdd) } } +#endif +#if canImport(SwiftUI) extension MBezierPath { func toSwiftUI(model: SVGShape, eoFill: Bool = false) -> some View { @@ -55,4 +70,5 @@ extension MBezierPath { } } } +#endif diff --git a/Source/Model/Shapes/SVGPolygon.swift b/Source/Model/Shapes/SVGPolygon.swift index 6031c9e..f323ddd 100644 --- a/Source/Model/Shapes/SVGPolygon.swift +++ b/Source/Model/Shapes/SVGPolygon.swift @@ -1,9 +1,17 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif -public class SVGPolygon: SVGShape, ObservableObject { +public class SVGPolygon: SVGShape { + #if os(WASI) || os(Linux) + public var points: [CGPoint] + #else @Published public var points: [CGPoint] + #endif public init(_ points: [CGPoint]) { self.points = points @@ -18,10 +26,10 @@ public class SVGPolygon: SVGShape, ObservableObject { return .zero } - var minX = CGFloat(INT16_MAX) - var minY = CGFloat(INT16_MAX) - var maxX = CGFloat(INT16_MIN) - var maxY = CGFloat(INT16_MIN) + var minX = CGFloat(Int16.max) + var minY = CGFloat(Int16.max) + var maxX = CGFloat(Int16.min) + var maxY = CGFloat(Int16.min) for point in points { minX = min(minX, point.x) @@ -45,11 +53,15 @@ public class SVGPolygon: SVGShape, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGPolygonView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGPolygon: ObservableObject {} struct SVGPolygonView: View { @ObservedObject var model = SVGPolygon() @@ -71,4 +83,5 @@ struct SVGPolygonView: View { return path } } +#endif diff --git a/Source/Model/Shapes/SVGPolyline.swift b/Source/Model/Shapes/SVGPolyline.swift index f0cbfc6..eb3e107 100644 --- a/Source/Model/Shapes/SVGPolyline.swift +++ b/Source/Model/Shapes/SVGPolyline.swift @@ -1,9 +1,16 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif -public class SVGPolyline: SVGShape, ObservableObject { - +public class SVGPolyline: SVGShape { + #if os(WASI) || os(Linux) + public var points: [CGPoint] + #else @Published public var points: [CGPoint] + #endif public init(_ points: [CGPoint]) { self.points = points @@ -18,10 +25,10 @@ public class SVGPolyline: SVGShape, ObservableObject { return .zero } - var minX = CGFloat(INT16_MAX) - var minY = CGFloat(INT16_MAX) - var maxX = CGFloat(INT16_MIN) - var maxY = CGFloat(INT16_MIN) + var minX = CGFloat(Int16.max) + var minY = CGFloat(Int16.max) + var maxX = CGFloat(Int16.min) + var maxY = CGFloat(Int16.min) for point in points { minX = min(minX, point.x) @@ -45,11 +52,15 @@ public class SVGPolyline: SVGShape, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGPolylineView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGPolyline: ObservableObject {} struct SVGPolylineView: View { @ObservedObject var model = SVGPolyline() @@ -69,4 +80,5 @@ struct SVGPolylineView: View { return path } } +#endif diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift index c046bdd..0179896 100644 --- a/Source/Model/Shapes/SVGRect.swift +++ b/Source/Model/Shapes/SVGRect.swift @@ -1,15 +1,27 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI import Combine +#endif -public class SVGRect: SVGShape, ObservableObject { +public class SVGRect: SVGShape { + #if os(WASI) || os(Linux) + public var x: CGFloat + public var y: CGFloat + public var width: CGFloat + public var height: CGFloat + public var rx: CGFloat = 0 + public var ry: CGFloat = 0 + #else @Published public var x: CGFloat @Published public var y: CGFloat @Published public var width: CGFloat @Published public var height: CGFloat @Published public var rx: CGFloat = 0 @Published public var ry: CGFloat = 0 - + #endif public init(x: CGFloat = 0, y: CGFloat = 0, width: CGFloat = 0, height: CGFloat = 0, rx: CGFloat = 0, ry: CGFloat = 0) { self.x = x self.y = y @@ -36,11 +48,15 @@ public class SVGRect: SVGShape, ObservableObject { super.serialize(serializer) } + #if canImport(SwiftUI) public func contentView() -> some View { SVGRectView(model: self) } + #endif } +#if canImport(SwiftUI) +extension SVGRect: ObservableObject {} struct SVGRectView: View { @ObservedObject var model: SVGRect @@ -54,3 +70,4 @@ struct SVGRectView: View { .offset(x: model.width/2, y: model.height/2) } } +#endif diff --git a/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift b/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift index 6f92235..d1399c4 100644 --- a/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift @@ -5,7 +5,7 @@ // Created by Yuri Strot on 29.05.2022. // -import CoreGraphics +import Foundation class SVGFontSizeAttribute: SVGDefaultAttribute { diff --git a/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift b/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift index 25205c6..2375752 100644 --- a/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift @@ -5,7 +5,8 @@ // Created by Yuri Strot on 29.05.2022. // -import CoreGraphics + +import Foundation class SVGLengthAttribute: SVGDefaultAttribute { diff --git a/Source/Parser/SVG/Elements/SVGImageParser.swift b/Source/Parser/SVG/Elements/SVGImageParser.swift index 73c3aed..6a8e865 100644 --- a/Source/Parser/SVG/Elements/SVGImageParser.swift +++ b/Source/Parser/SVG/Elements/SVGImageParser.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 29.05.2022. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif class SVGImageParser: SVGBaseElementParser { override func doParse(context: SVGNodeContext, delegate: (XMLElement) -> SVGNode?) -> SVGNode? { diff --git a/Source/Parser/SVG/Elements/SVGShapeParser.swift b/Source/Parser/SVG/Elements/SVGShapeParser.swift index 0e3aa4e..0e2e6eb 100644 --- a/Source/Parser/SVG/Elements/SVGShapeParser.swift +++ b/Source/Parser/SVG/Elements/SVGShapeParser.swift @@ -5,7 +5,6 @@ // Created by Yuri Strot on 29.05.2022. // -import CoreGraphics class SVGShapeParser: SVGBaseElementParser { diff --git a/Source/Parser/SVG/Elements/SVGStructureParsers.swift b/Source/Parser/SVG/Elements/SVGStructureParsers.swift index 17bd6af..4a13745 100644 --- a/Source/Parser/SVG/Elements/SVGStructureParsers.swift +++ b/Source/Parser/SVG/Elements/SVGStructureParsers.swift @@ -6,7 +6,6 @@ // import Foundation -import CoreGraphics class SVGViewportParser: SVGGroupParser { diff --git a/Source/Parser/SVG/Elements/SVGTextParser.swift b/Source/Parser/SVG/Elements/SVGTextParser.swift index 7f92119..5cbbe6f 100644 --- a/Source/Parser/SVG/Elements/SVGTextParser.swift +++ b/Source/Parser/SVG/Elements/SVGTextParser.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 29.05.2022. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif class SVGTextParser: SVGBaseElementParser { override func doParse(context: SVGNodeContext, delegate: (XMLElement) -> SVGNode?) -> SVGNode? { @@ -26,7 +30,7 @@ class SVGTextParser: SVGBaseElementParser { return .none } - private func parseTextAnchor(_ string: String?) -> HorizontalAlignment { + private func parseTextAnchor(_ string: String?) -> SVGText.Anchor { if let anchor = string { if anchor == "middle" { return .center diff --git a/Source/Parser/SVG/Primitives/SVGLengthParser.swift b/Source/Parser/SVG/Primitives/SVGLengthParser.swift index cc514bc..0312d3d 100644 --- a/Source/Parser/SVG/Primitives/SVGLengthParser.swift +++ b/Source/Parser/SVG/Primitives/SVGLengthParser.swift @@ -6,7 +6,6 @@ // import Foundation -import CoreGraphics enum SVGLengthAxis { diff --git a/Source/Parser/SVG/SVGContext.swift b/Source/Parser/SVG/SVGContext.swift index 5b81bc2..2bd58ea 100644 --- a/Source/Parser/SVG/SVGContext.swift +++ b/Source/Parser/SVG/SVGContext.swift @@ -5,7 +5,7 @@ // Created by Yuri Strot on 26.05.2022. // -import CoreGraphics +import Foundation protocol SVGContext { diff --git a/Source/Parser/SVG/SVGIndex.swift b/Source/Parser/SVG/SVGIndex.swift index f0ffe42..b1a3d18 100644 --- a/Source/Parser/SVG/SVGIndex.swift +++ b/Source/Parser/SVG/SVGIndex.swift @@ -5,7 +5,11 @@ // Created by Yuriy Strot on 21.02.2021. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif class SVGIndex { diff --git a/Source/Parser/SVG/SVGParser.swift b/Source/Parser/SVG/SVGParser.swift index 18f6513..125970e 100644 --- a/Source/Parser/SVG/SVGParser.swift +++ b/Source/Parser/SVG/SVGParser.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 20/07/2020. // -import SwiftUI +import Foundation public struct SVGParser { diff --git a/Source/Parser/SVG/SVGParserBasics.swift b/Source/Parser/SVG/SVGParserBasics.swift index a37688e..3c15b18 100644 --- a/Source/Parser/SVG/SVGParserBasics.swift +++ b/Source/Parser/SVG/SVGParserBasics.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 17/07/2020. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif extension SVGHelper { diff --git a/Source/Parser/SVG/SVGParserExtensions.swift b/Source/Parser/SVG/SVGParserExtensions.swift index ecfb6ed..ab8d953 100644 --- a/Source/Parser/SVG/SVGParserExtensions.swift +++ b/Source/Parser/SVG/SVGParserExtensions.swift @@ -5,10 +5,9 @@ // Created by Yuri Strot on 25.05.2022. // -import CoreGraphics +import Foundation extension CGFloat { - var degreesToRadians: CGFloat { return self * .pi / 180 } diff --git a/Source/Parser/SVG/SVGParserPrimitives.swift b/Source/Parser/SVG/SVGParserPrimitives.swift index 4e99c7d..250c5ef 100644 --- a/Source/Parser/SVG/SVGParserPrimitives.swift +++ b/Source/Parser/SVG/SVGParserPrimitives.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 20/07/2020. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public class SVGHelper: NSObject { diff --git a/Source/Parser/SVG/SVGPathReader.swift b/Source/Parser/SVG/SVGPathReader.swift index e3f00f4..f5608f5 100644 --- a/Source/Parser/SVG/SVGPathReader.swift +++ b/Source/Parser/SVG/SVGPathReader.swift @@ -5,13 +5,14 @@ // Created by Alisa Mylnikova on 23/07/2020. // -import SwiftUI - #if os(OSX) import AppKit public typealias MBezierPath = NSBezierPath -#else +#elseif os(iOS) || os(tvOS) || os(watchOS) +import UIKit public typealias MBezierPath = UIBezierPath +#elseif os(WASI) || os(Linux) +import Foundation #endif public enum PathSegmentType { @@ -335,7 +336,11 @@ extension SVGPath { return CGFloat > 0.5 ? true : false } + #if os(WASI) || os(Linux) + var bezierPath = MBezierPath() + #else let bezierPath = MBezierPath() + #endif var currentPoint: CGPoint? var cubicPoint: CGPoint? @@ -560,7 +565,11 @@ extension SVGPath { bezierPath.addArc(withCenter: CGPoint(x: cx, y: cy), radius: CGFloat(w / 2), startAngle: extent, endAngle: end, clockwise: arcAngle >= 0) } else { let maxSize = CGFloat(max(w, h)) + #if os(WASI) || os(Linux) + var path = MBezierPath(arcCenter: CGPoint.zero, radius: maxSize / 2, startAngle: extent, endAngle: end, clockwise: arcAngle >= 0) + #else let path = MBezierPath(arcCenter: CGPoint.zero, radius: maxSize / 2, startAngle: extent, endAngle: end, clockwise: arcAngle >= 0) + #endif var transform = CGAffineTransform(translationX: cx, y: cy) transform = transform.rotated(by: CGFloat(rotation)) diff --git a/Source/Parser/SVG/SVGView.swift b/Source/Parser/SVG/SVGView.swift index 8e9bf5d..119437c 100644 --- a/Source/Parser/SVG/SVGView.swift +++ b/Source/Parser/SVG/SVGView.swift @@ -5,8 +5,13 @@ // Created by Alisa Mylnikova on 20/08/2020. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif +#if canImport(SwiftUI) public struct SVGView: View { public let svg: SVGNode? @@ -49,3 +54,4 @@ public struct SVGView: View { } } +#endif diff --git a/Source/Parser/SVG/Settings/SVGScreen.swift b/Source/Parser/SVG/Settings/SVGScreen.swift index fb4d85c..4ffb8ca 100644 --- a/Source/Parser/SVG/Settings/SVGScreen.swift +++ b/Source/Parser/SVG/Settings/SVGScreen.swift @@ -5,7 +5,6 @@ // Created by Yuri Strot on 27.05.2022. // -import CoreGraphics struct SVGScreen { diff --git a/Source/Parser/SVG/Settings/SVGSettings.swift b/Source/Parser/SVG/Settings/SVGSettings.swift index e2800a6..f838526 100644 --- a/Source/Parser/SVG/Settings/SVGSettings.swift +++ b/Source/Parser/SVG/Settings/SVGSettings.swift @@ -6,7 +6,6 @@ // import Foundation -import CoreGraphics public struct SVGSettings { diff --git a/Source/Parser/XML/DOMParser.swift b/Source/Parser/XML/DOMParser.swift index b4198fc..a88b710 100644 --- a/Source/Parser/XML/DOMParser.swift +++ b/Source/Parser/XML/DOMParser.swift @@ -5,7 +5,12 @@ // Created by Alisa Mylnikova on 20/08/2020. // -import SwiftUI +#if os(WASI) || os(Linux) +import Foundation +import FoundationXML +#else +import Foundation +#endif public struct DOMParser { diff --git a/Source/Parser/XML/XMLNode.swift b/Source/Parser/XML/XMLNode.swift index a58bede..2a2cfcc 100644 --- a/Source/Parser/XML/XMLNode.swift +++ b/Source/Parser/XML/XMLNode.swift @@ -1,4 +1,8 @@ +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif public protocol XMLNode { } diff --git a/Source/Serialization/Serializations.swift b/Source/Serialization/Serializations.swift index 0e8571e..ca5e652 100644 --- a/Source/Serialization/Serializations.swift +++ b/Source/Serialization/Serializations.swift @@ -5,8 +5,11 @@ // Created by Yuriy Strot on 18.01.2021. // +#if os(WASI) || os(Linux) import Foundation +#else import SwiftUI +#endif extension Bool: SerializableAtom { @@ -44,18 +47,7 @@ extension Double: SerializableAtom { extension CGAffineTransform: SerializableAtom { func serialize() -> String { - let formatter = NumberFormatter() - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 10 - - let nums = [a, b, c, d, tx, ty] - - var result = "" - for num in nums { - result += formatter.string(from: num as NSNumber) ?? "n/a" - result += ", " - } - return "[\(result.dropLast(2))]" + return String(format: "[%.10f, %.10f, %.10f, %.10f, %.10f, %.10f]", a, b, c, d, tx, ty) } } @@ -173,7 +165,7 @@ extension CGPathFillRule: SerializableOption { } -extension HorizontalAlignment: SerializableOption { +extension SVGText.Anchor: SerializableOption { func isDefault() -> Bool { return self == .leading diff --git a/Source/Serialization/Serializer.swift b/Source/Serialization/Serializer.swift index bc7fadb..e9732c4 100644 --- a/Source/Serialization/Serializer.swift +++ b/Source/Serialization/Serializer.swift @@ -6,7 +6,6 @@ // import Foundation -import CoreGraphics class Serializer { diff --git a/Source/UI/MBezierPath+Extension_macOS.swift b/Source/UI/MBezierPath+Extension_macOS.swift index 4fbfe0b..1997008 100644 --- a/Source/UI/MBezierPath+Extension_macOS.swift +++ b/Source/UI/MBezierPath+Extension_macOS.swift @@ -29,37 +29,6 @@ public struct MRectCorner: OptionSet { } extension MBezierPath { - - public var cgPath: CGPath { - let path = CGMutablePath() - var points = [CGPoint](repeating: .zero, count: 3) - - for i in 0 ..< self.elementCount { - let type = self.element(at: i, associatedPoints: &points) - - switch type { - case .moveTo: - path.move(to: CGPoint(x: points[0].x, y: points[0].y)) - - case .lineTo: - path.addLine(to: CGPoint(x: points[0].x, y: points[0].y)) - - case .curveTo: - path.addCurve( - to: CGPoint(x: points[2].x, y: points[2].y), - control1: CGPoint(x: points[0].x, y: points[0].y), - control2: CGPoint(x: points[1].x, y: points[1].y)) - - case .closePath: - path.closeSubpath() - @unknown default: - fatalError("Type of element undefined") - } - } - - return path - } - public convenience init(arcCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) { self.init() self.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) diff --git a/Source/UI/UIExtensions.swift b/Source/UI/UIExtensions.swift index 5297c83..a35d9d2 100644 --- a/Source/UI/UIExtensions.swift +++ b/Source/UI/UIExtensions.swift @@ -5,8 +5,13 @@ // Created by Yuri Strot on 25.05.2022. // +#if os(WASI) || os(Linux) +import Foundation +#else import SwiftUI +#endif +#if canImport(SwiftUI) extension Shape { @ViewBuilder @@ -41,7 +46,9 @@ extension Shape { } } +#endif +#if canImport(SwiftUI) extension View { func applyShapeAttributes(model: SVGShape) -> some View { @@ -70,7 +77,9 @@ extension View { } } +#endif +#if canImport(SwiftUI) extension View { @ViewBuilder @@ -86,7 +95,9 @@ extension View { } } } +#endif +#if canImport(SwiftUI) extension View { @ViewBuilder @@ -99,3 +110,4 @@ extension View { } } +#endif diff --git a/Tests/CoreGraphicsPolyfillTests/PolyfillTests.swift b/Tests/CoreGraphicsPolyfillTests/PolyfillTests.swift new file mode 100644 index 0000000..01671ba --- /dev/null +++ b/Tests/CoreGraphicsPolyfillTests/PolyfillTests.swift @@ -0,0 +1,618 @@ +// +// PolyfillTests.swift +// SVGView +// +// Tests for CoreGraphicsPolyfill.swift - comprehensive testing of the CoreGraphics +// polyfill implementation for WASI/Linux platforms where CoreGraphics is not available. +// +// These tests verify: +// - CGAffineTransform operations (identity, translation, scaling, rotation, concatenation) +// - CGPath functionality (basic operations, bounding box calculation, shape addition) +// - MBezierPath operations (initialization, path building, transformations, arc handling) +// - PathElement enum and extensions +// - Edge cases and error handling +// +// On platforms with native CoreGraphics (macOS, iOS), only a fallback test runs +// since the polyfill types are aliases to the native CoreGraphics types. +// + +import XCTest + +@testable import SVGView + +final class PolyfillTests: XCTestCase { + + #if os(WASI) || os(Linux) + + // MARK: - CGAffineTransform Tests + + func testAffineTransformIdentity() { + let identity = CGAffineTransform.identity + XCTAssertEqual(identity.a, 1) + XCTAssertEqual(identity.b, 0) + XCTAssertEqual(identity.c, 0) + XCTAssertEqual(identity.d, 1) + XCTAssertEqual(identity.tx, 0) + XCTAssertEqual(identity.ty, 0) + XCTAssertTrue(identity.isIdentity) + } + + func testAffineTransformTranslation() { + let transform = CGAffineTransform(translationX: 10, y: 20) + XCTAssertEqual(transform.a, 1) + XCTAssertEqual(transform.b, 0) + XCTAssertEqual(transform.c, 0) + XCTAssertEqual(transform.d, 1) + XCTAssertEqual(transform.tx, 10) + XCTAssertEqual(transform.ty, 20) + XCTAssertFalse(transform.isIdentity) + } + + func testAffineTransformScale() { + let transform = CGAffineTransform(scaleX: 2, y: 3) + XCTAssertEqual(transform.a, 2) + XCTAssertEqual(transform.b, 0) + XCTAssertEqual(transform.c, 0) + XCTAssertEqual(transform.d, 3) + XCTAssertEqual(transform.tx, 0) + XCTAssertEqual(transform.ty, 0) + } + + func testAffineTransformRotation() { + let transform = CGAffineTransform(rotationAngle: .pi / 2) + XCTAssertEqual(transform.a, cos(.pi / 2), accuracy: 1e-10) + XCTAssertEqual(transform.b, sin(.pi / 2), accuracy: 1e-10) + XCTAssertEqual(transform.c, -sin(.pi / 2), accuracy: 1e-10) + XCTAssertEqual(transform.d, cos(.pi / 2), accuracy: 1e-10) + XCTAssertEqual(transform.tx, 0) + XCTAssertEqual(transform.ty, 0) + } + + func testPointTransformation() { + let point = CGPoint(x: 1, y: 2) + let transform = CGAffineTransform(translationX: 10, y: 20) + let transformedPoint = point.applying(transform) + + XCTAssertEqual(transformedPoint.x, 11) + XCTAssertEqual(transformedPoint.y, 22) + } + + func testTransformConcatenation() { + let transform1 = CGAffineTransform(translationX: 5, y: 10) + let transform2 = CGAffineTransform(scaleX: 2, y: 3) + let combined = transform1.concatenating(transform2) + + XCTAssertEqual(combined.a, 2) + XCTAssertEqual(combined.d, 3) + XCTAssertEqual(combined.tx, 5) + XCTAssertEqual(combined.ty, 10) + } + + func testTransformFluent() { + let transform = CGAffineTransform.identity + .translatedBy(x: 10, y: 20) + .scaledBy(x: 2, y: 3) + .rotated(by: .pi / 4) + + XCTAssertFalse(transform.isIdentity) + XCTAssertNotEqual(transform.tx, 0) + XCTAssertNotEqual(transform.ty, 0) + } + + func testComplexTransform() { + let point = CGPoint(x: 5, y: 5) + let transform = CGAffineTransform(rotationAngle: .pi / 4) + .translatedBy(x: 10, y: 10) + .scaledBy(x: 2, y: 2) + + let transformedPoint = point.applying(transform) + XCTAssertNotEqual(transformedPoint.x, point.x) + XCTAssertNotEqual(transformedPoint.y, point.y) + } + + // MARK: - CGLineJoin and CGLineCap Tests + + func testLineJoinEnum() { + let miterJoin = CGLineJoin.miter + let roundJoin = CGLineJoin.round + let bevelJoin = CGLineJoin.bevel + let defaultJoin = CGLineJoin() + + XCTAssertEqual(defaultJoin, .miter) + XCTAssertNotEqual(miterJoin, roundJoin) + XCTAssertNotEqual(roundJoin, bevelJoin) + } + + func testLineCapEnum() { + let buttCap = CGLineCap.butt + let roundCap = CGLineCap.round + let squareCap = CGLineCap.square + let defaultCap = CGLineCap() + + XCTAssertEqual(defaultCap, .butt) + XCTAssertNotEqual(buttCap, roundCap) + XCTAssertNotEqual(roundCap, squareCap) + } + + // MARK: - CGPath Tests + + func testPathElementCreation() { + let moveElement = PathElement.moveToPoint(CGPoint(x: 0, y: 0)) + let lineElement = PathElement.addLineToPoint(CGPoint(x: 10, y: 10)) + let _ = PathElement.addQuadCurveToPoint(CGPoint(x: 5, y: 5), CGPoint(x: 10, y: 0)) + let _ = PathElement.addCurveToPoint(CGPoint(x: 5, y: 5), CGPoint(x: 7, y: 3), CGPoint(x: 10, y: 0)) + let closeElement = PathElement.closeSubpath + + if case .moveToPoint(let point) = moveElement { + XCTAssertEqual(point.x, 0) + XCTAssertEqual(point.y, 0) + } else { + XCTFail("Expected moveToPoint") + } + + if case .addLineToPoint(let point) = lineElement { + XCTAssertEqual(point.x, 10) + XCTAssertEqual(point.y, 10) + } else { + XCTFail("Expected addLineToPoint") + } + + if case .closeSubpath = closeElement { + // Test passes + } else { + XCTFail("Expected closeSubpath") + } + } + + func testPathBasicOperations() { + let path = CGPath() + XCTAssertTrue(path.elements.isEmpty) + + path.move(to: CGPoint(x: 0, y: 0)) + XCTAssertEqual(path.elements.count, 1) + + path.addLine(to: CGPoint(x: 10, y: 10)) + XCTAssertEqual(path.elements.count, 2) + + path.closeSubpath() + XCTAssertEqual(path.elements.count, 3) + } + + func testPathBoundingBox() { + let path = CGPath() + path.move(to: CGPoint(x: 5, y: 5)) + path.addLine(to: CGPoint(x: 15, y: 10)) + path.addLine(to: CGPoint(x: 10, y: 20)) + + let bounds = path.boundingBoxOfPath + XCTAssertEqual(bounds.minX, 5) + XCTAssertEqual(bounds.minY, 5) + XCTAssertEqual(bounds.maxX, 15) + XCTAssertEqual(bounds.maxY, 20) + XCTAssertEqual(bounds.width, 10) + XCTAssertEqual(bounds.height, 15) + } + + func testPathBoundingBoxWithCurves() { + let path = CGPath() + path.move(to: CGPoint(x: 0, y: 0)) + path.addCurve(to: CGPoint(x: 10, y: 10), control1: CGPoint(x: 5, y: 5), control2: CGPoint(x: 15, y: 8)) + + let bounds = path.boundingBoxOfPath + XCTAssertEqual(bounds.minX, 0) + XCTAssertEqual(bounds.minY, 0) + XCTAssertEqual(bounds.maxX, 15) + XCTAssertEqual(bounds.maxY, 10) + } + + func testPathAddRect() { + let path = CGPath() + let rect = CGRect(x: 10, y: 20, width: 30, height: 40) + path.addRect(rect) + + XCTAssertEqual(path.elements.count, 5) // move + 3 lines + close + + let bounds = path.boundingBoxOfPath + XCTAssertEqual(bounds, rect) + } + + func testPathAddEllipse() { + let path = CGPath() + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + path.addEllipse(in: rect) + + XCTAssertFalse(path.elements.isEmpty) + + let bounds = path.boundingBoxOfPath + // Ellipse should fit within the rectangle (approximately) + XCTAssertGreaterThanOrEqual(bounds.minX, rect.minX - 1) + XCTAssertGreaterThanOrEqual(bounds.minY, rect.minY - 1) + XCTAssertLessThanOrEqual(bounds.maxX, rect.maxX + 1) + XCTAssertLessThanOrEqual(bounds.maxY, rect.maxY + 1) + } + + func testPathQuadCurve() { + let path = CGPath() + path.move(to: CGPoint(x: 0, y: 0)) + path.addQuadCurve(to: CGPoint(x: 10, y: 10), control: CGPoint(x: 5, y: 0)) + + XCTAssertEqual(path.elements.count, 2) + + if case .addQuadCurveToPoint(let control, let end) = path.elements[1] { + XCTAssertEqual(control.x, 5) + XCTAssertEqual(control.y, 0) + XCTAssertEqual(end.x, 10) + XCTAssertEqual(end.y, 10) + } else { + XCTFail("Expected quad curve element") + } + } + + // MARK: - MBezierPath Tests + + func testBezierPathInit() { + let path = MBezierPath() + XCTAssertTrue(path.isEmpty) + XCTAssertTrue(path.cgPath.elements.isEmpty) + } + + func testBezierPathRectInit() { + let rect = CGRect(x: 10, y: 20, width: 30, height: 40) + let path = MBezierPath(rect: rect) + + XCTAssertNotNil(path) + XCTAssertFalse(path!.isEmpty) + XCTAssertEqual(path!.bounds, rect) + } + + func testBezierPathOvalInit() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 100) + let path = MBezierPath(ovalIn: rect) + + XCTAssertNotNil(path) + XCTAssertFalse(path!.isEmpty) + } + + func testBezierPathArcInit() { + let center = CGPoint(x: 50, y: 50) + let radius: CGFloat = 25 + let path = MBezierPath( + arcCenter: center, + radius: radius, + startAngle: 0, + endAngle: .pi, + clockwise: true + ) + + XCTAssertFalse(path.isEmpty) + } + + func testBezierPathOperations() { + let path = MBezierPath() + + path.move(to: CGPoint(x: 0, y: 0)) + XCTAssertFalse(path.isEmpty) + + path.addLine(to: CGPoint(x: 10, y: 10)) + path.addQuadCurve(to: CGPoint(x: 20, y: 0), controlPoint: CGPoint(x: 15, y: -5)) + path.addCurve( + to: CGPoint(x: 30, y: 10), + controlPoint1: CGPoint(x: 25, y: 5), + controlPoint2: CGPoint(x: 28, y: 8) + ) + path.close() + + XCTAssertEqual(path.cgPath.elements.count, 5) + } + + func testBezierPathTransform() { + let path = MBezierPath() + path.move(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: 10, y: 10)) + + let originalBounds = path.bounds + let transform = CGAffineTransform(scaleX: 2, y: 2) + path.apply(transform) + + let newBounds = path.bounds + XCTAssertEqual(newBounds.width, originalBounds.width * 2, accuracy: 1e-10) + XCTAssertEqual(newBounds.height, originalBounds.height * 2, accuracy: 1e-10) + } + + func testBezierPathAppend() { + let path1 = MBezierPath() + path1.move(to: CGPoint(x: 0, y: 0)) + path1.addLine(to: CGPoint(x: 10, y: 10)) + + let path2 = MBezierPath() + path2.move(to: CGPoint(x: 20, y: 20)) + path2.addLine(to: CGPoint(x: 30, y: 30)) + + let originalCount = path1.cgPath.elements.count + path1.append(path2) + + XCTAssertEqual(path1.cgPath.elements.count, originalCount + path2.cgPath.elements.count) + } + + func testBezierPathArc() { + let path = MBezierPath() + let center = CGPoint(x: 50, y: 50) + let radius: CGFloat = 25 + + path.addArc( + withCenter: center, + radius: radius, + startAngle: 0, + endAngle: .pi / 2, + clockwise: true + ) + + XCTAssertFalse(path.isEmpty) + XCTAssertGreaterThan(path.cgPath.elements.count, 1) + } + + func testBezierPathArcCounterClockwise() { + let path = MBezierPath() + let center = CGPoint(x: 0, y: 0) + let radius: CGFloat = 10 + + path.addArc( + withCenter: center, + radius: radius, + startAngle: 0, + endAngle: -.pi / 2, + clockwise: false + ) + + XCTAssertFalse(path.isEmpty) + } + + func testBezierPathFullCircleArc() { + let path = MBezierPath() + let center = CGPoint(x: 50, y: 50) + let radius: CGFloat = 25 + + path.addArc( + withCenter: center, + radius: radius, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: true + ) + + XCTAssertFalse(path.isEmpty) + XCTAssertGreaterThan(path.cgPath.elements.count, 4) // Should have multiple segments + } + + // MARK: - PathElement Extension Tests + + func testPathElementLastPoint() { + let moveElement = PathElement.moveToPoint(CGPoint(x: 5, y: 10)) + let lineElement = PathElement.addLineToPoint(CGPoint(x: 15, y: 20)) + let closeElement = PathElement.closeSubpath + + XCTAssertEqual(moveElement.lastPoint, CGPoint(x: 5, y: 10)) + XCTAssertEqual(lineElement.lastPoint, CGPoint(x: 15, y: 20)) + XCTAssertNil(closeElement.lastPoint) + } + + func testPathElementIsCloseSubpath() { + let moveElement = PathElement.moveToPoint(CGPoint(x: 0, y: 0)) + let closeElement = PathElement.closeSubpath + + XCTAssertFalse(moveElement.isCloseSubpath) + XCTAssertTrue(closeElement.isCloseSubpath) + } + + func testPathElementLastPointWithCurves() { + let quadElement = PathElement.addQuadCurveToPoint(CGPoint(x: 5, y: 5), CGPoint(x: 10, y: 10)) + let curveElement = PathElement.addCurveToPoint(CGPoint(x: 1, y: 1), CGPoint(x: 2, y: 2), CGPoint(x: 3, y: 3)) + + XCTAssertEqual(quadElement.lastPoint, CGPoint(x: 10, y: 10)) + XCTAssertEqual(curveElement.lastPoint, CGPoint(x: 3, y: 3)) + } + + // MARK: - CGPathElement Tests + + func testCGPathElementCreation() { + let element = CGPathElement( + type: .moveToPoint, + points: (CGPoint(x: 1, y: 2), CGPoint.zero, CGPoint.zero) + ) + + XCTAssertEqual(element.type, .moveToPoint) + XCTAssertEqual(element.points.0, CGPoint(x: 1, y: 2)) + } + + // MARK: - CGPathElementType Tests + + func testCGPathElementTypeEnum() { + let moveType = CGPathElementType.moveToPoint + let lineType = CGPathElementType.addLineToPoint + let quadType = CGPathElementType.addQuadCurveToPoint + let curveType = CGPathElementType.addCurveToPoint + let closeType = CGPathElementType.closeSubpath + + // Ensure all enum cases are distinct + XCTAssertNotEqual(moveType, lineType) + XCTAssertNotEqual(lineType, quadType) + XCTAssertNotEqual(quadType, curveType) + XCTAssertNotEqual(curveType, closeType) + XCTAssertNotEqual(closeType, moveType) + } + + // MARK: - CGPathFillRule Tests + + func testCGPathFillRuleEnum() { + let evenOdd = CGPathFillRule.evenOdd + let winding = CGPathFillRule.winding + + XCTAssertNotEqual(evenOdd, winding) + XCTAssertNotEqual(evenOdd.rawValue, winding.rawValue) + } + + func testCGPathFillRuleRawValues() { + // Test that raw values are distinct and valid + let evenOdd = CGPathFillRule.evenOdd + let winding = CGPathFillRule.winding + + XCTAssertNotNil(CGPathFillRule(rawValue: evenOdd.rawValue)) + XCTAssertNotNil(CGPathFillRule(rawValue: winding.rawValue)) + XCTAssertEqual(CGPathFillRule(rawValue: evenOdd.rawValue), evenOdd) + XCTAssertEqual(CGPathFillRule(rawValue: winding.rawValue), winding) + } + + // MARK: - MBezierPath Static Method Tests + + func testMBezierPathAddArcToStatic() { + let path = CGPath() + let center = CGPoint(x: 10, y: 10) + let radius: CGFloat = 5 + + // Test static addArcTo method directly + MBezierPath.addArcTo( + path: path, + center: center, + radius: radius, + startAngle: 0, + endAngle: .pi / 2, + clockwise: true + ) + + XCTAssertFalse(path.elements.isEmpty) + XCTAssertGreaterThan(path.elements.count, 1) + + // First element should be move to starting point + if case .moveToPoint(let point) = path.elements.first { + XCTAssertEqual(point.x, center.x + radius, accuracy: 1e-10) + XCTAssertEqual(point.y, center.y, accuracy: 1e-10) + } else { + XCTFail("Expected first element to be moveToPoint") + } + } + + func testMBezierPathAddArcToStaticWithExistingPath() { + let path = CGPath() + path.move(to: CGPoint(x: 20, y: 20)) + path.addLine(to: CGPoint(x: 25, y: 25)) + + let originalCount = path.elements.count + + // Add arc to existing path + MBezierPath.addArcTo( + path: path, + center: CGPoint(x: 0, y: 0), + radius: 10, + startAngle: 0, + endAngle: .pi, + clockwise: false + ) + + // Should have added more elements + XCTAssertGreaterThan(path.elements.count, originalCount) + } + + // MARK: - Edge Cases + + func testEmptyPathBounds() { + let path = CGPath() + let bounds = path.boundingBoxOfPath + + XCTAssertTrue(bounds.width.isInfinite || bounds.width.isNaN) + XCTAssertTrue(bounds.height.isInfinite || bounds.height.isNaN) + } + + func testSinglePointPathBounds() { + let path = CGPath() + path.move(to: CGPoint(x: 10, y: 20)) + + let bounds = path.boundingBoxOfPath + XCTAssertEqual(bounds.origin.x, 10) + XCTAssertEqual(bounds.origin.y, 20) + XCTAssertEqual(bounds.width, 0) + XCTAssertEqual(bounds.height, 0) + } + + func testZeroRadiusArc() { + let path = MBezierPath() + path.addArc( + withCenter: CGPoint(x: 0, y: 0), + radius: 0, + startAngle: 0, + endAngle: .pi, + clockwise: true + ) + + // With zero radius, it creates move + curve elements all at center point + XCTAssertFalse(path.isEmpty) + XCTAssertEqual(path.cgPath.elements.count, 3) // move + 2 curves for π angle + + // First element should be moveToPoint at center + if case .moveToPoint(let point) = path.cgPath.elements[0] { + XCTAssertEqual(point.x, 0) + XCTAssertEqual(point.y, 0) + } else { + XCTFail("Expected first element to be moveToPoint") + } + + // All curve elements should have all points at center + for element in path.cgPath.elements.dropFirst() { + if case .addCurveToPoint(let cp1, let cp2, let end) = element { + XCTAssertEqual(cp1.x, 0) + XCTAssertEqual(cp1.y, 0) + XCTAssertEqual(cp2.x, 0) + XCTAssertEqual(cp2.y, 0) + XCTAssertEqual(end.x, 0) + XCTAssertEqual(end.y, 0) + } else { + XCTFail("Expected curve element") + } + } + } + + func testVerySmallAngleArc() { + let path = MBezierPath() + path.addArc( + withCenter: CGPoint(x: 0, y: 0), + radius: 10, + startAngle: 0, + endAngle: 1e-10, + clockwise: true + ) + + // Should handle very small angles (essentially no arc) + XCTAssertTrue(path.isEmpty || path.cgPath.elements.count <= 2) + } + + func testIdenticalStartEndAngles() { + let path = MBezierPath() + path.addArc( + withCenter: CGPoint(x: 0, y: 0), + radius: 10, + startAngle: .pi / 4, + endAngle: .pi / 4, + clockwise: true + ) + + // Should handle identical start and end angles + XCTAssertTrue(path.isEmpty || path.cgPath.elements.count <= 1) + } + + func testNegativeRectDimensions() { + let path = CGPath() + let rect = CGRect(x: 10, y: 10, width: -5, height: -5) + path.addRect(rect) + + // Should handle negative dimensions + XCTAssertFalse(path.elements.isEmpty) + } + + #else + + func testPolyfillNotNeeded() { + // On platforms with CoreGraphics, polyfill types should be aliases + XCTAssertTrue(true, "CoreGraphics polyfill not needed on this platform") + } + + #endif +}