Merge pull request #210 from exyte/feature/parse-svg-clip-path
Refactor locus to path implementation
@ -270,6 +270,14 @@
|
||||
C43B06661F99EE7300787A35 /* cubicAbsolute.svg in Resources */ = {isa = PBXBuildFile; fileRef = C43B06641F99EE7200787A35 /* cubicAbsolute.svg */; };
|
||||
C43B06671F99EE7300787A35 /* cubicRelative.svg in Resources */ = {isa = PBXBuildFile; fileRef = C43B06651F99EE7300787A35 /* cubicRelative.svg */; };
|
||||
C43B06691F99FC2300787A35 /* pathbounds4.svg in Resources */ = {isa = PBXBuildFile; fileRef = C43B06681F99FC2300787A35 /* pathbounds4.svg */; };
|
||||
C43B06511F9866E400787A35 /* Locus+ToPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43B06501F9866E400787A35 /* Locus+ToPath.swift */; };
|
||||
C43B06531F989D9300787A35 /* transform.reference in Resources */ = {isa = PBXBuildFile; fileRef = C43B06521F989D9300787A35 /* transform.reference */; };
|
||||
C43B06551F98A53600787A35 /* group.reference in Resources */ = {isa = PBXBuildFile; fileRef = C43B06541F98A53600787A35 /* group.reference */; };
|
||||
C43B06571F98A7B700787A35 /* arcsgroup.reference in Resources */ = {isa = PBXBuildFile; fileRef = C43B06561F98A7B700787A35 /* arcsgroup.reference */; };
|
||||
C43B06591F98A84200787A35 /* style.reference in Resources */ = {isa = PBXBuildFile; fileRef = C43B06581F98A84200787A35 /* style.reference */; };
|
||||
C43B065B1F98A9E000787A35 /* clipManual.reference in Resources */ = {isa = PBXBuildFile; fileRef = C43B065A1F98A9E000787A35 /* clipManual.reference */; };
|
||||
C43B065F1F98AAA500787A35 /* clip.reference in Resources */ = {isa = PBXBuildFile; fileRef = C43B065E1F98AAA500787A35 /* clip.reference */; };
|
||||
C43B06611F98ACFC00787A35 /* textBasicTransform.reference in Resources */ = {isa = PBXBuildFile; fileRef = C43B06601F98ACFC00787A35 /* textBasicTransform.reference */; };
|
||||
C46E83551F94B20E00208037 /* transform.svg in Resources */ = {isa = PBXBuildFile; fileRef = C46E83541F94B20E00208037 /* transform.svg */; };
|
||||
C4820B181F458D0E008CE0FF /* SVGSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4820B171F458D0E008CE0FF /* SVGSerializer.swift */; };
|
||||
C4820B1A1F458D64008CE0FF /* MacawSVGTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4820B191F458D64008CE0FF /* MacawSVGTests.swift */; };
|
||||
@ -459,6 +467,14 @@
|
||||
C43B06641F99EE7200787A35 /* cubicAbsolute.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = cubicAbsolute.svg; path = Bounds/cubicAbsolute.svg; sourceTree = "<group>"; };
|
||||
C43B06651F99EE7300787A35 /* cubicRelative.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = cubicRelative.svg; path = Bounds/cubicRelative.svg; sourceTree = "<group>"; };
|
||||
C43B06681F99FC2300787A35 /* pathbounds4.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = pathbounds4.svg; path = Bounds/pathbounds4.svg; sourceTree = "<group>"; };
|
||||
C43B06501F9866E400787A35 /* Locus+ToPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locus+ToPath.swift"; sourceTree = "<group>"; };
|
||||
C43B06521F989D9300787A35 /* transform.reference */ = {isa = PBXFileReference; lastKnownFileType = text; path = transform.reference; sourceTree = "<group>"; };
|
||||
C43B06541F98A53600787A35 /* group.reference */ = {isa = PBXFileReference; lastKnownFileType = text; path = group.reference; sourceTree = "<group>"; };
|
||||
C43B06561F98A7B700787A35 /* arcsgroup.reference */ = {isa = PBXFileReference; lastKnownFileType = text; path = arcsgroup.reference; sourceTree = "<group>"; };
|
||||
C43B06581F98A84200787A35 /* style.reference */ = {isa = PBXFileReference; lastKnownFileType = text; path = style.reference; sourceTree = "<group>"; };
|
||||
C43B065A1F98A9E000787A35 /* clipManual.reference */ = {isa = PBXFileReference; lastKnownFileType = text; path = clipManual.reference; sourceTree = "<group>"; };
|
||||
C43B065E1F98AAA500787A35 /* clip.reference */ = {isa = PBXFileReference; lastKnownFileType = text; path = clip.reference; sourceTree = "<group>"; };
|
||||
C43B06601F98ACFC00787A35 /* textBasicTransform.reference */ = {isa = PBXFileReference; lastKnownFileType = text; path = textBasicTransform.reference; sourceTree = "<group>"; };
|
||||
C46E83541F94B20E00208037 /* transform.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = transform.svg; sourceTree = "<group>"; };
|
||||
C4820B171F458D0E008CE0FF /* SVGSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGSerializer.swift; sourceTree = "<group>"; };
|
||||
C4820B191F458D64008CE0FF /* MacawSVGTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacawSVGTests.swift; sourceTree = "<group>"; };
|
||||
@ -554,6 +570,13 @@
|
||||
57CAB12B1D7832E000FD8E47 /* rect.svg */,
|
||||
57CAB12C1D7832E000FD8E47 /* roundRect.svg */,
|
||||
57CAB12D1D7832E000FD8E47 /* triangle.svg */,
|
||||
C43B06521F989D9300787A35 /* transform.reference */,
|
||||
C43B06541F98A53600787A35 /* group.reference */,
|
||||
C43B06561F98A7B700787A35 /* arcsgroup.reference */,
|
||||
C43B06581F98A84200787A35 /* style.reference */,
|
||||
C43B065A1F98A9E000787A35 /* clipManual.reference */,
|
||||
C43B065E1F98AAA500787A35 /* clip.reference */,
|
||||
C43B06601F98ACFC00787A35 /* textBasicTransform.reference */,
|
||||
);
|
||||
path = svg;
|
||||
sourceTree = "<group>";
|
||||
@ -723,6 +746,7 @@
|
||||
57E5E1281E3B393900D1CB28 /* Insets.swift */,
|
||||
57E5E1291E3B393900D1CB28 /* Line.swift */,
|
||||
57E5E12A1E3B393900D1CB28 /* Locus.swift */,
|
||||
C43B06501F9866E400787A35 /* Locus+ToPath.swift */,
|
||||
57E5E12B1E3B393900D1CB28 /* MoveTo.swift */,
|
||||
57E5E12C1E3B393900D1CB28 /* Path.swift */,
|
||||
57E5E12D1E3B393900D1CB28 /* PathBuilder.swift */,
|
||||
@ -1039,8 +1063,16 @@
|
||||
C43B064D1F9738EF00787A35 /* clip.svg in Resources */,
|
||||
57B7A4E11EE70DA5009D78D7 /* logo_base64.txt in Resources */,
|
||||
C43B06671F99EE7300787A35 /* cubicRelative.svg in Resources */,
|
||||
C43B06571F98A7B700787A35 /* arcsgroup.reference in Resources */,
|
||||
C43B06531F989D9300787A35 /* transform.reference in Resources */,
|
||||
C43B06611F98ACFC00787A35 /* textBasicTransform.reference in Resources */,
|
||||
C43B064D1F9738EF00787A35 /* clip.svg in Resources */,
|
||||
57B7A4E11EE70DA5009D78D7 /* logo_base64.txt in Resources */,
|
||||
C43B06591F98A84200787A35 /* style.reference in Resources */,
|
||||
C43B065F1F98AAA500787A35 /* clip.reference in Resources */,
|
||||
C410148E1F834D290022EE44 /* style.svg in Resources */,
|
||||
C4BD40BB1F8F58B0003034F0 /* pathbounds1.svg in Resources */,
|
||||
C43B065B1F98A9E000787A35 /* clipManual.reference in Resources */,
|
||||
C4BD40BC1F8F58B0003034F0 /* pathbounds2.svg in Resources */,
|
||||
57CAB1301D7832E000FD8E47 /* group.svg in Resources */,
|
||||
C43B06661F99EE7300787A35 /* cubicAbsolute.svg in Resources */,
|
||||
@ -1053,6 +1085,7 @@
|
||||
57CAB1321D7832E000FD8E47 /* polygon.svg in Resources */,
|
||||
57CAB12F1D7832E000FD8E47 /* ellipse.svg in Resources */,
|
||||
57CAB1341D7832E000FD8E47 /* rect.svg in Resources */,
|
||||
C43B06551F98A53600787A35 /* group.reference in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -1218,6 +1251,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
57E5E19E1E3B393900D1CB28 /* Group.swift in Sources */,
|
||||
C43B06511F9866E400787A35 /* Locus+ToPath.swift in Sources */,
|
||||
57E5E1A91E3B393900D1CB28 /* TextRenderer.swift in Sources */,
|
||||
57E5E1B01E3B393900D1CB28 /* CGFloat+Double.swift in Sources */,
|
||||
57E5E1591E3B393900D1CB28 /* CGExtensions.swift in Sources */,
|
||||
|
@ -13,29 +13,53 @@ class MacawSVGTests: XCTestCase {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testTextBasicTransform() {
|
||||
let referenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><g transform=\"translate(100,100)\" ><text dominant-baseline=\"text-before-edge\" fill=\"black\" transform=\"matrix(0.707106781186548,-0.707106781186547,0.707106781186547,0.707106781186548,0.0,0.0)\" >Point</text></g></g></svg>"
|
||||
let text1 = Text(text: "Point")
|
||||
text1.place = Transform(m11: cos(Double.pi/4.0), m12: -sin(Double.pi/4.0), m21: sin(Double.pi/4.0), m22: cos(Double.pi/4.0), dx: 0, dy: 0)
|
||||
let group1 = Group(contents: [text1])
|
||||
group1.place = Transform(dx: 100, dy: 100)
|
||||
let node = Group(contents: [group1])
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), referenceContent)
|
||||
}
|
||||
|
||||
func testClipWithParser() {
|
||||
func validate(node: Node, referenceFile: String) {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let clipReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><defs><clipPath id=\"clipPath1\"><path d=\"M 20 0m -20 0a 20 20 0 1 0 40 0a 20 20 0 1 0 -40 0M 0 10L 0 45L 100 45L 100 10z M 0 55L 0 90L 100 90L 100 55z \" /></clipPath></defs><g><circle r=\"50\" cy=\"50\" cx=\"50\" clip-path=\"url(#clipPath1)\" fill=\"black\"/></g></svg>"
|
||||
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "clip")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), clipReferenceContent)
|
||||
if let path = bundle.path(forResource: referenceFile, ofType: "reference") {
|
||||
let clipReferenceContent = try String.init(contentsOfFile: path).trimmingCharacters(in: .newlines)
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), clipReferenceContent)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func validate(_ test: String, withReference: Bool = false) {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
var ext = "svg"
|
||||
if withReference {
|
||||
ext = "reference"
|
||||
}
|
||||
do {
|
||||
if let path = bundle.path(forResource: test, ofType: ext) {
|
||||
let referenceContent = try String.init(contentsOfFile: path).trimmingCharacters(in: .newlines)
|
||||
let node = try SVGParser.parse(bundle:bundle, path: test)
|
||||
let testContent = SVGSerializer.serialize(node: node)
|
||||
.replacingOccurrences(of: "version=\"1.1\" ><g>", with: "version=\"1.1\" >")
|
||||
.replacingOccurrences(of: "defs><g>", with: "defs>")
|
||||
.replacingOccurrences(of: "</g></svg>", with: "</svg>")
|
||||
XCTAssertEqual(testContent, referenceContent)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testTextBasicTransform() {
|
||||
let text1 = Text(text: "Point")
|
||||
text1.place = Transform(m11: cos(.pi/4.0), m12: -sin(.pi/4.0), m21: sin(.pi/4.0), m22: cos(.pi/4.0), dx: 0, dy: 0)
|
||||
let group1 = Group(contents: [text1])
|
||||
group1.place = Transform(dx: 100, dy: 100)
|
||||
let node = Group(contents: [group1])
|
||||
|
||||
validate(node: node, referenceFile: "testBasicTransform")
|
||||
}
|
||||
|
||||
func testClipManual() {
|
||||
let clipReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><defs><clipPath id=\"clipPath1\"><rect height=\"90\" x=\"10\" y=\"10\" width=\"90\" /></clipPath><clipPath id=\"clipPath2\"><rect height=\"190\" x=\"110\" y=\"110\" width=\"190\" /></clipPath></defs><g><circle r=\"20\" cy=\"20\" cx=\"20\" clip-path=\"url(#clipPath1)\" fill=\"red\"/><circle r=\"20\" cy=\"120\" cx=\"120\" clip-path=\"url(#clipPath2)\" fill=\"green\"/></g></svg>"
|
||||
let path1 = Rect(x: 10, y: 10, w: 90, h: 90)
|
||||
let circle1 = Circle(cx: 20, cy: 20, r: 20).fill(with: Color.red)
|
||||
circle1.clip = path1
|
||||
@ -43,18 +67,8 @@ class MacawSVGTests: XCTestCase {
|
||||
let circle2 = Circle(cx: 120, cy: 120, r: 20).fill(with: Color.green)
|
||||
circle2.clip = path2
|
||||
let node = Group(contents:[circle1, circle2])
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), clipReferenceContent)
|
||||
}
|
||||
|
||||
func testCSSStyleReference() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let styleReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><g><circle r=\"10\" cy=\"50\" cx=\"50\" fill=\"white\" stroke=\"#231F20\" stroke-width=\"1.5\"/><circle r=\"10\" cy=\"50\" cx=\"80\" fill=\"black\"/></g></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "style")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), styleReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate(node: node, referenceFile: "clipManual")
|
||||
}
|
||||
|
||||
func testSVGClearColor() {
|
||||
@ -69,8 +83,8 @@ class MacawSVGTests: XCTestCase {
|
||||
let g2 = Group(contents:[Ellipse(cx: 20, cy: 20, rx: 20, ry:20).arc(shift: 1.570796251297, extent: 1.57079637050629).stroke(fill: Color.green)], place: Transform(dx:10, dy: 140))
|
||||
let g3 = Group(contents:[Ellipse(cx: 20, cy: 20, rx: 20, ry:20).arc(shift: 3.14159250259399, extent: 2.67794513702393).stroke(fill: Color.green)], place: Transform(dx:110, dy: 140) )
|
||||
let group = Group(contents:[g1, g2, g3])
|
||||
let arcGroupReference = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><g transform=\"translate(10,10)\" ><ellipse cy=\"20\" ry=\"20\" rx=\"20\" cx=\"20\" fill=\"none\" stroke=\"green\" stroke-width=\"1.0\"/></g><g transform=\"translate(10,140)\" ><path d=\"M20.0000015099579,39.9999999999999 A 20.0,20.0 0.0 0, 1 1.06581410364015e-14,20.0000006357301\" fill=\"none\" stroke=\"green\" stroke-width=\"1.0\"/></g><g transform=\"translate(110,140)\" ><path d=\"M2.27373675443232e-13,20.0000030199161 A 20.0,20.0 0.0 0, 1 37.888543296214,11.0557270424323\" fill=\"none\" stroke=\"green\" stroke-width=\"1.0\"/></g></g></svg>"
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: group), arcGroupReference)
|
||||
|
||||
validate(node: group, referenceFile: "arcsGroup")
|
||||
}
|
||||
|
||||
func testSVGImage() {
|
||||
@ -84,113 +98,51 @@ class MacawSVGTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testSVGTransform() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let transformReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><g transform=\"matrix(2.0,1.0,1.0,1.0,0.0,0.0)\" ><rect height=\"5\" x=\"0\" y=\"0\" width=\"150\" fill=\"blue\"/><rect height=\"50\" x=\"0\" y=\"0\" width=\"5\" fill=\"red\"/><rect height=\"50\" x=\"150\" y=\"0\" width=\"5\" fill=\"black\"/><rect height=\"5\" x=\"0\" y=\"50\" width=\"150\" fill=\"black\"/><ellipse cy=\"25\" ry=\"15\" rx=\"40\" cx=\"75\" fill=\"purple\"/></g></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "transform")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), transformReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
func testClipWithParser() {
|
||||
validate("clip", withReference: true)
|
||||
}
|
||||
|
||||
func testCSSStyleReference() {
|
||||
validate("style", withReference: true)
|
||||
}
|
||||
|
||||
func testSVGTransformSkew() {
|
||||
validate("transform", withReference: true)
|
||||
}
|
||||
|
||||
func testSVGEllipse() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let ellipseReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><ellipse cy=\"80\" ry=\"50\" rx=\"100\" cx=\"200\" fill=\"yellow\" stroke=\"purple\" stroke-width=\"2.0\"/></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "ellipse")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), ellipseReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("ellipse")
|
||||
}
|
||||
|
||||
func testSVGCircle() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let circleReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><circle r=\"40\" cy=\"50\" cx=\"50\" fill=\"red\" stroke=\"black\" stroke-width=\"3.0\"/><circle r=\"40\" cy=\"50\" cx=\"140\" fill=\"#F0F0AA\" stroke=\"black\" stroke-width=\"3.0\"/></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "circle")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), circleReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("circle")
|
||||
}
|
||||
|
||||
func testSVGGroup() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let groupReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><g><path d=\"M 150 0L 75 200L 225 200z \" fill=\"black\" stroke=\"black\" stroke-width=\"2.0\"/><line y1=\"0\" x2=\"200\" x1=\"0\" y2=\"200\" fill=\"black\" stroke=\"white\" stroke-width=\"2.0\"/></g></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "group")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), groupReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("group", withReference: true)
|
||||
}
|
||||
|
||||
func testSVGLine() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let lineReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><line y1=\"0\" x2=\"200\" x1=\"0\" y2=\"200\" fill=\"black\" stroke=\"red\" stroke-width=\"2.0\"/></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "line")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), lineReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("line")
|
||||
}
|
||||
|
||||
func testSVGPolygon() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let lineReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><polygon points=\"200.0,10.0,250.0,190.0,160.0,210.0\" fill=\"black\"/></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "polygon")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), lineReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("polygon")
|
||||
}
|
||||
|
||||
func testSVGPolyline() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let lineReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><polyline points=\"0.0,40.0,40.0,40.0,40.0,80.0,80.0,80.0,80.0,120.0,120.0,120.0,120.0,160.0\" fill=\"white\" stroke=\"red\" stroke-width=\"4.0\"/></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "polyline")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), lineReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("polyline")
|
||||
}
|
||||
|
||||
func testSVGRect() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let rectReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><rect height=\"150\" x=\"50\" y=\"0\" width=\"150\" fill=\"black\"/></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "rect")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), rectReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("rect")
|
||||
}
|
||||
|
||||
func testSVGRoundRect() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let roundRectReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><rect height=\"150\" ry=\"20\" rx=\"20\" width=\"150\" fill=\"black\"/></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "roundRect")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), roundRectReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("roundRect")
|
||||
}
|
||||
|
||||
func testSVGTriangle() {
|
||||
let bundle = Bundle(for: type(of: TestUtils()))
|
||||
let triangleReferenceContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ><g><path d=\"M 150 0L 75 200L 225 200z \" fill=\"black\" stroke=\"black\" stroke-width=\"2.0\"/></g></svg>"
|
||||
do {
|
||||
let node = try SVGParser.parse(bundle:bundle, path: "triangle")
|
||||
XCTAssertEqual(SVGSerializer.serialize(node: node), triangleReferenceContent)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
validate("triangle")
|
||||
}
|
||||
}
|
||||
|
1
MacawTests/svg/arcsgroup.reference
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><g transform="translate(10,10)" ><ellipse cy="20" ry="20" rx="20" cx="20" fill="none" stroke="green" stroke-width="1.0"/></g><g transform="translate(10,140)" ><path d="M20.0000015099579,39.9999999999999 A 20.0,20.0 0.0 0, 1 1.06581410364015e-14,20.0000006357301" fill="none" stroke="green" stroke-width="1.0"/></g><g transform="translate(110,140)" ><path d="M2.27373675443232e-13,20.0000030199161 A 20.0,20.0 0.0 0, 1 37.888543296214,11.0557270424323" fill="none" stroke="green" stroke-width="1.0"/></g></g></svg>
|
After Width: | Height: | Size: 622 B |
@ -1,6 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="circle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
|
||||
<circle cx="50" cy="50" r="40" stroke="#000000" stroke-width="3" fill="#f00" />
|
||||
<circle cx="140" cy="50" r="40" stroke="#000000" stroke-width="3" fill="#f0f0aa" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><circle r="40" cy="50" cx="50" fill="red" stroke="black" stroke-width="3.0"/><circle r="40" cy="50" cx="140" fill="#F0F0AA" stroke="black" stroke-width="3.0"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 276 B |
1
MacawTests/svg/clip.reference
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><defs><clipPath id="clipPath1"><path d="M 20 0m -20 0a 20 20 0 1 0 40 0a 20 20 0 1 0 -40 0M 0 10L 0 45L 100 45L 100 10z M 0 55L 0 90L 100 90L 100 55z " /></clipPath></defs><circle r="50" cy="50" cx="50" clip-path="url(#clipPath1)" fill="black"/></svg>
|
After Width: | Height: | Size: 355 B |
1
MacawTests/svg/clipManual.reference
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><defs><clipPath id="clipPath1"><rect height="90" x="10" y="10" width="90" /></clipPath><clipPath id="clipPath2"><rect height="190" x="110" y="110" width="190" /></clipPath></defs><g><circle r="20" cy="20" cx="20" clip-path="url(#clipPath1)" fill="red"/><circle r="20" cy="120" cx="120" clip-path="url(#clipPath2)" fill="green"/></g></svg>
|
After Width: | Height: | Size: 446 B |
@ -1,5 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="ellipse" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
|
||||
<ellipse cx="200" cy="80" rx="100" ry="50" style="fill:#ffff00;stroke:#800080;stroke-width:2" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><g><ellipse cy="80" ry="50" rx="100" cx="200" fill="yellow" stroke="purple" stroke-width="2.0"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 215 B |
1
MacawTests/svg/group.reference
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><path d="M 150 0L 75 200L 225 200z " fill="black" stroke="black" stroke-width="2.0"/><line y1="0" x2="200" x1="0" y2="200" fill="black" stroke="white" stroke-width="2.0"/></g></svg>
|
After Width: | Height: | Size: 288 B |
@ -1,5 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="line" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
|
||||
<line x1="0" y1="0" x2="200" y2="200" style="stroke:#ff0000;stroke-width:2"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><line y1="0" x2="200" x1="0" y2="200" fill="black" stroke="red" stroke-width="2.0"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 199 B |
@ -1,5 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="polygon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
|
||||
<polygon points="200,10 250,190 160,210"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><polygon points="200.0,10.0,250.0,190.0,160.0,210.0" fill="black"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 226 B After Width: | Height: | Size: 182 B |
@ -1,5 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="polyline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
|
||||
<polyline points="0,40 40,40 40,80 80,80 80,120 120,120 120,160" style="fill:#ffffff;stroke:#ff0000;stroke-width:4" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><polyline points="0.0,40.0,40.0,40.0,40.0,80.0,80.0,80.0,80.0,120.0,120.0,120.0,120.0,160.0" fill="white" stroke="red" stroke-width="4.0"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 254 B |
@ -1,5 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="rountRect" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
|
||||
<rect x="50" width="150" height="150"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><rect height="150" x="50" y="0" width="150" fill="black"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 225 B After Width: | Height: | Size: 173 B |
@ -1,5 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="rountRect" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
|
||||
<rect rx="20" ry="20" width="150" height="150"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><rect height="150" ry="20" rx="20" width="150" fill="black"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 234 B After Width: | Height: | Size: 176 B |
1
MacawTests/svg/style.reference
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><circle r="10" cy="50" cx="50" fill="white" stroke="#231F20" stroke-width="1.5"/><circle r="10" cy="50" cx="80" fill="black"/></g></svg>
|
After Width: | Height: | Size: 243 B |
1
MacawTests/svg/textBasicTransform.reference
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><g transform="translate(100,100)" ><text dominant-baseline="text-before-edge" fill="black" transform="matrix(0.707106781186548,-0.707106781186547,0.707106781186547,0.707106781186548,0.0,0.0)" >Point</text></g></g></svg>
|
After Width: | Height: | Size: 326 B |
1
MacawTests/svg/transform.reference
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g transform="matrix(2.0,1.0,1.0,1.0,0.0,0.0)" ><rect height="5" x="0" y="0" width="150" fill="blue"/><rect height="50" x="0" y="0" width="5" fill="red"/><rect height="50" x="150" y="0" width="5" fill="black"/><rect height="5" x="0" y="50" width="150" fill="black"/><ellipse cy="25" ry="15" rx="40" cx="75" fill="purple"/></g></svg>
|
After Width: | Height: | Size: 442 B |
@ -1,5 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg version="1.1" id="triangle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
|
||||
<path d="M 150 0 L 75 200 L 225 200 Z" style="stroke:#000000;stroke-width:2"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><g><path d="M 150 0L 75 200L 225 200z " fill="black" stroke="black" stroke-width="2.0"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 200 B |
@ -1,78 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
open class GeomUtils {
|
||||
|
||||
fileprivate class func rectToPath(_ rect: Rect) -> Path {
|
||||
return MoveTo(x: rect.x, y: rect.y).lineTo(x: rect.x, y: rect.y + rect.h).lineTo(x: rect.x + rect.w, y: rect.y + rect.h).lineTo(x: rect.x + rect.w, y: rect.y).close().build()
|
||||
}
|
||||
|
||||
fileprivate class func circleToPath(_ circle: Circle) -> Path {
|
||||
return MoveTo(x: circle.cx, y: circle.cy).m(-circle.r, 0).a(circle.r, circle.r, 0.0, true, false, circle.r * 2.0, 0.0).a(circle.r, circle.r, 0.0, true, false, -(circle.r * 2.0), 0.0).build()
|
||||
}
|
||||
|
||||
fileprivate class func arcToPath(_ arc: Arc) -> Path {
|
||||
let rx = arc.ellipse.rx
|
||||
let ry = arc.ellipse.ry
|
||||
let cx = arc.ellipse.cx
|
||||
let cy = arc.ellipse.cy
|
||||
|
||||
var delta = arc.extent
|
||||
if arc.shift == 0.0 && abs(arc.extent - Double.pi * 2.0) < 0.00001 {
|
||||
delta = Double.pi * 2.0 - 0.001
|
||||
}
|
||||
let theta1 = arc.shift
|
||||
|
||||
let theta2 = theta1 + delta
|
||||
|
||||
let x1 = cx + rx * cos(theta1)
|
||||
let y1 = cy + ry * sin(theta1)
|
||||
|
||||
let x2 = cx + rx * cos(theta2)
|
||||
let y2 = cy + ry * sin(theta2)
|
||||
|
||||
let largeArcFlag = abs(delta) > .pi ? true : false
|
||||
let sweepFlag = delta > 0.0 ? true : false
|
||||
|
||||
return PathBuilder(segment: PathSegment(type: .M, data: [x1, y1])).A(rx, ry, 0.0, largeArcFlag, sweepFlag, x2, y2).build()
|
||||
}
|
||||
|
||||
fileprivate class func pointToPath(_ point: Point) -> Path {
|
||||
return MoveTo(x: point.x, y: point.y).lineTo(x: point.x, y: point.y).build()
|
||||
}
|
||||
|
||||
fileprivate class func pointsToPath(_ points: [Double], close: Bool = false) -> Path {
|
||||
var pb = PathBuilder(segment: PathSegment(type: .M, data: [points[0], points[1]]))
|
||||
if points.count > 2 {
|
||||
let parts = stride(from: 2, to: points.count, by: 2).map { Array(points[$0 ..< $0 + 2]) }
|
||||
for part in parts {
|
||||
pb = pb.lineTo(x: part[0], y: part[1])
|
||||
}
|
||||
}
|
||||
if close {
|
||||
pb = pb.close()
|
||||
}
|
||||
return pb.build()
|
||||
}
|
||||
|
||||
open class func locusToPath(_ locus: Locus) -> Path {
|
||||
if let rect = locus as? Rect {
|
||||
return rectToPath(rect)
|
||||
} else if let circle = locus as? Circle {
|
||||
return circleToPath(circle)
|
||||
} else if let arc = locus as? Arc {
|
||||
return arcToPath(arc)
|
||||
} else if let point = locus as? Point {
|
||||
return MoveTo(x: point.x, y: point.y).lineTo(x: point.x, y: point.y).build()
|
||||
} else if let line = locus as? Line {
|
||||
return MoveTo(x: line.x1, y: line.y1).lineTo(x: line.x2, y: line.y2).build()
|
||||
} else if let polygon = locus as? Polygon {
|
||||
return pointsToPath(polygon.points, close: true)
|
||||
} else if let polyline = locus as? Polyline {
|
||||
return pointsToPath(polyline.points)
|
||||
}
|
||||
fatalError("Unsupported locus: \(locus)")
|
||||
}
|
||||
|
||||
open class func concat(t1: Transform, t2: Transform) -> Transform {
|
||||
let nm11 = t2.m11 * t1.m11 + t2.m12 * t1.m21
|
||||
let nm21 = t2.m21 * t1.m11 + t2.m22 * t1.m21
|
||||
|
75
Source/model/geom2d/Locus+ToPath.swift
Normal file
@ -0,0 +1,75 @@
|
||||
import Foundation
|
||||
|
||||
extension Locus {
|
||||
|
||||
fileprivate func rectToPath(_ rect: Rect) -> Path {
|
||||
return MoveTo(x: rect.x, y: rect.y).lineTo(x: rect.x, y: rect.y + rect.h).lineTo(x: rect.x + rect.w, y: rect.y + rect.h).lineTo(x: rect.x + rect.w, y: rect.y).close().build()
|
||||
}
|
||||
|
||||
fileprivate func circleToPath(_ circle: Circle) -> Path {
|
||||
return MoveTo(x: circle.cx, y: circle.cy).m(-circle.r, 0).a(circle.r, circle.r, 0.0, true, false, circle.r * 2.0, 0.0).a(circle.r, circle.r, 0.0, true, false, -(circle.r * 2.0), 0.0).build()
|
||||
}
|
||||
|
||||
fileprivate func arcToPath(_ arc: Arc) -> Path {
|
||||
let rx = arc.ellipse.rx
|
||||
let ry = arc.ellipse.ry
|
||||
let cx = arc.ellipse.cx
|
||||
let cy = arc.ellipse.cy
|
||||
|
||||
var delta = arc.extent
|
||||
if (arc.shift == 0.0 && abs(arc.extent - .pi * 2.0) < 0.00001) {
|
||||
delta = .pi * 2.0 - 0.001
|
||||
}
|
||||
let theta1 = arc.shift
|
||||
|
||||
let theta2 = theta1 + delta
|
||||
|
||||
let x1 = cx + rx * cos(theta1)
|
||||
let y1 = cy + ry * sin(theta1)
|
||||
|
||||
let x2 = cx + rx * cos(theta2)
|
||||
let y2 = cy + ry * sin(theta2)
|
||||
|
||||
let largeArcFlag = abs(delta) > .pi ? true : false
|
||||
let sweepFlag = delta > 0.0 ? true : false
|
||||
|
||||
return PathBuilder(segment: PathSegment(type: .M, data: [x1, y1])).A(rx, ry, 0.0, largeArcFlag, sweepFlag, x2, y2).build()
|
||||
}
|
||||
|
||||
fileprivate func pointToPath(_ point: Point) -> Path {
|
||||
return MoveTo(x: point.x, y: point.y).lineTo(x: point.x, y: point.y).build()
|
||||
}
|
||||
|
||||
fileprivate func pointsToPath(_ points: [Double], close: Bool = false) -> Path {
|
||||
var pb = PathBuilder(segment: PathSegment(type: .M, data: [points[0], points[1]]))
|
||||
if (points.count > 2) {
|
||||
let parts = stride(from: 2, to: points.count, by: 2).map { Array(points[$0 ..< $0 + 2]) }
|
||||
for part in parts {
|
||||
pb = pb.lineTo(x: part[0], y: part[1])
|
||||
}
|
||||
}
|
||||
if close {
|
||||
pb = pb.close()
|
||||
}
|
||||
return pb.build()
|
||||
}
|
||||
|
||||
public func toPath() -> Path {
|
||||
if let rect = self as? Rect {
|
||||
return rectToPath(rect)
|
||||
} else if let circle = self as? Circle {
|
||||
return circleToPath(circle)
|
||||
} else if let arc = self as? Arc {
|
||||
return arcToPath(arc)
|
||||
} else if let point = self as? Point {
|
||||
return MoveTo(x: point.x, y: point.y).lineTo(x: point.x, y: point.y).build()
|
||||
} else if let line = self as? Line {
|
||||
return MoveTo(x: line.x1, y: line.y1).lineTo(x: line.x2, y: line.y2).build()
|
||||
} else if let polygon = self as? Polygon {
|
||||
return pointsToPath(polygon.points, close: true)
|
||||
} else if let polyline = self as? Polyline {
|
||||
return pointsToPath(polyline.points)
|
||||
}
|
||||
fatalError("Unsupported locus: \(self)")
|
||||
}
|
||||
}
|
@ -853,9 +853,9 @@ open class SVGParser {
|
||||
clip.children.forEach { indexer in
|
||||
if let shape = parseNode(indexer) as? Shape {
|
||||
if let p = path {
|
||||
path = Path(segments: p.segments + GeomUtils.locusToPath(shape.form).segments)
|
||||
path = Path(segments: p.segments + shape.form.toPath() .segments)
|
||||
} else {
|
||||
path = GeomUtils.locusToPath(shape.form)
|
||||
path = shape.form.toPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ open class SVGSerializer {
|
||||
}
|
||||
|
||||
fileprivate func arcToSVG(_ arc: Arc) -> String {
|
||||
if arc.shift == 0.0 && abs(arc.extent - Double.pi * 2.0) < SVGEpsilon {
|
||||
if arc.shift == 0.0 && abs(arc.extent - .pi * 2.0) < SVGEpsilon {
|
||||
return tag(SVGEllipseOpenTag, ["cx": att(arc.ellipse.cx), "cy": att(arc.ellipse.cy), "rx": att(arc.ellipse.rx), "ry": att(arc.ellipse.ry)])
|
||||
} else {
|
||||
let rx = arc.ellipse.rx
|
||||
|