From 639cdc15967fde02b286ca5a1cd729114e91e119 Mon Sep 17 00:00:00 2001 From: khoi Date: Thu, 15 May 2025 15:06:34 +0700 Subject: [PATCH 01/26] Add WASI platform support with conditional imports - Add conditional import statements across all SVG-related files - Use Foundation for WebAssembly System Interface (WASI) platform - Keep SwiftUI/Combine imports for other platforms - Enable cross-platform compatibility for WebAssembly environments --- Source/Model/Images/SVGDataImage.swift | 5 +++++ Source/Model/Images/SVGURLImage.swift | 4 ++++ Source/Model/Nodes/SVGGroup.swift | 4 ++++ Source/Model/Nodes/SVGImage.swift | 4 ++++ Source/Model/Nodes/SVGNode.swift | 4 ++++ Source/Model/Nodes/SVGShape.swift | 4 ++++ Source/Model/Nodes/SVGText.swift | 4 ++++ Source/Model/Nodes/SVGUserSpaceNode.swift | 4 ++++ Source/Model/Nodes/SVGViewport.swift | 4 ++++ Source/Model/Primitives/SVGColor.swift | 4 ++++ Source/Model/Primitives/SVGFont.swift | 4 ++++ Source/Model/Primitives/SVGGradient.swift | 4 ++++ Source/Model/Primitives/SVGLength.swift | 4 ++++ Source/Model/Primitives/SVGPaint.swift | 3 +++ Source/Model/Primitives/SVGPreserveAspectRatio.swift | 4 ++++ Source/Model/Primitives/SVGStroke.swift | 4 ++++ Source/Model/Shapes/SVGCircle.swift | 4 ++++ Source/Model/Shapes/SVGEllipse.swift | 4 ++++ Source/Model/Shapes/SVGLine.swift | 4 ++++ Source/Model/Shapes/SVGPath.swift | 4 ++++ Source/Model/Shapes/SVGPolygon.swift | 4 ++++ Source/Model/Shapes/SVGPolyline.swift | 4 ++++ Source/Model/Shapes/SVGRect.swift | 4 ++++ Source/Parser/SVG/Elements/SVGImageParser.swift | 4 ++++ Source/Parser/SVG/Elements/SVGTextParser.swift | 4 ++++ Source/Parser/SVG/SVGIndex.swift | 4 ++++ Source/Parser/SVG/SVGParserBasics.swift | 4 ++++ Source/Parser/SVG/SVGParserPrimitives.swift | 4 ++++ Source/Parser/SVG/SVGPathReader.swift | 4 ++++ Source/Parser/SVG/SVGView.swift | 4 ++++ Source/Parser/XML/DOMParser.swift | 4 ++++ Source/Parser/XML/XMLNode.swift | 4 ++++ Source/Serialization/Serializations.swift | 3 +++ Source/UI/UIExtensions.swift | 4 ++++ 34 files changed, 135 insertions(+) diff --git a/Source/Model/Images/SVGDataImage.swift b/Source/Model/Images/SVGDataImage.swift index c929ea2..a58cbbb 100644 --- a/Source/Model/Images/SVGDataImage.swift +++ b/Source/Model/Images/SVGDataImage.swift @@ -5,8 +5,13 @@ // Created by Alisa Mylnikova on 10/06/2021. // +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif + public class SVGDataImage: SVGImage, ObservableObject { diff --git a/Source/Model/Images/SVGURLImage.swift b/Source/Model/Images/SVGURLImage.swift index 4071e91..fdb33dd 100644 --- a/Source/Model/Images/SVGURLImage.swift +++ b/Source/Model/Images/SVGURLImage.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 22/09/2021. // +#if os(WASI) +import Foundation +#else import SwiftUI +#endif public class SVGURLImage: SVGImage, ObservableObject { diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift index a9ea4ae..e21321a 100644 --- a/Source/Model/Nodes/SVGGroup.swift +++ b/Source/Model/Nodes/SVGGroup.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGGroup: SVGNode, ObservableObject { diff --git a/Source/Model/Nodes/SVGImage.swift b/Source/Model/Nodes/SVGImage.swift index 65b2c1a..3ba9c9f 100644 --- a/Source/Model/Nodes/SVGImage.swift +++ b/Source/Model/Nodes/SVGImage.swift @@ -5,8 +5,12 @@ // Created by Alisa Mylnikova on 03/06/2021. // +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGImage: SVGNode { diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift index fecfd06..82d570d 100644 --- a/Source/Model/Nodes/SVGNode.swift +++ b/Source/Model/Nodes/SVGNode.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGNode: SerializableElement { diff --git a/Source/Model/Nodes/SVGShape.swift b/Source/Model/Nodes/SVGShape.swift index e32f3df..519919e 100644 --- a/Source/Model/Nodes/SVGShape.swift +++ b/Source/Model/Nodes/SVGShape.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGShape: SVGNode { diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift index 930ee46..de314f6 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGText: SVGNode, ObservableObject { diff --git a/Source/Model/Nodes/SVGUserSpaceNode.swift b/Source/Model/Nodes/SVGUserSpaceNode.swift index 7e6d90a..6e8c20c 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) +import Foundation +#else import SwiftUI +#endif public class SVGUserSpaceNode: SVGNode { diff --git a/Source/Model/Nodes/SVGViewport.swift b/Source/Model/Nodes/SVGViewport.swift index 0aa9f7f..409d73c 100644 --- a/Source/Model/Nodes/SVGViewport.swift +++ b/Source/Model/Nodes/SVGViewport.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGViewport: SVGGroup { diff --git a/Source/Model/Primitives/SVGColor.swift b/Source/Model/Primitives/SVGColor.swift index 33d1d51..83e9e0a 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) +import Foundation +#else import SwiftUI +#endif public class SVGColor: SVGPaint { diff --git a/Source/Model/Primitives/SVGFont.swift b/Source/Model/Primitives/SVGFont.swift index 77e6c86..f04b8bd 100644 --- a/Source/Model/Primitives/SVGFont.swift +++ b/Source/Model/Primitives/SVGFont.swift @@ -1,4 +1,8 @@ +#if os(WASI) +import Foundation +#else import SwiftUI +#endif public class SVGFont: SerializableBlock { diff --git a/Source/Model/Primitives/SVGGradient.swift b/Source/Model/Primitives/SVGGradient.swift index 42cafb5..1364d5a 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) +import Foundation +#else import SwiftUI +#endif public class SVGLinearGradient: SVGGradient { diff --git a/Source/Model/Primitives/SVGLength.swift b/Source/Model/Primitives/SVGLength.swift index 1c31259..349859f 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) +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..367cdc9 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) import Foundation +#else import SwiftUI +#endif public class SVGPaint { diff --git a/Source/Model/Primitives/SVGPreserveAspectRatio.swift b/Source/Model/Primitives/SVGPreserveAspectRatio.swift index 00904a1..4cf601f 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) +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..76d0aca 100644 --- a/Source/Model/Primitives/SVGStroke.swift +++ b/Source/Model/Primitives/SVGStroke.swift @@ -1,4 +1,8 @@ +#if os(WASI) +import Foundation +#else import SwiftUI +#endif public class SVGStroke: SerializableBlock { diff --git a/Source/Model/Shapes/SVGCircle.swift b/Source/Model/Shapes/SVGCircle.swift index 91c54b0..104ba32 100644 --- a/Source/Model/Shapes/SVGCircle.swift +++ b/Source/Model/Shapes/SVGCircle.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGCircle: SVGShape, ObservableObject { diff --git a/Source/Model/Shapes/SVGEllipse.swift b/Source/Model/Shapes/SVGEllipse.swift index 44e90df..c3ec9cf 100644 --- a/Source/Model/Shapes/SVGEllipse.swift +++ b/Source/Model/Shapes/SVGEllipse.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGEllipse: SVGShape, ObservableObject { diff --git a/Source/Model/Shapes/SVGLine.swift b/Source/Model/Shapes/SVGLine.swift index 2b02c81..6d7514b 100644 --- a/Source/Model/Shapes/SVGLine.swift +++ b/Source/Model/Shapes/SVGLine.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGLine: SVGShape, ObservableObject { diff --git a/Source/Model/Shapes/SVGPath.swift b/Source/Model/Shapes/SVGPath.swift index a37e15a..83828da 100644 --- a/Source/Model/Shapes/SVGPath.swift +++ b/Source/Model/Shapes/SVGPath.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGPath: SVGShape, ObservableObject { diff --git a/Source/Model/Shapes/SVGPolygon.swift b/Source/Model/Shapes/SVGPolygon.swift index 6031c9e..37dcd2b 100644 --- a/Source/Model/Shapes/SVGPolygon.swift +++ b/Source/Model/Shapes/SVGPolygon.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGPolygon: SVGShape, ObservableObject { diff --git a/Source/Model/Shapes/SVGPolyline.swift b/Source/Model/Shapes/SVGPolyline.swift index f0cbfc6..1bfe6e4 100644 --- a/Source/Model/Shapes/SVGPolyline.swift +++ b/Source/Model/Shapes/SVGPolyline.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGPolyline: SVGShape, ObservableObject { diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift index c046bdd..9cf73b9 100644 --- a/Source/Model/Shapes/SVGRect.swift +++ b/Source/Model/Shapes/SVGRect.swift @@ -1,5 +1,9 @@ +#if os(WASI) +import Foundation +#else import SwiftUI import Combine +#endif public class SVGRect: SVGShape, ObservableObject { diff --git a/Source/Parser/SVG/Elements/SVGImageParser.swift b/Source/Parser/SVG/Elements/SVGImageParser.swift index 73c3aed..652af4d 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) +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/SVGTextParser.swift b/Source/Parser/SVG/Elements/SVGTextParser.swift index 7f92119..558b090 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) +import Foundation +#else import SwiftUI +#endif class SVGTextParser: SVGBaseElementParser { override func doParse(context: SVGNodeContext, delegate: (XMLElement) -> SVGNode?) -> SVGNode? { diff --git a/Source/Parser/SVG/SVGIndex.swift b/Source/Parser/SVG/SVGIndex.swift index f0ffe42..0f6024f 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) +import Foundation +#else import SwiftUI +#endif class SVGIndex { diff --git a/Source/Parser/SVG/SVGParserBasics.swift b/Source/Parser/SVG/SVGParserBasics.swift index a37688e..ac5649f 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) +import Foundation +#else import SwiftUI +#endif extension SVGHelper { diff --git a/Source/Parser/SVG/SVGParserPrimitives.swift b/Source/Parser/SVG/SVGParserPrimitives.swift index 4e99c7d..e609d47 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) +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..6503635 100644 --- a/Source/Parser/SVG/SVGPathReader.swift +++ b/Source/Parser/SVG/SVGPathReader.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 23/07/2020. // +#if os(WASI) +import Foundation +#else import SwiftUI +#endif #if os(OSX) import AppKit diff --git a/Source/Parser/SVG/SVGView.swift b/Source/Parser/SVG/SVGView.swift index 8e9bf5d..1731d72 100644 --- a/Source/Parser/SVG/SVGView.swift +++ b/Source/Parser/SVG/SVGView.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 20/08/2020. // +#if os(WASI) +import Foundation +#else import SwiftUI +#endif public struct SVGView: View { diff --git a/Source/Parser/XML/DOMParser.swift b/Source/Parser/XML/DOMParser.swift index b4198fc..0b1da9b 100644 --- a/Source/Parser/XML/DOMParser.swift +++ b/Source/Parser/XML/DOMParser.swift @@ -5,7 +5,11 @@ // Created by Alisa Mylnikova on 20/08/2020. // +#if os(WASI) +import Foundation +#else import SwiftUI +#endif public struct DOMParser { diff --git a/Source/Parser/XML/XMLNode.swift b/Source/Parser/XML/XMLNode.swift index a58bede..d03b938 100644 --- a/Source/Parser/XML/XMLNode.swift +++ b/Source/Parser/XML/XMLNode.swift @@ -1,4 +1,8 @@ +#if os(WASI) +import Foundation +#else import SwiftUI +#endif public protocol XMLNode { } diff --git a/Source/Serialization/Serializations.swift b/Source/Serialization/Serializations.swift index 0e8571e..40748c8 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) import Foundation +#else import SwiftUI +#endif extension Bool: SerializableAtom { diff --git a/Source/UI/UIExtensions.swift b/Source/UI/UIExtensions.swift index 5297c83..9396a02 100644 --- a/Source/UI/UIExtensions.swift +++ b/Source/UI/UIExtensions.swift @@ -5,7 +5,11 @@ // Created by Yuri Strot on 25.05.2022. // +#if os(WASI) +import Foundation +#else import SwiftUI +#endif extension Shape { From cb812978821fc73e6889b481fba7706d9d2dfe58 Mon Sep 17 00:00:00 2001 From: khoi Date: Thu, 15 May 2025 15:13:43 +0700 Subject: [PATCH 02/26] Add conditional compilation for model properties in WASI target - Remove @Published property wrappers when targeting WASI platform - Maintain identical public API surface with conditional compilation - Continue implementation of WASI platform support from previous commit --- Source/Model/Images/SVGDataImage.swift | 4 ++++ Source/Model/Nodes/SVGGroup.swift | 4 ++++ Source/Model/Nodes/SVGImage.swift | 7 +++++++ Source/Model/Nodes/SVGNode.swift | 9 +++++++++ Source/Model/Nodes/SVGShape.swift | 5 +++++ Source/Model/Nodes/SVGText.swift | 8 ++++++++ Source/Model/Nodes/SVGViewport.swift | 10 +++++++--- Source/Model/Shapes/SVGCircle.swift | 6 ++++++ Source/Model/Shapes/SVGEllipse.swift | 7 +++++++ Source/Model/Shapes/SVGLine.swift | 7 +++++++ Source/Model/Shapes/SVGPath.swift | 5 +++++ Source/Model/Shapes/SVGPolygon.swift | 4 ++++ Source/Model/Shapes/SVGPolyline.swift | 4 ++++ Source/Model/Shapes/SVGRect.swift | 9 +++++++++ 14 files changed, 86 insertions(+), 3 deletions(-) diff --git a/Source/Model/Images/SVGDataImage.swift b/Source/Model/Images/SVGDataImage.swift index a58cbbb..352b811 100644 --- a/Source/Model/Images/SVGDataImage.swift +++ b/Source/Model/Images/SVGDataImage.swift @@ -15,7 +15,11 @@ import Combine public class SVGDataImage: SVGImage, ObservableObject { +#if os(WASI) + public var data: Data +#else @Published public var data: Data +#endif public init(x: CGFloat = 0, y: CGFloat = 0, width: CGFloat = 0, height: CGFloat = 0, data: Data) { self.data = data diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift index e21321a..6b9b9b6 100644 --- a/Source/Model/Nodes/SVGGroup.swift +++ b/Source/Model/Nodes/SVGGroup.swift @@ -7,7 +7,11 @@ import Combine public class SVGGroup: SVGNode, ObservableObject { +#if os(WASI) + 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) diff --git a/Source/Model/Nodes/SVGImage.swift b/Source/Model/Nodes/SVGImage.swift index 3ba9c9f..06d35a1 100644 --- a/Source/Model/Nodes/SVGImage.swift +++ b/Source/Model/Nodes/SVGImage.swift @@ -14,10 +14,17 @@ import Combine public class SVGImage: SVGNode { +#if os(WASI) + 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 82d570d..08b7bcb 100644 --- a/Source/Model/Nodes/SVGNode.swift +++ b/Source/Model/Nodes/SVGNode.swift @@ -7,12 +7,21 @@ import Combine public class SVGNode: SerializableElement { +#if os(WASI) + 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<()>]() diff --git a/Source/Model/Nodes/SVGShape.swift b/Source/Model/Nodes/SVGShape.swift index 519919e..396ee3c 100644 --- a/Source/Model/Nodes/SVGShape.swift +++ b/Source/Model/Nodes/SVGShape.swift @@ -7,8 +7,13 @@ import Combine public class SVGShape: SVGNode { +#if os(WASI) + 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 de314f6..8f10f3e 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -7,11 +7,19 @@ import Combine public class SVGText: SVGNode, ObservableObject { +#if os(WASI) + public var text: String + public var font: SVGFont? + public var fill: SVGPaint? + public var stroke: SVGStroke? + public var textAnchor: HorizontalAlignment = .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 +#endif 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) { self.text = text diff --git a/Source/Model/Nodes/SVGViewport.swift b/Source/Model/Nodes/SVGViewport.swift index 409d73c..64a84bc 100644 --- a/Source/Model/Nodes/SVGViewport.swift +++ b/Source/Model/Nodes/SVGViewport.swift @@ -7,29 +7,33 @@ import Combine public class SVGViewport: SVGGroup { +#if os(WASI) + 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() } } - @Published public var height: SVGLength { willSet { self.objectWillChange.send() } } - @Published public var viewBox: CGRect? { willSet { self.objectWillChange.send() } } - @Published public var preserveAspectRatio: SVGPreserveAspectRatio { willSet { self.objectWillChange.send() } } +#endif public init(width: SVGLength, height: SVGLength, viewBox: CGRect? = .none, preserveAspectRatio: SVGPreserveAspectRatio, contents: [SVGNode] = []) { self.width = width diff --git a/Source/Model/Shapes/SVGCircle.swift b/Source/Model/Shapes/SVGCircle.swift index 104ba32..02089b8 100644 --- a/Source/Model/Shapes/SVGCircle.swift +++ b/Source/Model/Shapes/SVGCircle.swift @@ -7,9 +7,15 @@ import Combine public class SVGCircle: SVGShape, ObservableObject { +#if os(WASI) + 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 diff --git a/Source/Model/Shapes/SVGEllipse.swift b/Source/Model/Shapes/SVGEllipse.swift index c3ec9cf..2edb8fe 100644 --- a/Source/Model/Shapes/SVGEllipse.swift +++ b/Source/Model/Shapes/SVGEllipse.swift @@ -7,10 +7,17 @@ import Combine public class SVGEllipse: SVGShape, ObservableObject { +#if os(WASI) + 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 diff --git a/Source/Model/Shapes/SVGLine.swift b/Source/Model/Shapes/SVGLine.swift index 6d7514b..262350e 100644 --- a/Source/Model/Shapes/SVGLine.swift +++ b/Source/Model/Shapes/SVGLine.swift @@ -7,10 +7,17 @@ import Combine public class SVGLine: SVGShape, ObservableObject { +#if os(WASI) + 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 diff --git a/Source/Model/Shapes/SVGPath.swift b/Source/Model/Shapes/SVGPath.swift index 83828da..1ace78d 100644 --- a/Source/Model/Shapes/SVGPath.swift +++ b/Source/Model/Shapes/SVGPath.swift @@ -7,8 +7,13 @@ import Combine public class SVGPath: SVGShape, ObservableObject { +#if os(WASI) + 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 diff --git a/Source/Model/Shapes/SVGPolygon.swift b/Source/Model/Shapes/SVGPolygon.swift index 37dcd2b..6add006 100644 --- a/Source/Model/Shapes/SVGPolygon.swift +++ b/Source/Model/Shapes/SVGPolygon.swift @@ -7,7 +7,11 @@ import Combine public class SVGPolygon: SVGShape, ObservableObject { +#if os(WASI) + public var points: [CGPoint] +#else @Published public var points: [CGPoint] +#endif public init(_ points: [CGPoint]) { self.points = points diff --git a/Source/Model/Shapes/SVGPolyline.swift b/Source/Model/Shapes/SVGPolyline.swift index 1bfe6e4..ece9d3c 100644 --- a/Source/Model/Shapes/SVGPolyline.swift +++ b/Source/Model/Shapes/SVGPolyline.swift @@ -7,7 +7,11 @@ import Combine public class SVGPolyline: SVGShape, ObservableObject { +#if os(WASI) + public var points: [CGPoint] +#else @Published public var points: [CGPoint] +#endif public init(_ points: [CGPoint]) { self.points = points diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift index 9cf73b9..651e80e 100644 --- a/Source/Model/Shapes/SVGRect.swift +++ b/Source/Model/Shapes/SVGRect.swift @@ -7,12 +7,21 @@ import Combine public class SVGRect: SVGShape, ObservableObject { +#if os(WASI) + 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 From a764e06c7801aa8cff8b7b4731db1e3949d5b1c3 Mon Sep 17 00:00:00 2001 From: khoi Date: Thu, 15 May 2025 15:45:13 +0700 Subject: [PATCH 03/26] Refactor WASI conditional compilation directives - Change approach from conditionally compiling model properties to conditionally compiling UI code - Wrap SwiftUI view implementations in #if !os(WASI) blocks instead of duplicating property declarations - Simplify code structure while maintaining full WASI platform compatibility - Improve maintainability by reducing conditional compilation complexity --- Source/Model/Images/SVGDataImage.swift | 8 ++++---- Source/Model/Images/SVGURLImage.swift | 4 ++++ Source/Model/Nodes/SVGGroup.swift | 8 ++++---- Source/Model/Nodes/SVGText.swift | 12 ++++-------- Source/Model/Nodes/SVGUserSpaceNode.swift | 4 ++++ Source/Model/Nodes/SVGViewport.swift | 12 +++++------- Source/Model/Primitives/SVGColor.swift | 4 ++++ Source/Model/Primitives/SVGGradient.swift | 4 ++++ Source/Model/Shapes/SVGCircle.swift | 10 ++++------ Source/Model/Shapes/SVGEllipse.swift | 11 ++++------- Source/Model/Shapes/SVGLine.swift | 11 ++++------- Source/Model/Shapes/SVGPath.swift | 11 ++++++----- Source/Model/Shapes/SVGPolygon.swift | 8 ++++---- Source/Model/Shapes/SVGPolyline.swift | 8 ++++---- Source/Model/Shapes/SVGRect.swift | 13 ++++--------- Source/Parser/SVG/SVGView.swift | 2 ++ Source/UI/UIExtensions.swift | 8 ++++++++ 17 files changed, 73 insertions(+), 65 deletions(-) diff --git a/Source/Model/Images/SVGDataImage.swift b/Source/Model/Images/SVGDataImage.swift index 352b811..f1d562e 100644 --- a/Source/Model/Images/SVGDataImage.swift +++ b/Source/Model/Images/SVGDataImage.swift @@ -15,11 +15,7 @@ import Combine public class SVGDataImage: SVGImage, ObservableObject { -#if os(WASI) - public var data: Data -#else @Published public var data: Data -#endif public init(x: CGFloat = 0, y: CGFloat = 0, width: CGFloat = 0, height: CGFloat = 0, data: Data) { self.data = data @@ -31,11 +27,14 @@ public class SVGDataImage: SVGImage, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGDataImageView(model: self) } + #endif } +#if !os(WASI) struct SVGDataImageView: View { #if os(OSX) @@ -64,3 +63,4 @@ struct SVGDataImageView: View { .applyNodeAttributes(model: model) } } +#endif diff --git a/Source/Model/Images/SVGURLImage.swift b/Source/Model/Images/SVGURLImage.swift index fdb33dd..ebad15e 100644 --- a/Source/Model/Images/SVGURLImage.swift +++ b/Source/Model/Images/SVGURLImage.swift @@ -27,11 +27,14 @@ public class SVGURLImage: SVGImage, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGUrlImageView(model: self) } + #endif } +#if !os(WASI) struct SVGUrlImageView: View { @ObservedObject var model: SVGURLImage @@ -60,4 +63,5 @@ struct SVGUrlImageView: View { .applyNodeAttributes(model: model) } } +#endif diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift index 6b9b9b6..b081400 100644 --- a/Source/Model/Nodes/SVGGroup.swift +++ b/Source/Model/Nodes/SVGGroup.swift @@ -7,11 +7,7 @@ import Combine public class SVGGroup: SVGNode, ObservableObject { -#if os(WASI) - 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) @@ -39,11 +35,14 @@ public class SVGGroup: SVGNode, ObservableObject { serializer.add("contents", contents) } + #if !os(WASI) public func contentView() -> some View { SVGGroupView(model: self) } + #endif } +#if !os(WASI) struct SVGGroupView: View { @ObservedObject var model: SVGGroup @@ -60,4 +59,5 @@ struct SVGGroupView: View { .applyNodeAttributes(model: model) } } +#endif diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift index 8f10f3e..b910c5a 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -7,19 +7,11 @@ import Combine public class SVGText: SVGNode, ObservableObject { -#if os(WASI) - public var text: String - public var font: SVGFont? - public var fill: SVGPaint? - public var stroke: SVGStroke? - public var textAnchor: HorizontalAlignment = .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 -#endif 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) { self.text = text @@ -37,11 +29,14 @@ public class SVGText: SVGNode, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGTextView(model: self) } + #endif } +#if !os(WASI) struct SVGTextView: View { @ObservedObject var model: SVGText @@ -74,3 +69,4 @@ struct SVGTextView: View { .frame(alignment: .topLeading) } } +#endif diff --git a/Source/Model/Nodes/SVGUserSpaceNode.swift b/Source/Model/Nodes/SVGUserSpaceNode.swift index 6e8c20c..d60b226 100644 --- a/Source/Model/Nodes/SVGUserSpaceNode.swift +++ b/Source/Model/Nodes/SVGUserSpaceNode.swift @@ -32,11 +32,14 @@ public class SVGUserSpaceNode: SVGNode { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGUserSpaceNodeView(model: self) } + #endif } +#if !os(WASI) struct SVGUserSpaceNodeView: View { let model: SVGUserSpaceNode @@ -48,3 +51,4 @@ struct SVGUserSpaceNodeView: View { } } } +#endif diff --git a/Source/Model/Nodes/SVGViewport.swift b/Source/Model/Nodes/SVGViewport.swift index 64a84bc..f9c9760 100644 --- a/Source/Model/Nodes/SVGViewport.swift +++ b/Source/Model/Nodes/SVGViewport.swift @@ -7,33 +7,29 @@ import Combine public class SVGViewport: SVGGroup { -#if os(WASI) - 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() } } + @Published public var height: SVGLength { willSet { self.objectWillChange.send() } } + @Published public var viewBox: CGRect? { willSet { self.objectWillChange.send() } } + @Published public var preserveAspectRatio: SVGPreserveAspectRatio { willSet { self.objectWillChange.send() } } -#endif public init(width: SVGLength, height: SVGLength, viewBox: CGRect? = .none, preserveAspectRatio: SVGPreserveAspectRatio, contents: [SVGNode] = []) { self.width = width @@ -65,6 +61,7 @@ public class SVGViewport: SVGGroup { } +#if !os(WASI) struct SVGViewportView: View { @ObservedObject var model: SVGViewport @@ -97,3 +94,4 @@ struct SVGViewportView: View { } } +#endif diff --git a/Source/Model/Primitives/SVGColor.swift b/Source/Model/Primitives/SVGColor.swift index 83e9e0a..107e97a 100644 --- a/Source/Model/Primitives/SVGColor.swift +++ b/Source/Model/Primitives/SVGColor.swift @@ -67,9 +67,11 @@ public class SVGColor: SVGPaint { return Color(red: Double(r) / 0xff, green: Double(g) / 0xff, blue: Double(b) / 0xff).opacity(opacity) } + #if !os(WASI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { view.foregroundColor(toSwiftUI()) } + #endif public var r: Int { return (value >> 16) & 0xff @@ -105,6 +107,7 @@ public func == (lhs: SVGColor, rhs: SVGColor) -> Bool { return lhs.value == rhs.value } +#if !os(WASI) extension Color: SerializableAtom { static func by(name: String) -> Color? { @@ -153,6 +156,7 @@ extension Color: SerializableAtom { } } +#endif class SVGColors { diff --git a/Source/Model/Primitives/SVGGradient.swift b/Source/Model/Primitives/SVGGradient.swift index 1364d5a..c027c93 100644 --- a/Source/Model/Primitives/SVGGradient.swift +++ b/Source/Model/Primitives/SVGGradient.swift @@ -69,6 +69,7 @@ public class SVGLinearGradient: SVGGradient { ) } + #if !os(WASI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { let frame = model?.frame() ?? CGRect() let bounds = model?.bounds() ?? CGRect() @@ -89,6 +90,7 @@ public class SVGLinearGradient: SVGGradient { .mask(view) ) } + #endif } @@ -120,6 +122,7 @@ public class SVGRadialGradient: SVGGradient { return RadialGradient(gradient: Gradient(stops: suiStops), center: UnitPoint(x: ncx, y: ncy), startRadius: 0, endRadius: userSpace ? r : r * s) } + #if !os(WASI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { let frame = model?.frame() ?? CGRect() let bounds = model?.bounds() ?? CGRect() @@ -136,6 +139,7 @@ public class SVGRadialGradient: SVGGradient { .mask(view) ) } + #endif } diff --git a/Source/Model/Shapes/SVGCircle.swift b/Source/Model/Shapes/SVGCircle.swift index 02089b8..f6ac74b 100644 --- a/Source/Model/Shapes/SVGCircle.swift +++ b/Source/Model/Shapes/SVGCircle.swift @@ -7,15 +7,9 @@ import Combine public class SVGCircle: SVGShape, ObservableObject { -#if os(WASI) - 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 @@ -32,11 +26,14 @@ public class SVGCircle: SVGShape, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGCircleView(model: self) } + #endif } +#if !os(WASI) struct SVGCircleView: View { @ObservedObject var model = SVGCircle() @@ -49,3 +46,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 2edb8fe..185d796 100644 --- a/Source/Model/Shapes/SVGEllipse.swift +++ b/Source/Model/Shapes/SVGEllipse.swift @@ -7,17 +7,10 @@ import Combine public class SVGEllipse: SVGShape, ObservableObject { -#if os(WASI) - 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 @@ -35,11 +28,14 @@ public class SVGEllipse: SVGShape, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGEllipseView(model: self) } + #endif } +#if !os(WASI) struct SVGEllipseView: View { @ObservedObject var model = SVGEllipse() @@ -52,4 +48,5 @@ struct SVGEllipseView: View { .applyShapeAttributes(model: model) } } +#endif diff --git a/Source/Model/Shapes/SVGLine.swift b/Source/Model/Shapes/SVGLine.swift index 262350e..091d303 100644 --- a/Source/Model/Shapes/SVGLine.swift +++ b/Source/Model/Shapes/SVGLine.swift @@ -7,17 +7,10 @@ import Combine public class SVGLine: SVGShape, ObservableObject { -#if os(WASI) - 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 @@ -42,11 +35,14 @@ public class SVGLine: SVGShape, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGLineView(model: self) } + #endif } +#if !os(WASI) struct SVGLineView: View { @ObservedObject var model = SVGLine() @@ -62,4 +58,5 @@ struct SVGLineView: View { return line } } +#endif diff --git a/Source/Model/Shapes/SVGPath.swift b/Source/Model/Shapes/SVGPath.swift index 1ace78d..037b11f 100644 --- a/Source/Model/Shapes/SVGPath.swift +++ b/Source/Model/Shapes/SVGPath.swift @@ -7,13 +7,8 @@ import Combine public class SVGPath: SVGShape, ObservableObject { -#if os(WASI) - 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 @@ -35,11 +30,14 @@ public class SVGPath: SVGShape, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGPathView(model: self) } + #endif } +#if !os(WASI) struct SVGPathView: View { @ObservedObject var model = SVGPath() @@ -48,7 +46,9 @@ struct SVGPathView: View { model.toBezierPath().toSwiftUI(model: model, eoFill: model.fillRule == .evenOdd) } } +#endif +#if !os(WASI) extension MBezierPath { func toSwiftUI(model: SVGShape, eoFill: Bool = false) -> some View { @@ -64,4 +64,5 @@ extension MBezierPath { } } } +#endif diff --git a/Source/Model/Shapes/SVGPolygon.swift b/Source/Model/Shapes/SVGPolygon.swift index 6add006..9b53bb8 100644 --- a/Source/Model/Shapes/SVGPolygon.swift +++ b/Source/Model/Shapes/SVGPolygon.swift @@ -7,11 +7,7 @@ import Combine public class SVGPolygon: SVGShape, ObservableObject { -#if os(WASI) - public var points: [CGPoint] -#else @Published public var points: [CGPoint] -#endif public init(_ points: [CGPoint]) { self.points = points @@ -53,11 +49,14 @@ public class SVGPolygon: SVGShape, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGPolygonView(model: self) } + #endif } +#if !os(WASI) struct SVGPolygonView: View { @ObservedObject var model = SVGPolygon() @@ -79,4 +78,5 @@ struct SVGPolygonView: View { return path } } +#endif diff --git a/Source/Model/Shapes/SVGPolyline.swift b/Source/Model/Shapes/SVGPolyline.swift index ece9d3c..c42ae28 100644 --- a/Source/Model/Shapes/SVGPolyline.swift +++ b/Source/Model/Shapes/SVGPolyline.swift @@ -7,11 +7,7 @@ import Combine public class SVGPolyline: SVGShape, ObservableObject { -#if os(WASI) - public var points: [CGPoint] -#else @Published public var points: [CGPoint] -#endif public init(_ points: [CGPoint]) { self.points = points @@ -53,11 +49,14 @@ public class SVGPolyline: SVGShape, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGPolylineView(model: self) } + #endif } +#if !os(WASI) struct SVGPolylineView: View { @ObservedObject var model = SVGPolyline() @@ -77,4 +76,5 @@ struct SVGPolylineView: View { return path } } +#endif diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift index 651e80e..f20bde3 100644 --- a/Source/Model/Shapes/SVGRect.swift +++ b/Source/Model/Shapes/SVGRect.swift @@ -7,21 +7,12 @@ import Combine public class SVGRect: SVGShape, ObservableObject { -#if os(WASI) - 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 @@ -49,11 +40,14 @@ public class SVGRect: SVGShape, ObservableObject { super.serialize(serializer) } + #if !os(WASI) public func contentView() -> some View { SVGRectView(model: self) } + #endif } +#if !os(WASI) struct SVGRectView: View { @ObservedObject var model: SVGRect @@ -67,3 +61,4 @@ struct SVGRectView: View { .offset(x: model.width/2, y: model.height/2) } } +#endif diff --git a/Source/Parser/SVG/SVGView.swift b/Source/Parser/SVG/SVGView.swift index 1731d72..83ac7be 100644 --- a/Source/Parser/SVG/SVGView.swift +++ b/Source/Parser/SVG/SVGView.swift @@ -11,6 +11,7 @@ import Foundation import SwiftUI #endif +#if !os(WASI) public struct SVGView: View { public let svg: SVGNode? @@ -53,3 +54,4 @@ public struct SVGView: View { } } +#endif diff --git a/Source/UI/UIExtensions.swift b/Source/UI/UIExtensions.swift index 9396a02..bd6c8c1 100644 --- a/Source/UI/UIExtensions.swift +++ b/Source/UI/UIExtensions.swift @@ -11,6 +11,7 @@ import Foundation import SwiftUI #endif +#if !os(WASI) extension Shape { @ViewBuilder @@ -45,7 +46,9 @@ extension Shape { } } +#endif +#if !os(WASI) extension View { func applyShapeAttributes(model: SVGShape) -> some View { @@ -74,7 +77,9 @@ extension View { } } +#endif +#if !os(WASI) extension View { @ViewBuilder @@ -90,7 +95,9 @@ extension View { } } } +#endif +#if !os(WASI) extension View { @ViewBuilder @@ -103,3 +110,4 @@ extension View { } } +#endif From fab1eaeec86406d44fecf77863b4c95a4bc7c5c7 Mon Sep 17 00:00:00 2001 From: khoi Date: Mon, 19 May 2025 11:38:30 +0700 Subject: [PATCH 04/26] Update package to Swift 5.9 and macOS 14 - Upgrade Swift tools version from 5.3 to 5.9 - Increase minimum macOS version from 11 to 14 - Maintain existing requirements for iOS (14) and watchOS (7) --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 7f5e8f7..a4bc055 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) ], From 020fd0b35f6206657c4ee4312eb0a7d9b04bb92a Mon Sep 17 00:00:00 2001 From: khoi Date: Mon, 19 May 2025 11:56:33 +0700 Subject: [PATCH 05/26] Remove cgPath property from MBezierPath macOS extension --- Source/UI/MBezierPath+Extension_macOS.swift | 31 --------------------- 1 file changed, 31 deletions(-) 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) From 3682ff3d9089b6995b97d569fab95fdd2590698d Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 12:28:18 +0700 Subject: [PATCH 06/26] Add Linux platform support - Change `#if os(WASI)` to `#if os(WASI) || os(Linux)` throughout codebase - Replace `#if !os(WASI)` with `#if canImport(SwiftUI)` for better platform detection - Add conditional compilation for published properties on Linux platform - Ensure proper handling of SwiftUI dependencies across all platforms --- Source/Model/Images/SVGDataImage.swift | 10 +++++++--- Source/Model/Images/SVGURLImage.swift | 6 +++--- Source/Model/Nodes/SVGGroup.swift | 12 +++++++----- Source/Model/Nodes/SVGImage.swift | 4 ++-- Source/Model/Nodes/SVGNode.swift | 4 ++-- Source/Model/Nodes/SVGShape.swift | 4 ++-- Source/Model/Nodes/SVGText.swift | 16 ++++++++++++---- Source/Model/Nodes/SVGUserSpaceNode.swift | 6 +++--- Source/Model/Nodes/SVGViewport.swift | 12 +++++++++--- Source/Model/Primitives/SVGColor.swift | 6 +++--- Source/Model/Primitives/SVGFont.swift | 2 +- Source/Model/Primitives/SVGGradient.swift | 6 +++--- Source/Model/Primitives/SVGLength.swift | 2 +- Source/Model/Primitives/SVGPaint.swift | 2 +- .../Primitives/SVGPreserveAspectRatio.swift | 2 +- Source/Model/Primitives/SVGStroke.swift | 2 +- Source/Model/Shapes/SVGCircle.swift | 13 ++++++++++--- Source/Model/Shapes/SVGEllipse.swift | 15 ++++++++++----- Source/Model/Shapes/SVGLine.swift | 13 ++++++++++--- Source/Model/Shapes/SVGPath.swift | 13 +++++++++---- Source/Model/Shapes/SVGPolygon.swift | 10 +++++++--- Source/Model/Shapes/SVGPolyline.swift | 11 +++++++---- Source/Model/Shapes/SVGRect.swift | 16 ++++++++++++---- .../SVG/Attributes/SVGFontSizeAttribute.swift | 1 - .../SVG/Attributes/SVGLengthAttribute.swift | 1 - Source/Parser/SVG/Elements/SVGImageParser.swift | 2 +- Source/Parser/SVG/Elements/SVGShapeParser.swift | 1 - .../SVG/Elements/SVGStructureParsers.swift | 1 - Source/Parser/SVG/Elements/SVGTextParser.swift | 2 +- .../Parser/SVG/Primitives/SVGLengthParser.swift | 1 - Source/Parser/SVG/SVGContext.swift | 2 -- Source/Parser/SVG/SVGIndex.swift | 2 +- Source/Parser/SVG/SVGParser.swift | 2 +- Source/Parser/SVG/SVGParserBasics.swift | 2 +- Source/Parser/SVG/SVGParserExtensions.swift | 2 -- Source/Parser/SVG/SVGParserPrimitives.swift | 2 +- Source/Parser/SVG/SVGPathReader.swift | 2 +- Source/Parser/SVG/SVGView.swift | 4 ++-- Source/Parser/SVG/Settings/SVGScreen.swift | 1 - Source/Parser/SVG/Settings/SVGSettings.swift | 1 - Source/Parser/XML/DOMParser.swift | 2 +- Source/Parser/XML/XMLNode.swift | 2 +- Source/Serialization/Serializations.swift | 2 +- Source/Serialization/Serializer.swift | 1 - Source/UI/UIExtensions.swift | 10 +++++----- 45 files changed, 140 insertions(+), 93 deletions(-) diff --git a/Source/Model/Images/SVGDataImage.swift b/Source/Model/Images/SVGDataImage.swift index f1d562e..aacc4f5 100644 --- a/Source/Model/Images/SVGDataImage.swift +++ b/Source/Model/Images/SVGDataImage.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 10/06/2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -15,7 +15,11 @@ import Combine public class SVGDataImage: SVGImage, ObservableObject { + #if os(WASI) || os(Linux) + public var data: Data + #else @Published public var data: Data + #endif public init(x: CGFloat = 0, y: CGFloat = 0, width: CGFloat = 0, height: CGFloat = 0, data: Data) { self.data = data @@ -27,14 +31,14 @@ public class SVGDataImage: SVGImage, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGDataImageView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGDataImageView: View { #if os(OSX) diff --git a/Source/Model/Images/SVGURLImage.swift b/Source/Model/Images/SVGURLImage.swift index ebad15e..ec79d5c 100644 --- a/Source/Model/Images/SVGURLImage.swift +++ b/Source/Model/Images/SVGURLImage.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 22/09/2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -27,14 +27,14 @@ public class SVGURLImage: SVGImage, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGUrlImageView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGUrlImageView: View { @ObservedObject var model: SVGURLImage diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift index b081400..e91b025 100644 --- a/Source/Model/Nodes/SVGGroup.swift +++ b/Source/Model/Nodes/SVGGroup.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -6,9 +6,11 @@ import Combine #endif public class SVGGroup: SVGNode, ObservableObject { - + #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 @@ -35,14 +37,14 @@ public class SVGGroup: SVGNode, ObservableObject { serializer.add("contents", contents) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGGroupView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGGroupView: View { @ObservedObject var model: SVGGroup diff --git a/Source/Model/Nodes/SVGImage.swift b/Source/Model/Nodes/SVGImage.swift index 06d35a1..da2d974 100644 --- a/Source/Model/Nodes/SVGImage.swift +++ b/Source/Model/Nodes/SVGImage.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 03/06/2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -14,7 +14,7 @@ import Combine public class SVGImage: SVGNode { -#if os(WASI) +#if os(WASI) || os(Linux) public var x: CGFloat public var y: CGFloat public var width: CGFloat diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift index 08b7bcb..3bb810d 100644 --- a/Source/Model/Nodes/SVGNode.swift +++ b/Source/Model/Nodes/SVGNode.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -7,7 +7,7 @@ import Combine public class SVGNode: SerializableElement { -#if os(WASI) +#if os(WASI) || os(Linux) public var transform: CGAffineTransform = CGAffineTransform.identity public var opaque: Bool public var opacity: Double diff --git a/Source/Model/Nodes/SVGShape.swift b/Source/Model/Nodes/SVGShape.swift index 396ee3c..ea23743 100644 --- a/Source/Model/Nodes/SVGShape.swift +++ b/Source/Model/Nodes/SVGShape.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -7,7 +7,7 @@ import Combine public class SVGShape: SVGNode { -#if os(WASI) +#if os(WASI) || os(Linux) public var fill: SVGPaint? public var stroke: SVGStroke? #else diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift index b910c5a..e86be72 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -7,12 +7,20 @@ import Combine public class SVGText: SVGNode, ObservableObject { + #if os(WASI) || os(Linux) @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 - + #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 + #endif + 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) { self.text = text self.font = font @@ -29,14 +37,14 @@ public class SVGText: SVGNode, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGTextView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGTextView: View { @ObservedObject var model: SVGText diff --git a/Source/Model/Nodes/SVGUserSpaceNode.swift b/Source/Model/Nodes/SVGUserSpaceNode.swift index d60b226..c48c6c1 100644 --- a/Source/Model/Nodes/SVGUserSpaceNode.swift +++ b/Source/Model/Nodes/SVGUserSpaceNode.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 14/10/2020. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -32,14 +32,14 @@ public class SVGUserSpaceNode: SVGNode { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGUserSpaceNodeView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGUserSpaceNodeView: View { let model: SVGUserSpaceNode diff --git a/Source/Model/Nodes/SVGViewport.swift b/Source/Model/Nodes/SVGViewport.swift index f9c9760..8911dce 100644 --- a/Source/Model/Nodes/SVGViewport.swift +++ b/Source/Model/Nodes/SVGViewport.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -6,7 +6,12 @@ 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() @@ -30,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 @@ -61,7 +67,7 @@ public class SVGViewport: SVGGroup { } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGViewportView: View { @ObservedObject var model: SVGViewport diff --git a/Source/Model/Primitives/SVGColor.swift b/Source/Model/Primitives/SVGColor.swift index 107e97a..34eb991 100644 --- a/Source/Model/Primitives/SVGColor.swift +++ b/Source/Model/Primitives/SVGColor.swift @@ -5,7 +5,7 @@ // Created by Yuriy Strot on 19.01.2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -67,7 +67,7 @@ public class SVGColor: SVGPaint { return Color(red: Double(r) / 0xff, green: Double(g) / 0xff, blue: Double(b) / 0xff).opacity(opacity) } - #if !os(WASI) + #if canImport(SwiftUI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { view.foregroundColor(toSwiftUI()) } @@ -107,7 +107,7 @@ public func == (lhs: SVGColor, rhs: SVGColor) -> Bool { return lhs.value == rhs.value } -#if !os(WASI) +#if canImport(SwiftUI) extension Color: SerializableAtom { static func by(name: String) -> Color? { diff --git a/Source/Model/Primitives/SVGFont.swift b/Source/Model/Primitives/SVGFont.swift index f04b8bd..693488f 100644 --- a/Source/Model/Primitives/SVGFont.swift +++ b/Source/Model/Primitives/SVGFont.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGGradient.swift b/Source/Model/Primitives/SVGGradient.swift index c027c93..c4033e2 100644 --- a/Source/Model/Primitives/SVGGradient.swift +++ b/Source/Model/Primitives/SVGGradient.swift @@ -5,7 +5,7 @@ // Created by Yuriy Strot on 22.02.2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -69,7 +69,7 @@ public class SVGLinearGradient: SVGGradient { ) } - #if !os(WASI) + #if canImport(SwiftUI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { let frame = model?.frame() ?? CGRect() let bounds = model?.bounds() ?? CGRect() @@ -122,7 +122,7 @@ public class SVGRadialGradient: SVGGradient { return RadialGradient(gradient: Gradient(stops: suiStops), center: UnitPoint(x: ncx, y: ncy), startRadius: 0, endRadius: userSpace ? r : r * s) } - #if !os(WASI) + #if canImport(SwiftUI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { let frame = model?.frame() ?? CGRect() let bounds = model?.bounds() ?? CGRect() diff --git a/Source/Model/Primitives/SVGLength.swift b/Source/Model/Primitives/SVGLength.swift index 349859f..466afb3 100644 --- a/Source/Model/Primitives/SVGLength.swift +++ b/Source/Model/Primitives/SVGLength.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 13/10/2020. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGPaint.swift b/Source/Model/Primitives/SVGPaint.swift index 367cdc9..5441d0b 100644 --- a/Source/Model/Primitives/SVGPaint.swift +++ b/Source/Model/Primitives/SVGPaint.swift @@ -5,7 +5,7 @@ // Created by Yuriy Strot on 19.01.2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGPreserveAspectRatio.swift b/Source/Model/Primitives/SVGPreserveAspectRatio.swift index 4cf601f..0c6461f 100644 --- a/Source/Model/Primitives/SVGPreserveAspectRatio.swift +++ b/Source/Model/Primitives/SVGPreserveAspectRatio.swift @@ -5,7 +5,7 @@ // Created by Yuriy Strot on 20.01.2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Model/Primitives/SVGStroke.swift b/Source/Model/Primitives/SVGStroke.swift index 76d0aca..dd88a62 100644 --- a/Source/Model/Primitives/SVGStroke.swift +++ b/Source/Model/Primitives/SVGStroke.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Model/Shapes/SVGCircle.swift b/Source/Model/Shapes/SVGCircle.swift index f6ac74b..d447cfb 100644 --- a/Source/Model/Shapes/SVGCircle.swift +++ b/Source/Model/Shapes/SVGCircle.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -7,9 +7,16 @@ import Combine public class SVGCircle: SVGShape, ObservableObject { + #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 @@ -26,14 +33,14 @@ public class SVGCircle: SVGShape, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGCircleView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGCircleView: View { @ObservedObject var model = SVGCircle() diff --git a/Source/Model/Shapes/SVGEllipse.swift b/Source/Model/Shapes/SVGEllipse.swift index 185d796..f6ec454 100644 --- a/Source/Model/Shapes/SVGEllipse.swift +++ b/Source/Model/Shapes/SVGEllipse.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -6,12 +6,17 @@ import Combine #endif public class SVGEllipse: SVGShape, ObservableObject { - + #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 @@ -28,14 +33,14 @@ public class SVGEllipse: SVGShape, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGEllipseView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGEllipseView: View { @ObservedObject var model = SVGEllipse() diff --git a/Source/Model/Shapes/SVGLine.swift b/Source/Model/Shapes/SVGLine.swift index 091d303..246965b 100644 --- a/Source/Model/Shapes/SVGLine.swift +++ b/Source/Model/Shapes/SVGLine.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -7,10 +7,17 @@ import Combine public class SVGLine: SVGShape, ObservableObject { + #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 @@ -35,14 +42,14 @@ public class SVGLine: SVGShape, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGLineView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGLineView: View { @ObservedObject var model = SVGLine() diff --git a/Source/Model/Shapes/SVGPath.swift b/Source/Model/Shapes/SVGPath.swift index 037b11f..d284114 100644 --- a/Source/Model/Shapes/SVGPath.swift +++ b/Source/Model/Shapes/SVGPath.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -7,8 +7,13 @@ import Combine public class SVGPath: SVGShape, ObservableObject { + #if os(WASI) || os(Linux) @Published public var segments: [PathSegment] @Published public var fillRule: CGPathFillRule + #else + public var segments: [PathSegment] + public var fillRule: CGPathFillRule + #endif public init(segments: [PathSegment] = [], fillRule: CGPathFillRule = .winding) { self.segments = segments @@ -30,14 +35,14 @@ public class SVGPath: SVGShape, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGPathView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGPathView: View { @ObservedObject var model = SVGPath() @@ -48,7 +53,7 @@ struct SVGPathView: View { } #endif -#if !os(WASI) +#if canImport(SwiftUI) extension MBezierPath { func toSwiftUI(model: SVGShape, eoFill: Bool = false) -> some View { diff --git a/Source/Model/Shapes/SVGPolygon.swift b/Source/Model/Shapes/SVGPolygon.swift index 9b53bb8..eaa0dd4 100644 --- a/Source/Model/Shapes/SVGPolygon.swift +++ b/Source/Model/Shapes/SVGPolygon.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -7,7 +7,11 @@ import Combine public class SVGPolygon: SVGShape, ObservableObject { + #if os(WASI) || os(Linux) @Published public var points: [CGPoint] + #else + public var points: [CGPoint] + #endif public init(_ points: [CGPoint]) { self.points = points @@ -49,14 +53,14 @@ public class SVGPolygon: SVGShape, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGPolygonView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGPolygonView: View { @ObservedObject var model = SVGPolygon() diff --git a/Source/Model/Shapes/SVGPolyline.swift b/Source/Model/Shapes/SVGPolyline.swift index c42ae28..309afc0 100644 --- a/Source/Model/Shapes/SVGPolyline.swift +++ b/Source/Model/Shapes/SVGPolyline.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -6,8 +6,11 @@ import Combine #endif public class SVGPolyline: SVGShape, ObservableObject { - + #if os(WASI) || os(Linux) + public var points: [CGPoint] + #else @Published public var points: [CGPoint] + #endif public init(_ points: [CGPoint]) { self.points = points @@ -49,14 +52,14 @@ public class SVGPolyline: SVGShape, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGPolylineView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGPolylineView: View { @ObservedObject var model = SVGPolyline() diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift index f20bde3..2c5cc5a 100644 --- a/Source/Model/Shapes/SVGRect.swift +++ b/Source/Model/Shapes/SVGRect.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI @@ -7,13 +7,21 @@ import Combine public class SVGRect: SVGShape, ObservableObject { + #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 @@ -40,14 +48,14 @@ public class SVGRect: SVGShape, ObservableObject { super.serialize(serializer) } - #if !os(WASI) + #if canImport(SwiftUI) public func contentView() -> some View { SVGRectView(model: self) } #endif } -#if !os(WASI) +#if canImport(SwiftUI) struct SVGRectView: View { @ObservedObject var model: SVGRect diff --git a/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift b/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift index 6f92235..92859dd 100644 --- a/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift @@ -5,7 +5,6 @@ // Created by Yuri Strot on 29.05.2022. // -import CoreGraphics class SVGFontSizeAttribute: SVGDefaultAttribute { diff --git a/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift b/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift index 25205c6..0aa5561 100644 --- a/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift @@ -5,7 +5,6 @@ // Created by Yuri Strot on 29.05.2022. // -import CoreGraphics class SVGLengthAttribute: SVGDefaultAttribute { diff --git a/Source/Parser/SVG/Elements/SVGImageParser.swift b/Source/Parser/SVG/Elements/SVGImageParser.swift index 652af4d..6a8e865 100644 --- a/Source/Parser/SVG/Elements/SVGImageParser.swift +++ b/Source/Parser/SVG/Elements/SVGImageParser.swift @@ -5,7 +5,7 @@ // Created by Yuri Strot on 29.05.2022. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI 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 558b090..e3924ef 100644 --- a/Source/Parser/SVG/Elements/SVGTextParser.swift +++ b/Source/Parser/SVG/Elements/SVGTextParser.swift @@ -5,7 +5,7 @@ // Created by Yuri Strot on 29.05.2022. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI 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..a2923d9 100644 --- a/Source/Parser/SVG/SVGContext.swift +++ b/Source/Parser/SVG/SVGContext.swift @@ -5,8 +5,6 @@ // Created by Yuri Strot on 26.05.2022. // -import CoreGraphics - protocol SVGContext { var logger: SVGLogger { get } diff --git a/Source/Parser/SVG/SVGIndex.swift b/Source/Parser/SVG/SVGIndex.swift index 0f6024f..b1a3d18 100644 --- a/Source/Parser/SVG/SVGIndex.swift +++ b/Source/Parser/SVG/SVGIndex.swift @@ -5,7 +5,7 @@ // Created by Yuriy Strot on 21.02.2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI 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 ac5649f..3c15b18 100644 --- a/Source/Parser/SVG/SVGParserBasics.swift +++ b/Source/Parser/SVG/SVGParserBasics.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 17/07/2020. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Parser/SVG/SVGParserExtensions.swift b/Source/Parser/SVG/SVGParserExtensions.swift index ecfb6ed..a9c2e18 100644 --- a/Source/Parser/SVG/SVGParserExtensions.swift +++ b/Source/Parser/SVG/SVGParserExtensions.swift @@ -5,10 +5,8 @@ // Created by Yuri Strot on 25.05.2022. // -import CoreGraphics extension CGFloat { - var degreesToRadians: CGFloat { return self * .pi / 180 } diff --git a/Source/Parser/SVG/SVGParserPrimitives.swift b/Source/Parser/SVG/SVGParserPrimitives.swift index e609d47..250c5ef 100644 --- a/Source/Parser/SVG/SVGParserPrimitives.swift +++ b/Source/Parser/SVG/SVGParserPrimitives.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 20/07/2020. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Parser/SVG/SVGPathReader.swift b/Source/Parser/SVG/SVGPathReader.swift index 6503635..5b763dd 100644 --- a/Source/Parser/SVG/SVGPathReader.swift +++ b/Source/Parser/SVG/SVGPathReader.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 23/07/2020. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Parser/SVG/SVGView.swift b/Source/Parser/SVG/SVGView.swift index 83ac7be..119437c 100644 --- a/Source/Parser/SVG/SVGView.swift +++ b/Source/Parser/SVG/SVGView.swift @@ -5,13 +5,13 @@ // Created by Alisa Mylnikova on 20/08/2020. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI #endif -#if !os(WASI) +#if canImport(SwiftUI) public struct SVGView: View { public let svg: SVGNode? 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 0b1da9b..e5f3205 100644 --- a/Source/Parser/XML/DOMParser.swift +++ b/Source/Parser/XML/DOMParser.swift @@ -5,7 +5,7 @@ // Created by Alisa Mylnikova on 20/08/2020. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Parser/XML/XMLNode.swift b/Source/Parser/XML/XMLNode.swift index d03b938..2a2cfcc 100644 --- a/Source/Parser/XML/XMLNode.swift +++ b/Source/Parser/XML/XMLNode.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI diff --git a/Source/Serialization/Serializations.swift b/Source/Serialization/Serializations.swift index 40748c8..537d12b 100644 --- a/Source/Serialization/Serializations.swift +++ b/Source/Serialization/Serializations.swift @@ -5,7 +5,7 @@ // Created by Yuriy Strot on 18.01.2021. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI 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/UIExtensions.swift b/Source/UI/UIExtensions.swift index bd6c8c1..a35d9d2 100644 --- a/Source/UI/UIExtensions.swift +++ b/Source/UI/UIExtensions.swift @@ -5,13 +5,13 @@ // Created by Yuri Strot on 25.05.2022. // -#if os(WASI) +#if os(WASI) || os(Linux) import Foundation #else import SwiftUI #endif -#if !os(WASI) +#if canImport(SwiftUI) extension Shape { @ViewBuilder @@ -48,7 +48,7 @@ extension Shape { } #endif -#if !os(WASI) +#if canImport(SwiftUI) extension View { func applyShapeAttributes(model: SVGShape) -> some View { @@ -79,7 +79,7 @@ extension View { } #endif -#if !os(WASI) +#if canImport(SwiftUI) extension View { @ViewBuilder @@ -97,7 +97,7 @@ extension View { } #endif -#if !os(WASI) +#if canImport(SwiftUI) extension View { @ViewBuilder From eb8bb34945ae0abe9ebb1dfe5defa3edaeba98cc Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 12:30:10 +0700 Subject: [PATCH 07/26] Add CoreGraphics polyfill for Linux and WASI platforms - Implement CGPath, CGAffineTransform and related types for non-Apple platforms - Create MBezierPath implementation compatible with Linux/WASI - Add mathematical function wrappers needed for graphics operations - Use conditional compilation to maintain native CoreGraphics on Apple platforms --- Source/CoreGraphicsPolyfill.swift | 493 ++++++++++++++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100644 Source/CoreGraphicsPolyfill.swift diff --git a/Source/CoreGraphicsPolyfill.swift b/Source/CoreGraphicsPolyfill.swift new file mode 100644 index 0000000..7af0340 --- /dev/null +++ b/Source/CoreGraphicsPolyfill.swift @@ -0,0 +1,493 @@ +// +// CoreGraphicsPolyfills.swift +// SVGView +// +// Created by khoi on 10/5/25. +// + +import Foundation + +public typealias CGFloat = Foundation.CGFloat +public typealias CGSize = Foundation.CGSize +public typealias CGPoint = Foundation.CGPoint + +#if os(WASI) || os(Linux) + import Glibc + public func sqrt(_ x: CGFloat) -> CGFloat { return x.squareRoot() } + public func copysign(_ x: CGFloat, _ y: CGFloat) -> CGFloat { + let magnitude = x >= 0 ? x : -x + return y >= 0 ? magnitude : -magnitude + } + public func acos(_ x: CGFloat) -> CGFloat { + return Glibc.acos(x) + } + + public func cos(_ x: CGFloat) -> CGFloat { + return Glibc.cos(x) + } + + public func sin(_ x: CGFloat) -> CGFloat { + return Glibc.sin(x) + } + + 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 struct 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 mutating 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 mutating 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 mutating func move(to point: CGPoint) { + + elements.append(.moveToPoint(point)) + } + + public mutating func addLine(to point: CGPoint) { + + elements.append(.addLineToPoint(point)) + } + + public mutating func addCurve(to endPoint: CGPoint, control1: CGPoint, control2: CGPoint) { + + elements.append(.addCurveToPoint(control1, control2, endPoint)) + } + + public mutating func addQuadCurve(to endPoint: CGPoint, control: CGPoint) { + + elements.append(.addQuadCurveToPoint(control, endPoint)) + } + + public mutating 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 struct 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 mutating func move(to point: CGPoint) { + cgPath.move(to: point) + } + + public mutating func addLine(to point: CGPoint) { + cgPath.addLine(to: point) + } + + public mutating func addCurve( + to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint + ) { + cgPath.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) + } + + public mutating func addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint) { + cgPath.addQuadCurve(to: endPoint, control: controlPoint) + } + + public mutating 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 mutating func append(_ path: MBezierPath) { + cgPath.elements.append(contentsOf: path.cgPath.elements) + } + + public mutating func close() { + cgPath.closeSubpath() + } + + public mutating 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: inout 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.. Date: Tue, 20 May 2025 12:46:19 +0700 Subject: [PATCH 08/26] Move ObservableObject to extensions across SVG component classes - Improves code organization by separating core SVG functionality from SwiftUI integrations - Reduces platform-specific dependencies in base class definitions - Makes the codebase more maintainable by isolating UI framework bindings - Consistent with modern Swift architectural practices of protocol conformance via extensions --- Source/Model/Images/SVGDataImage.swift | 5 ++--- Source/Model/Images/SVGURLImage.swift | 3 ++- Source/Model/Nodes/SVGGroup.swift | 3 ++- Source/Model/Nodes/SVGText.swift | 13 +++++++------ Source/Model/Shapes/SVGCircle.swift | 3 ++- Source/Model/Shapes/SVGEllipse.swift | 3 ++- Source/Model/Shapes/SVGLine.swift | 3 ++- Source/Model/Shapes/SVGPath.swift | 9 +++++---- Source/Model/Shapes/SVGPolygon.swift | 7 ++++--- Source/Model/Shapes/SVGPolyline.swift | 3 ++- Source/Model/Shapes/SVGRect.swift | 3 ++- 11 files changed, 32 insertions(+), 23 deletions(-) diff --git a/Source/Model/Images/SVGDataImage.swift b/Source/Model/Images/SVGDataImage.swift index aacc4f5..7102972 100644 --- a/Source/Model/Images/SVGDataImage.swift +++ b/Source/Model/Images/SVGDataImage.swift @@ -12,9 +12,7 @@ import SwiftUI import Combine #endif - -public class SVGDataImage: SVGImage, ObservableObject { - +public class SVGDataImage: SVGImage { #if os(WASI) || os(Linux) public var data: Data #else @@ -39,6 +37,7 @@ public class SVGDataImage: SVGImage, ObservableObject { } #if canImport(SwiftUI) +extension SVGDataImage: ObservableObject {} struct SVGDataImageView: View { #if os(OSX) diff --git a/Source/Model/Images/SVGURLImage.swift b/Source/Model/Images/SVGURLImage.swift index ec79d5c..bca987a 100644 --- a/Source/Model/Images/SVGURLImage.swift +++ b/Source/Model/Images/SVGURLImage.swift @@ -11,7 +11,7 @@ import Foundation import SwiftUI #endif -public class SVGURLImage: SVGImage, ObservableObject { +public class SVGURLImage: SVGImage { public let src: String public let data: Data? @@ -35,6 +35,7 @@ public class SVGURLImage: SVGImage, ObservableObject { } #if canImport(SwiftUI) +extension SVGURLImage: ObservableObject {} struct SVGUrlImageView: View { @ObservedObject var model: SVGURLImage diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift index e91b025..406839a 100644 --- a/Source/Model/Nodes/SVGGroup.swift +++ b/Source/Model/Nodes/SVGGroup.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine #endif -public class SVGGroup: SVGNode, ObservableObject { +public class SVGGroup: SVGNode { #if os(WASI) || os(Linux) public var contents: [SVGNode] = [] #else @@ -45,6 +45,7 @@ public class SVGGroup: SVGNode, ObservableObject { } #if canImport(SwiftUI) +extension SVGGroup: ObservableObject {} struct SVGGroupView: View { @ObservedObject var model: SVGGroup diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift index e86be72..7a70641 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -5,14 +5,14 @@ import SwiftUI import Combine #endif -public class SVGText: SVGNode, ObservableObject { +public class SVGText: SVGNode { #if os(WASI) || os(Linux) - @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 var text: String + public var font: SVGFont? + public var fill: SVGPaint? + public var stroke: SVGStroke? + public var textAnchor: HorizontalAlignment = .leading #else @Published public var text: String @Published public var font: SVGFont? @@ -45,6 +45,7 @@ public class SVGText: SVGNode, ObservableObject { } #if canImport(SwiftUI) +extension SVGText: ObservableObject {} struct SVGTextView: View { @ObservedObject var model: SVGText diff --git a/Source/Model/Shapes/SVGCircle.swift b/Source/Model/Shapes/SVGCircle.swift index d447cfb..49f8e19 100644 --- a/Source/Model/Shapes/SVGCircle.swift +++ b/Source/Model/Shapes/SVGCircle.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine #endif -public class SVGCircle: SVGShape, ObservableObject { +public class SVGCircle: SVGShape { #if os(WASI) || os(Linux) public var cx: CGFloat @@ -41,6 +41,7 @@ public class SVGCircle: SVGShape, ObservableObject { } #if canImport(SwiftUI) +extension SVGCircle: ObservableObject {} struct SVGCircleView: View { @ObservedObject var model = SVGCircle() diff --git a/Source/Model/Shapes/SVGEllipse.swift b/Source/Model/Shapes/SVGEllipse.swift index f6ec454..efa9e25 100644 --- a/Source/Model/Shapes/SVGEllipse.swift +++ b/Source/Model/Shapes/SVGEllipse.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine #endif -public class SVGEllipse: SVGShape, ObservableObject { +public class SVGEllipse: SVGShape { #if os(WASI) || os(Linux) public var cx: CGFloat public var cy: CGFloat @@ -41,6 +41,7 @@ public class SVGEllipse: SVGShape, ObservableObject { } #if canImport(SwiftUI) +extension SVGEllipse: ObservableObject {} struct SVGEllipseView: View { @ObservedObject var model = SVGEllipse() diff --git a/Source/Model/Shapes/SVGLine.swift b/Source/Model/Shapes/SVGLine.swift index 246965b..825e819 100644 --- a/Source/Model/Shapes/SVGLine.swift +++ b/Source/Model/Shapes/SVGLine.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine #endif -public class SVGLine: SVGShape, ObservableObject { +public class SVGLine: SVGShape { #if os(WASI) || os(Linux) public var x1: CGFloat @@ -50,6 +50,7 @@ public class SVGLine: SVGShape, ObservableObject { } #if canImport(SwiftUI) +extension SVGLine: ObservableObject {} struct SVGLineView: View { @ObservedObject var model = SVGLine() diff --git a/Source/Model/Shapes/SVGPath.swift b/Source/Model/Shapes/SVGPath.swift index d284114..0e9b86e 100644 --- a/Source/Model/Shapes/SVGPath.swift +++ b/Source/Model/Shapes/SVGPath.swift @@ -5,14 +5,14 @@ import SwiftUI import Combine #endif -public class SVGPath: SVGShape, ObservableObject { +public class SVGPath: SVGShape { #if os(WASI) || os(Linux) - @Published public var segments: [PathSegment] - @Published public var fillRule: CGPathFillRule - #else 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) { @@ -43,6 +43,7 @@ public class SVGPath: SVGShape, ObservableObject { } #if canImport(SwiftUI) +extension SVGPath: ObservableObject {} struct SVGPathView: View { @ObservedObject var model = SVGPath() diff --git a/Source/Model/Shapes/SVGPolygon.swift b/Source/Model/Shapes/SVGPolygon.swift index eaa0dd4..cc205dc 100644 --- a/Source/Model/Shapes/SVGPolygon.swift +++ b/Source/Model/Shapes/SVGPolygon.swift @@ -5,12 +5,12 @@ import SwiftUI import Combine #endif -public class SVGPolygon: SVGShape, ObservableObject { +public class SVGPolygon: SVGShape { #if os(WASI) || os(Linux) - @Published public var points: [CGPoint] - #else public var points: [CGPoint] + #else + @Published public var points: [CGPoint] #endif public init(_ points: [CGPoint]) { @@ -61,6 +61,7 @@ public class SVGPolygon: SVGShape, ObservableObject { } #if canImport(SwiftUI) +extension SVGPolygon: ObservableObject {} struct SVGPolygonView: View { @ObservedObject var model = SVGPolygon() diff --git a/Source/Model/Shapes/SVGPolyline.swift b/Source/Model/Shapes/SVGPolyline.swift index 309afc0..0a9deeb 100644 --- a/Source/Model/Shapes/SVGPolyline.swift +++ b/Source/Model/Shapes/SVGPolyline.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine #endif -public class SVGPolyline: SVGShape, ObservableObject { +public class SVGPolyline: SVGShape { #if os(WASI) || os(Linux) public var points: [CGPoint] #else @@ -60,6 +60,7 @@ public class SVGPolyline: SVGShape, ObservableObject { } #if canImport(SwiftUI) +extension SVGPolyline: ObservableObject {} struct SVGPolylineView: View { @ObservedObject var model = SVGPolyline() diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift index 2c5cc5a..0179896 100644 --- a/Source/Model/Shapes/SVGRect.swift +++ b/Source/Model/Shapes/SVGRect.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine #endif -public class SVGRect: SVGShape, ObservableObject { +public class SVGRect: SVGShape { #if os(WASI) || os(Linux) public var x: CGFloat @@ -56,6 +56,7 @@ public class SVGRect: SVGShape, ObservableObject { } #if canImport(SwiftUI) +extension SVGRect: ObservableObject {} struct SVGRectView: View { @ObservedObject var model: SVGRect From 053bb046c070e95e3a4b76210406e53c580cd4dd Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 12:50:57 +0700 Subject: [PATCH 09/26] Improve cross-platform support by conditionally compiling SwiftUI code - Wrap SwiftUI-dependent methods in SVGNode, SVGColor, SVGFont and SVGStroke with #if canImport(SwiftUI) - Move gesture handling code inside conditional compilation blocks - Ensure SVG components work on platforms where SwiftUI is unavailable - Continues the cross-platform compatibility work for Linux and WASI platforms --- Source/Model/Nodes/SVGNode.swift | 6 +++++- Source/Model/Primitives/SVGColor.swift | 4 ++-- Source/Model/Primitives/SVGFont.swift | 4 +++- Source/Model/Primitives/SVGStroke.swift | 2 ++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift index 3bb810d..214b486 100644 --- a/Source/Model/Nodes/SVGNode.swift +++ b/Source/Model/Nodes/SVGNode.swift @@ -23,7 +23,6 @@ public class SVGNode: SerializableElement { @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 @@ -47,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() @@ -61,6 +62,7 @@ public class SVGNode: SerializableElement { public func removeAllGestures() { gestures.removeAll() } + #endif func serialize(_ serializer: Serializer) { if !transform.isIdentity { @@ -77,6 +79,7 @@ public class SVGNode: SerializableElement { } +#if canImport(SwiftUI) extension SVGNode { @ViewBuilder public func toSwiftUI() -> some View { @@ -116,3 +119,4 @@ extension SVGNode { } } } +#endif diff --git a/Source/Model/Primitives/SVGColor.swift b/Source/Model/Primitives/SVGColor.swift index 34eb991..bebfd8b 100644 --- a/Source/Model/Primitives/SVGColor.swift +++ b/Source/Model/Primitives/SVGColor.swift @@ -62,12 +62,12 @@ 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) } - #if canImport(SwiftUI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { view.foregroundColor(toSwiftUI()) } diff --git a/Source/Model/Primitives/SVGFont.swift b/Source/Model/Primitives/SVGFont.swift index 693488f..888c6de 100644 --- a/Source/Model/Primitives/SVGFont.swift +++ b/Source/Model/Primitives/SVGFont.swift @@ -15,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/SVGStroke.swift b/Source/Model/Primitives/SVGStroke.swift index dd88a62..e813f11 100644 --- a/Source/Model/Primitives/SVGStroke.swift +++ b/Source/Model/Primitives/SVGStroke.swift @@ -24,6 +24,7 @@ public class SVGStroke: SerializableBlock { self.offset = offset } + #if canImport(SwiftUI) public func toSwiftUI() -> StrokeStyle { StrokeStyle(lineWidth: width, lineCap: cap, @@ -32,6 +33,7 @@ public class SVGStroke: SerializableBlock { dash: dashes, dashPhase: offset) } + #endif func serialize(_ serializer: Serializer) { fill.serialize(key: "fill", serializer: serializer) From c179754ab9161d3445958649620dbf4c73c3633b Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 13:05:21 +0700 Subject: [PATCH 10/26] Refactor SVGPathReader platform-specific imports and type aliases - Organize imports into clearer platform-specific sections - Add explicit UIKit import for iOS/tvOS/watchOS platforms - Remove unnecessary SwiftUI import - Move Foundation import to Linux/WASI specific section --- Source/Parser/SVG/SVGPathReader.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Source/Parser/SVG/SVGPathReader.swift b/Source/Parser/SVG/SVGPathReader.swift index 5b763dd..5819eb2 100644 --- a/Source/Parser/SVG/SVGPathReader.swift +++ b/Source/Parser/SVG/SVGPathReader.swift @@ -5,17 +5,14 @@ // Created by Alisa Mylnikova on 23/07/2020. // -#if os(WASI) || os(Linux) -import Foundation -#else -import SwiftUI -#endif - #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 { From cbf5c702f1b226bd72550cb0ee272ca00a030596 Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 13:05:59 +0700 Subject: [PATCH 11/26] Refactor DOMParser platform-specific imports --- Source/Parser/XML/DOMParser.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Parser/XML/DOMParser.swift b/Source/Parser/XML/DOMParser.swift index e5f3205..520397f 100644 --- a/Source/Parser/XML/DOMParser.swift +++ b/Source/Parser/XML/DOMParser.swift @@ -6,9 +6,9 @@ // #if os(WASI) || os(Linux) -import Foundation +import FoundationXML #else -import SwiftUI +import Foundation #endif public struct DOMParser { From a196fee4d854be12b02644b1f633427b90df13d5 Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 13:13:47 +0700 Subject: [PATCH 12/26] Refactor text anchor handling for improved cross-platform support - Replace SwiftUI's HorizontalAlignment with custom SVGText.Anchor enum - Add conversion between Anchor and HorizontalAlignment for SwiftUI compatibility - Update serialization to work with the new enum - Continue ongoing efforts to reduce platform-specific dependencies --- Source/Model/Nodes/SVGText.swift | 41 ++++++++++++++++--- .../Parser/SVG/Elements/SVGTextParser.swift | 2 +- Source/Serialization/Serializations.swift | 2 +- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift index 7a70641..098cb99 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -6,22 +6,38 @@ import Combine #endif 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: HorizontalAlignment = .leading + 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 + @Published public var textAnchor: Anchor = .leading #endif - 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) { + 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 @@ -46,6 +62,19 @@ public class SVGText: SVGNode { #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 @@ -70,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) diff --git a/Source/Parser/SVG/Elements/SVGTextParser.swift b/Source/Parser/SVG/Elements/SVGTextParser.swift index e3924ef..5cbbe6f 100644 --- a/Source/Parser/SVG/Elements/SVGTextParser.swift +++ b/Source/Parser/SVG/Elements/SVGTextParser.swift @@ -30,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/Serialization/Serializations.swift b/Source/Serialization/Serializations.swift index 537d12b..655bc8b 100644 --- a/Source/Serialization/Serializations.swift +++ b/Source/Serialization/Serializations.swift @@ -176,7 +176,7 @@ extension CGPathFillRule: SerializableOption { } -extension HorizontalAlignment: SerializableOption { +extension SVGText.Anchor: SerializableOption { func isDefault() -> Bool { return self == .leading From 6601c424a00274327a06aa71b6eae0b05a6f4e4a Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 13:14:46 +0700 Subject: [PATCH 13/26] Refactor SVGPaint SwiftUI extension with conditional compilation --- Source/Model/Primitives/SVGPaint.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Model/Primitives/SVGPaint.swift b/Source/Model/Primitives/SVGPaint.swift index 5441d0b..1907613 100644 --- a/Source/Model/Primitives/SVGPaint.swift +++ b/Source/Model/Primitives/SVGPaint.swift @@ -23,6 +23,7 @@ public class SVGPaint { } +#if canImport(SwiftUI) extension View { @ViewBuilder @@ -44,3 +45,4 @@ extension View { } } +#endif From 0a6a5590eabe673217efd49852c428bd37a63f88 Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 13:15:16 +0700 Subject: [PATCH 14/26] Refactor SVGPolygon and SVGPolyline to use Swift's typed constants --- Source/Model/Shapes/SVGPolygon.swift | 8 ++++---- Source/Model/Shapes/SVGPolyline.swift | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/Model/Shapes/SVGPolygon.swift b/Source/Model/Shapes/SVGPolygon.swift index cc205dc..f323ddd 100644 --- a/Source/Model/Shapes/SVGPolygon.swift +++ b/Source/Model/Shapes/SVGPolygon.swift @@ -26,10 +26,10 @@ public class SVGPolygon: SVGShape { 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) diff --git a/Source/Model/Shapes/SVGPolyline.swift b/Source/Model/Shapes/SVGPolyline.swift index 0a9deeb..eb3e107 100644 --- a/Source/Model/Shapes/SVGPolyline.swift +++ b/Source/Model/Shapes/SVGPolyline.swift @@ -25,10 +25,10 @@ public class SVGPolyline: SVGShape { 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) From 31cea92ce64814e3370c71e6634ac97d88c92fd3 Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 13:23:44 +0700 Subject: [PATCH 15/26] Simplify CGAffineTransform serialization --- Source/Serialization/Serializations.swift | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Source/Serialization/Serializations.swift b/Source/Serialization/Serializations.swift index 655bc8b..ca5e652 100644 --- a/Source/Serialization/Serializations.swift +++ b/Source/Serialization/Serializations.swift @@ -47,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) } } From c672e04a985df5118171306bcf7df00950296e21 Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 20 May 2025 13:25:28 +0700 Subject: [PATCH 16/26] Fix import statements in DOMParser for Linux/WASI --- Source/Parser/XML/DOMParser.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Parser/XML/DOMParser.swift b/Source/Parser/XML/DOMParser.swift index 520397f..a88b710 100644 --- a/Source/Parser/XML/DOMParser.swift +++ b/Source/Parser/XML/DOMParser.swift @@ -6,6 +6,7 @@ // #if os(WASI) || os(Linux) +import Foundation import FoundationXML #else import Foundation From e24b4ed492ae6a4529b86fd316d083cef80d9c53 Mon Sep 17 00:00:00 2001 From: khoi Date: Thu, 22 May 2025 09:44:15 +0700 Subject: [PATCH 17/26] Reorganize SwiftUI conditional compilation in SVGGradient --- Source/Model/Primitives/SVGGradient.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Model/Primitives/SVGGradient.swift b/Source/Model/Primitives/SVGGradient.swift index c4033e2..184557b 100644 --- a/Source/Model/Primitives/SVGGradient.swift +++ b/Source/Model/Primitives/SVGGradient.swift @@ -58,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 @@ -69,7 +70,6 @@ public class SVGLinearGradient: SVGGradient { ) } - #if canImport(SwiftUI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { let frame = model?.frame() ?? CGRect() let bounds = model?.bounds() ?? CGRect() @@ -113,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) @@ -122,7 +123,6 @@ public class SVGRadialGradient: SVGGradient { return RadialGradient(gradient: Gradient(stops: suiStops), center: UnitPoint(x: ncx, y: ncy), startRadius: 0, endRadius: userSpace ? r : r * s) } - #if canImport(SwiftUI) func apply(view: S, model: SVGShape? = nil) -> some View where S : View { let frame = model?.frame() ?? CGRect() let bounds = model?.bounds() ?? CGRect() From 42bcbeee78016c4a7b3e21b58020516af65211fe Mon Sep 17 00:00:00 2001 From: khoi Date: Thu, 22 May 2025 09:50:24 +0700 Subject: [PATCH 18/26] Fix SVGPathReader bezierPath initialization for Linux/WASI - Add conditional compilation directives to make bezierPath variables mutable on Linux/WASI - Follows pattern of other platform-specific fixes for cross-platform compatibility --- Source/Parser/SVG/SVGPathReader.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Source/Parser/SVG/SVGPathReader.swift b/Source/Parser/SVG/SVGPathReader.swift index 5819eb2..f5608f5 100644 --- a/Source/Parser/SVG/SVGPathReader.swift +++ b/Source/Parser/SVG/SVGPathReader.swift @@ -336,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? @@ -561,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)) From 00f6c68b603e31de3d68e0f5af37a2832c66ed2b Mon Sep 17 00:00:00 2001 From: khoi Date: Thu, 22 May 2025 16:01:09 +0700 Subject: [PATCH 19/26] Fix Foundation imports and remove redundant typealiases - Remove unnecessary CGFloat, CGSize, and CGPoint typealiases - Add missing Foundation imports to SVG parser files - Continue improving cross-platform compatibility --- Source/CoreGraphicsPolyfill.swift | 4 ---- Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift | 1 + Source/Parser/SVG/Attributes/SVGLengthAttribute.swift | 2 ++ Source/Parser/SVG/SVGContext.swift | 2 ++ Source/Parser/SVG/SVGParserExtensions.swift | 1 + 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/CoreGraphicsPolyfill.swift b/Source/CoreGraphicsPolyfill.swift index 7af0340..16e607c 100644 --- a/Source/CoreGraphicsPolyfill.swift +++ b/Source/CoreGraphicsPolyfill.swift @@ -7,10 +7,6 @@ import Foundation -public typealias CGFloat = Foundation.CGFloat -public typealias CGSize = Foundation.CGSize -public typealias CGPoint = Foundation.CGPoint - #if os(WASI) || os(Linux) import Glibc public func sqrt(_ x: CGFloat) -> CGFloat { return x.squareRoot() } diff --git a/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift b/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift index 92859dd..d1399c4 100644 --- a/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGFontSizeAttribute.swift @@ -5,6 +5,7 @@ // Created by Yuri Strot on 29.05.2022. // +import Foundation class SVGFontSizeAttribute: SVGDefaultAttribute { diff --git a/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift b/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift index 0aa5561..2375752 100644 --- a/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift +++ b/Source/Parser/SVG/Attributes/SVGLengthAttribute.swift @@ -6,6 +6,8 @@ // +import Foundation + class SVGLengthAttribute: SVGDefaultAttribute { private let attrName: String diff --git a/Source/Parser/SVG/SVGContext.swift b/Source/Parser/SVG/SVGContext.swift index a2923d9..2bd58ea 100644 --- a/Source/Parser/SVG/SVGContext.swift +++ b/Source/Parser/SVG/SVGContext.swift @@ -5,6 +5,8 @@ // Created by Yuri Strot on 26.05.2022. // +import Foundation + protocol SVGContext { var logger: SVGLogger { get } diff --git a/Source/Parser/SVG/SVGParserExtensions.swift b/Source/Parser/SVG/SVGParserExtensions.swift index a9c2e18..ab8d953 100644 --- a/Source/Parser/SVG/SVGParserExtensions.swift +++ b/Source/Parser/SVG/SVGParserExtensions.swift @@ -5,6 +5,7 @@ // Created by Yuri Strot on 25.05.2022. // +import Foundation extension CGFloat { var degreesToRadians: CGFloat { From 642372c9b99c7c41b5b7d27f225e05aae67b54cd Mon Sep 17 00:00:00 2001 From: khoi Date: Thu, 22 May 2025 16:02:15 +0700 Subject: [PATCH 20/26] Remove redundant math functions from Linux/WASI polyfill These math function declarations (sqrt, copysign, acos, cos, sin) are no longer needed in the Linux/WASI environment as they're likely available from standard libraries or other imports. --- Source/CoreGraphicsPolyfill.swift | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Source/CoreGraphicsPolyfill.swift b/Source/CoreGraphicsPolyfill.swift index 16e607c..3182ddd 100644 --- a/Source/CoreGraphicsPolyfill.swift +++ b/Source/CoreGraphicsPolyfill.swift @@ -8,24 +8,6 @@ import Foundation #if os(WASI) || os(Linux) - import Glibc - public func sqrt(_ x: CGFloat) -> CGFloat { return x.squareRoot() } - public func copysign(_ x: CGFloat, _ y: CGFloat) -> CGFloat { - let magnitude = x >= 0 ? x : -x - return y >= 0 ? magnitude : -magnitude - } - public func acos(_ x: CGFloat) -> CGFloat { - return Glibc.acos(x) - } - - public func cos(_ x: CGFloat) -> CGFloat { - return Glibc.cos(x) - } - - public func sin(_ x: CGFloat) -> CGFloat { - return Glibc.sin(x) - } - private let KAPPA: CGFloat = 0.5522847498 // 4 *(sqrt(2) -1)/3 public struct CGAffineTransform: Equatable { From 5b1af60a84efa52c7cc2b031b180d4a9600a8289 Mon Sep 17 00:00:00 2001 From: khoi Date: Thu, 22 May 2025 17:07:47 +0700 Subject: [PATCH 21/26] Convert CGPath and MBezierPath to classes in CoreGraphicsPolyfill - Change implementation from struct to class to better match CoreGraphics behavior - Remove mutating keywords from methods as they're no longer needed - Update parameter types in addArcTo method to match class-based approach --- Source/CoreGraphicsPolyfill.swift | 40 +++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Source/CoreGraphicsPolyfill.swift b/Source/CoreGraphicsPolyfill.swift index 3182ddd..aa7de7e 100644 --- a/Source/CoreGraphicsPolyfill.swift +++ b/Source/CoreGraphicsPolyfill.swift @@ -43,7 +43,7 @@ import Foundation } /// A graphics path is a mathematical description of a series of shapes or lines. - public struct CGPath { + public class CGPath { public typealias Element = PathElement @@ -116,7 +116,7 @@ import Foundation return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) } - public mutating func addRect(_ rect: CGRect) { + public func addRect(_ rect: CGRect) { let newElements: [Element] = [ .moveToPoint(CGPoint(x: rect.minX, y: rect.minY)), @@ -129,7 +129,7 @@ import Foundation elements.append(contentsOf: newElements) } - public mutating func addEllipse(in rect: CGRect) { + public func addEllipse(in rect: CGRect) { var p = CGPoint() var p1 = CGPoint() @@ -165,27 +165,27 @@ import Foundation elements.append(.addCurveToPoint(p1, p2, p)) } - public mutating func move(to point: CGPoint) { + public func move(to point: CGPoint) { elements.append(.moveToPoint(point)) } - public mutating func addLine(to point: CGPoint) { + public func addLine(to point: CGPoint) { elements.append(.addLineToPoint(point)) } - public mutating func addCurve(to endPoint: CGPoint, control1: CGPoint, control2: CGPoint) { + public func addCurve(to endPoint: CGPoint, control1: CGPoint, control2: CGPoint) { elements.append(.addCurveToPoint(control1, control2, endPoint)) } - public mutating func addQuadCurve(to endPoint: CGPoint, control: CGPoint) { + public func addQuadCurve(to endPoint: CGPoint, control: CGPoint) { elements.append(.addQuadCurveToPoint(control, endPoint)) } - public mutating func closeSubpath() { + public func closeSubpath() { elements.append(.closeSubpath) } @@ -294,7 +294,7 @@ import Foundation } } - public struct MBezierPath { + public class MBezierPath { public var cgPath: CGPath public init() { @@ -317,46 +317,46 @@ import Foundation ) { self.cgPath = CGPath() MBezierPath.addArcTo( - path: &self.cgPath, center: arcCenter, radius: radius, startAngle: startAngle, + path: self.cgPath, center: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) } - public mutating func move(to point: CGPoint) { + public func move(to point: CGPoint) { cgPath.move(to: point) } - public mutating func addLine(to point: CGPoint) { + public func addLine(to point: CGPoint) { cgPath.addLine(to: point) } - public mutating func addCurve( + public func addCurve( to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint ) { cgPath.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) } - public mutating func addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint) { + public func addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint) { cgPath.addQuadCurve(to: endPoint, control: controlPoint) } - public mutating func addArc( + 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, + path: self.cgPath, center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) } - public mutating func append(_ path: MBezierPath) { + public func append(_ path: MBezierPath) { cgPath.elements.append(contentsOf: path.cgPath.elements) } - public mutating func close() { + public func close() { cgPath.closeSubpath() } - public mutating func apply(_ transform: CGAffineTransform) { + public func apply(_ transform: CGAffineTransform) { var newElements: [CGPath.Element] = [] for element in cgPath.elements { switch element { @@ -389,7 +389,7 @@ import Foundation } static func addArcTo( - path: inout CGPath, center: CGPoint, radius: CGFloat, startAngle: CGFloat, + path: CGPath, center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool ) { var deltaAngle: CGFloat From 85aca572fd6e5e144d0fca7c6fe18f47b4ba2c45 Mon Sep 17 00:00:00 2001 From: khoi Date: Fri, 23 May 2025 09:49:08 +0700 Subject: [PATCH 22/26] Add comprehensive test suite for CoreGraphicsPolyfill - Tests CGAffineTransform operations (identity, translation, scaling, rotation) - Verifies CGPath functionality (basic operations, bounding box calculation) - Covers MBezierPath operations (initialization, path building, transformations) - Includes tests for PathElement enum and extensions - Handles edge cases and platform-specific behavior --- .../PolyfillTests.swift | 618 ++++++++++++++++++ 1 file changed, 618 insertions(+) create mode 100644 Tests/CoreGraphicsPolyfillTests/PolyfillTests.swift 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 +} From c7e476c307c7e04d1a85de33a797dc60c9a33794 Mon Sep 17 00:00:00 2001 From: khoi Date: Fri, 23 May 2025 09:55:45 +0700 Subject: [PATCH 23/26] Add CI workflow and configure test target - Set up GitHub Actions workflow to run tests on Linux - Add CoreGraphicsPolyfillTests test target to Package.swift - Remove unnecessary exclude entry for Info.plist --- .github/workflows/test.yml | 30 ++++++++++++++++++++++++++++++ Package.swift | 7 +++++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..840cac3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +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: Cache Swift PM + uses: actions/cache@v4 + with: + path: .build + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Run tests + run: swift test --enable-test-discovery + \ No newline at end of file diff --git a/Package.swift b/Package.swift index a4bc055..6ddc86c 100644 --- a/Package.swift +++ b/Package.swift @@ -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] From 8e8f553a40d6b9f97e0fe7eb72140ba334b14919 Mon Sep 17 00:00:00 2001 From: khoi Date: Fri, 23 May 2025 10:04:34 +0700 Subject: [PATCH 24/26] Add macOS CI job for native CoreGraphics testing - Test on macOS-latest to validate against native CoreGraphics implementation - Mirror existing Linux test job structure with caching and test discovery --- .github/workflows/test.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 840cac3..25ab15d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,4 +27,23 @@ jobs: - name: Run tests run: swift test --enable-test-discovery + + test-macos: + name: Test on macOS (Native CoreGraphics) + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Swift PM + uses: actions/cache@v4 + with: + path: .build + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Run tests + run: swift test --enable-test-discovery \ No newline at end of file From 2a4afcc983fce7512fa4e06b8eb3509530c78e95 Mon Sep 17 00:00:00 2001 From: khoi Date: Fri, 23 May 2025 10:05:08 +0700 Subject: [PATCH 25/26] Simplify macOS test job name in CI workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 25ab15d..ec9d7dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: run: swift test --enable-test-discovery test-macos: - name: Test on macOS (Native CoreGraphics) + name: Test on macOS runs-on: macos-latest steps: From 554086dbd2b1e3b36c5cb52cf05ff263ff347ec4 Mon Sep 17 00:00:00 2001 From: khoi Date: Fri, 23 May 2025 10:06:59 +0700 Subject: [PATCH 26/26] Remove Swift PM caching from CI workflow --- .github/workflows/test.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec9d7dc..c4674c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,14 +16,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Cache Swift PM - uses: actions/cache@v4 - with: - path: .build - key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm- - name: Run tests run: swift test --enable-test-discovery @@ -35,14 +27,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Cache Swift PM - uses: actions/cache@v4 - with: - path: .build - key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm- - name: Run tests run: swift test --enable-test-discovery