From a96991a7103eddd274372e265fdbca5c9c63ad07 Mon Sep 17 00:00:00 2001 From: Garush Batikyan Date: Mon, 19 Oct 2020 16:46:56 +0400 Subject: [PATCH] Add support for elliptical arc command --- SwiftSVG.xcodeproj/project.pbxproj | 4 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++ .../CenterParametrizedEllipticalArc.swift | 41 ++++++ SwiftSVG/SVG/Iterators/PathCommand.swift | 124 +++++++++++++++++- SwiftSVG/SVG/Iterators/PathDLexer.swift | 2 + 5 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 SwiftSVG.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SwiftSVG/SVG/Helpers/CenterParametrizedEllipticalArc.swift diff --git a/SwiftSVG.xcodeproj/project.pbxproj b/SwiftSVG.xcodeproj/project.pbxproj index ab5553a..6a124d4 100644 --- a/SwiftSVG.xcodeproj/project.pbxproj +++ b/SwiftSVG.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ E8235A6222F2686C00E8F1B3 /* Dictionary+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8235A6022F2614000E8F1B3 /* Dictionary+JSON.swift */; }; E8235A6322F2687F00E8F1B3 /* cssColorNames.json in Resources */ = {isa = PBXBuildFile; fileRef = E8235A5E22F260E000E8F1B3 /* cssColorNames.json */; }; E8AFE31C22EBD6F600BD1672 /* Print.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8AFE31B22EBD6F600BD1672 /* Print.swift */; }; + FAB19BF0253E0632001A2C01 /* CenterParametrizedEllipticalArc.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB19BEF253E0632001A2C01 /* CenterParametrizedEllipticalArc.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -237,6 +238,7 @@ E8235A5E22F260E000E8F1B3 /* cssColorNames.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = cssColorNames.json; sourceTree = ""; }; E8235A6022F2614000E8F1B3 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = ""; }; E8AFE31B22EBD6F600BD1672 /* Print.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Print.swift; sourceTree = ""; }; + FAB19BEF253E0632001A2C01 /* CenterParametrizedEllipticalArc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenterParametrizedEllipticalArc.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -347,6 +349,7 @@ 9E245B6A1AAE383200472F5A /* UIColor+Extensions.swift */, 66F9844A20EF511F00A2CBEF /* Unown.swift */, E8235A5E22F260E000E8F1B3 /* cssColorNames.json */, + FAB19BEF253E0632001A2C01 /* CenterParametrizedEllipticalArc.swift */, ); name = Helpers; path = SVG/Helpers; @@ -753,6 +756,7 @@ 175093C11E6C669F00EF1853 /* SVGParserSupportedElements.swift in Sources */, 3807CCF01F29940A00E78314 /* Fillable.swift in Sources */, 387090361F0C134400F72660 /* ParsesAsynchronously.swift in Sources */, + FAB19BF0253E0632001A2C01 /* CenterParametrizedEllipticalArc.swift in Sources */, 381E99B11EF8AFE500839918 /* PathCommand.swift in Sources */, 3807CCF21F29940A00E78314 /* Stylable.swift in Sources */, 380FECF81F000AA000EDB255 /* FloatingPoint+DegreesRadians.swift in Sources */, diff --git a/SwiftSVG.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SwiftSVG.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/SwiftSVG.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SwiftSVG/SVG/Helpers/CenterParametrizedEllipticalArc.swift b/SwiftSVG/SVG/Helpers/CenterParametrizedEllipticalArc.swift new file mode 100644 index 0000000..6ae04a8 --- /dev/null +++ b/SwiftSVG/SVG/Helpers/CenterParametrizedEllipticalArc.swift @@ -0,0 +1,41 @@ +// +// CenterParametrizedEllipticalArc.swift +// SwiftSVG +// +// Created by Garush Batikyan on 10/19/20. +// Copyright © 2020 Strauss LLC. All rights reserved. +// http://www.github.com/mchoe +// http://www.straussmade.com/ +// http://www.twitter.com/_mchoe +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import CoreGraphics + +struct CenterParametrizedEllipticalArc { + var cx: CGFloat + var cy: CGFloat + var rx: CGFloat + var ry: CGFloat + var theta: CGFloat + var dTheta: CGFloat + var psi: CGFloat + var fs: CGFloat +} + diff --git a/SwiftSVG/SVG/Iterators/PathCommand.swift b/SwiftSVG/SVG/Iterators/PathCommand.swift index 9e918b5..072686b 100644 --- a/SwiftSVG/SVG/Iterators/PathCommand.swift +++ b/SwiftSVG/SVG/Iterators/PathCommand.swift @@ -476,7 +476,6 @@ internal struct SmoothQuadraticCurveTo: PathCommand { /** The `PathCommand` that corresponds to the SVG `A` or `a` command - - TODO: Still needs an implementation */ internal struct EllipticalArc: PathCommand { @@ -484,7 +483,7 @@ internal struct EllipticalArc: PathCommand { internal var coordinateBuffer = [Double]() /// :nodoc: - internal let numberOfRequiredParameters = 2 + internal let numberOfRequiredParameters = 7 /// :nodoc: internal let pathType: PathType @@ -494,8 +493,127 @@ internal struct EllipticalArc: PathCommand { self.pathType = pathType } + /// :nodoc: + internal func convertEndpointToCenterParameterization( + x1: CGFloat, + y1: CGFloat, + x2: CGFloat, + y2: CGFloat, + fa: CGFloat, + fs: CGFloat, + rx: CGFloat, + ry: CGFloat, + psiDeg: CGFloat) -> CenterParametrizedEllipticalArc + { + var rx = rx + var ry = ry + let psi = psiDeg * (CGFloat.pi / 180.0) + let xp = (cos(psi) * ((x1 - x2) / 2.0)) + (sin(psi) * ((y1 - y2) / 2.0)) + let yp = (-1 * sin(psi) * ((x1 - x2) / 2.0)) + (cos(psi) * ((y1 - y2) / 2.0)) + let lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry) + + if (lambda > 1) { + rx = rx * sqrt(lambda) + ry = ry * sqrt(lambda) + } + + var f = sqrt( + (rx * rx * (ry * ry) - rx * rx * (yp * yp) - ry * ry * (xp * xp)) / + (rx * rx * (yp * yp) + ry * ry * (xp * xp)) + ) + + if (fa == fs) { + f *= -1 + } + + if (f.isNaN) { + f = 0 + } + + let cxp = (f * rx * yp) / ry + let cyp = (f * -ry * xp) / rx + + let cx = (x1 + x2) / 2.0 + cos(psi) * cxp - sin(psi) * cyp + let cy = (y1 + y2) / 2.0 + sin(psi) * cxp + cos(psi) * cyp + + let vMag:(Array) -> CGFloat = { v -> CGFloat in + return sqrt(v[0] * v[0] + v[1] * v[1]) + } + let vRatio: (Array,Array) -> CGFloat = { u,v -> CGFloat in + return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v)); + }; + let vAngle: (Array,Array) -> CGFloat = { u,v -> CGFloat in + return ((u[0] * v[1] < u[1] * v[0]) ? -1 : 1) * acos(vRatio(u, v)) + }; + let theta = vAngle([1, 0], [(xp - cxp) / rx, (yp - cyp) / ry]) + let u = [(xp - cxp) / rx, (yp - cyp) / ry] + let v = [(-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry] + var dTheta = vAngle(u, v) + + if (vRatio(u, v) <= -1) { + dTheta = CGFloat.pi + } + if (vRatio(u, v) >= 1) { + dTheta = 0 + } + if (fs == 0 && dTheta > 0) { + dTheta = dTheta - 2 * CGFloat.pi + } + if (fs == 1 && dTheta < 0) { + dTheta = dTheta + 2 * CGFloat.pi + } + return CenterParametrizedEllipticalArc(cx: cx, + cy: cy, + rx: rx, + ry: ry, + theta: theta, + dTheta: dTheta, + psi: psi, + fs: fs) + } + + /// :nodoc: + internal func drawArc(parameters: CenterParametrizedEllipticalArc, to path: UIBezierPath) + { + let r = parameters.rx > parameters.ry ? parameters.rx : parameters.ry + let scaleX = parameters.rx > parameters.ry ? 1 : parameters.rx / parameters.ry + let scaleY = parameters.rx > parameters.ry ? parameters.ry / parameters.rx : 1 + path.apply(CGAffineTransform(translationX: -parameters.cx, y: -parameters.cy)) + path.apply(CGAffineTransform(rotationAngle: -parameters.psi)) + path.apply(CGAffineTransform(scaleX: 1/scaleX, y: 1/scaleY)) + let clockwise = (1 - parameters.fs) == 0 + path.addArc(withCenter: .zero, + radius: r, + startAngle: parameters.theta, + endAngle: parameters.theta + parameters.dTheta, + clockwise: clockwise) + path.apply(CGAffineTransform(scaleX: scaleX, y: scaleY)) + path.apply(CGAffineTransform(rotationAngle: parameters.psi)) + path.apply(CGAffineTransform(translationX: parameters.cx, y: parameters.cy)) + } + /// :nodoc: internal func execute(on path: UIBezierPath, previousCommand: PreviousCommand? = nil) { - assert(false, "Needs Implementation") + + let rx = CGFloat(self.coordinateBuffer[0]) + let ry = CGFloat(self.coordinateBuffer[1]) + let psi = CGFloat(self.coordinateBuffer[2]) + let fa = CGFloat(self.coordinateBuffer[3]) + let fs = CGFloat(self.coordinateBuffer[4]) + let x1 = CGFloat(path.currentPoint.x) + let y1 = CGFloat(path.currentPoint.y) + let ap = CGPoint(x: coordinateBuffer[5], y: coordinateBuffer[6]) + let rp = self.pointForPathType(ap, relativeTo: path.currentPoint) + let a = self.convertEndpointToCenterParameterization(x1: x1, + y1: y1, + x2: rp.x, + y2: rp.y, + fa: fa, + fs: fs, + rx: rx, + ry: ry, + psiDeg: psi) + + drawArc(parameters: a, to: path) } } diff --git a/SwiftSVG/SVG/Iterators/PathDLexer.swift b/SwiftSVG/SVG/Iterators/PathDLexer.swift index d28e070..37c460d 100644 --- a/SwiftSVG/SVG/Iterators/PathDLexer.swift +++ b/SwiftSVG/SVG/Iterators/PathDLexer.swift @@ -92,6 +92,8 @@ internal struct PathDConstants { DCharacter.q.rawValue: QuadraticCurveTo(pathType: .relative), DCharacter.T.rawValue: SmoothQuadraticCurveTo(pathType: .absolute), DCharacter.t.rawValue: SmoothQuadraticCurveTo(pathType: .relative), + DCharacter.A.rawValue: EllipticalArc(pathType: .absolute), + DCharacter.a.rawValue: EllipticalArc(pathType: .relative), ] }