Skip to content

Commit 68d9d50

Browse files
committed
Subpaths
1 parent ddc4256 commit 68d9d50

File tree

5 files changed

+374
-1
lines changed

5 files changed

+374
-1
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// LayerTreet.Path+Reversed.swift
3+
// SwiftDraw
4+
//
5+
// Created by Simon Whitty on 21/8/22.
6+
// Copyright 2022 Simon Whitty
7+
//
8+
// Distributed under the permissive zlib license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/SwiftDraw
12+
//
13+
// This software is provided 'as-is', without any express or implied
14+
// warranty. In no event will the authors be held liable for any damages
15+
// arising from the use of this software.
16+
//
17+
// Permission is granted to anyone to use this software for any purpose,
18+
// including commercial applications, and to alter it and redistribute it
19+
// freely, subject to the following restrictions:
20+
//
21+
// 1. The origin of this software must not be misrepresented; you must not
22+
// claim that you wrote the original software. If you use this software
23+
// in a product, an acknowledgment in the product documentation would be
24+
// appreciated but is not required.
25+
//
26+
// 2. Altered source versions must be plainly marked as such, and must not be
27+
// misrepresented as being the original software.
28+
//
29+
// 3. This notice may not be removed or altered from any source distribution.
30+
//
31+
32+
extension LayerTree.Path {
33+
34+
var reversed: LayerTree.Path {
35+
var reversed = segments
36+
.reversed()
37+
.paired(with: .nextSkippingLast)
38+
.compactMap { segment, next in
39+
segment.reversing(to: next.location)
40+
}
41+
42+
if let point = segments.lastLocation {
43+
reversed.insert(.move(to: point), at: 0)
44+
}
45+
46+
while reversed.last?.isMove == true {
47+
reversed.removeLast()
48+
}
49+
50+
if segments.last?.isClose == true {
51+
reversed.append(.close)
52+
}
53+
54+
return .init(reversed)
55+
}
56+
}
57+
58+
private extension Array where Element == LayerTree.Path.Segment {
59+
60+
var lastLocation: LayerTree.Point? {
61+
for segment in reversed() {
62+
if let location = segment.location {
63+
return location
64+
}
65+
}
66+
return nil
67+
}
68+
}
69+
70+
private extension LayerTree.Path.Segment {
71+
72+
func reversing(to point: LayerTree.Point?) -> Self? {
73+
guard let point = point else { return nil }
74+
switch self {
75+
case .move:
76+
return .move(to: point)
77+
case .line:
78+
return .line(to: point)
79+
case let .cubic(to: _, control1: control1, control2: control2):
80+
return .cubic(to: point, control1: control2, control2: control1)
81+
case .close:
82+
return nil
83+
}
84+
}
85+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// LayerTreet.Path+Subpath.swift
3+
// SwiftDraw
4+
//
5+
// Created by Simon Whitty on 21/8/22.
6+
// Copyright 2022 Simon Whitty
7+
//
8+
// Distributed under the permissive zlib license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/SwiftDraw
12+
//
13+
// This software is provided 'as-is', without any express or implied
14+
// warranty. In no event will the authors be held liable for any damages
15+
// arising from the use of this software.
16+
//
17+
// Permission is granted to anyone to use this software for any purpose,
18+
// including commercial applications, and to alter it and redistribute it
19+
// freely, subject to the following restrictions:
20+
//
21+
// 1. The origin of this software must not be misrepresented; you must not
22+
// claim that you wrote the original software. If you use this software
23+
// in a product, an acknowledgment in the product documentation would be
24+
// appreciated but is not required.
25+
//
26+
// 2. Altered source versions must be plainly marked as such, and must not be
27+
// misrepresented as being the original software.
28+
//
29+
// 3. This notice may not be removed or altered from any source distribution.
30+
//
31+
32+
extension LayerTree.Path {
33+
34+
var subpaths: [LayerTree.Path] {
35+
var builder = SubpathBuilder()
36+
return builder.makeSubpaths(for: self)
37+
}
38+
}
39+
40+
extension LayerTree.Path {
41+
42+
struct SubpathBuilder {
43+
typealias Point = LayerTree.Point
44+
45+
var start: Point?
46+
var location: Point = .zero
47+
48+
var subpaths = [LayerTree.Path]()
49+
var current = [LayerTree.Path.Segment]()
50+
51+
mutating func makeSubpaths(for path: LayerTree.Path) -> [LayerTree.Path] {
52+
subpaths = []
53+
current = []
54+
start = nil
55+
for s in path.segments {
56+
appendSegment(s)
57+
}
58+
59+
if current.contains(where: \.isEdge) {
60+
subpaths.append(.init(current))
61+
}
62+
63+
return subpaths
64+
}
65+
66+
mutating func appendSegment(_ segment: LayerTree.Path.Segment) {
67+
switch segment {
68+
case let .move(to: p):
69+
if let idx = current.indices.last, current[idx].isMove {
70+
current[idx] = segment
71+
} else {
72+
current.append(segment)
73+
}
74+
location = p
75+
start = nil
76+
case let .line(to: p):
77+
current.append(segment)
78+
if start == nil {
79+
start = location
80+
}
81+
location = p
82+
case let .cubic(to: p, control1: _, control2: _):
83+
current.append(segment)
84+
if start == nil {
85+
start = location
86+
}
87+
location = p
88+
case .close:
89+
current.append(segment)
90+
subpaths.append(.init(current))
91+
current = []
92+
if let start = start {
93+
location = start
94+
current.append(.move(to: start))
95+
}
96+
start = nil
97+
}
98+
}
99+
}
100+
}
101+
102+
private extension LayerTree.Path.Segment {
103+
104+
var isEdge: Bool {
105+
switch self {
106+
case .line, .cubic:
107+
return true
108+
case .move, .close:
109+
return false
110+
}
111+
}
112+
}

SwiftDraw/LayerTree.Path.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ extension LayerTree.Path {
8888
}
8989
}
9090

91-
private extension LayerTree.Path.Segment {
91+
extension LayerTree.Path.Segment {
9292

9393
var isClose: Bool {
9494
guard case .close = self else {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// LayerTreet.Path+ReversedTests.swift
3+
// SwiftDraw
4+
//
5+
// Created by Simon Whitty on 21/8/22.
6+
// Copyright 2022 Simon Whitty
7+
//
8+
// Distributed under the permissive zlib license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/SwiftDraw
12+
//
13+
// This software is provided 'as-is', without any express or implied
14+
// warranty. In no event will the authors be held liable for any damages
15+
// arising from the use of this software.
16+
//
17+
// Permission is granted to anyone to use this software for any purpose,
18+
// including commercial applications, and to alter it and redistribute it
19+
// freely, subject to the following restrictions:
20+
//
21+
// 1. The origin of this software must not be misrepresented; you must not
22+
// claim that you wrote the original software. If you use this software
23+
// in a product, an acknowledgment in the product documentation would be
24+
// appreciated but is not required.
25+
//
26+
// 2. Altered source versions must be plainly marked as such, and must not be
27+
// misrepresented as being the original software.
28+
//
29+
// 3. This notice may not be removed or altered from any source distribution.
30+
//
31+
32+
@testable import SwiftDraw
33+
import XCTest
34+
35+
final class LayerTreePathReversedTests: XCTestCase {
36+
37+
typealias Point = LayerTree.Point
38+
typealias Rect = LayerTree.Rect
39+
40+
func testIgnoresRedundantMoves() {
41+
let path = LayerTree.Path([
42+
.move(to: Point(100, 100)),
43+
.line(to: Point(200, 200)),
44+
.line(to: Point(300, 300))
45+
])
46+
47+
XCTAssertEqual(
48+
path.reversed.segments,
49+
[.move(to: Point(300, 300)),
50+
.line(to: Point(200, 200)),
51+
.line(to: Point(100, 100))]
52+
)
53+
}
54+
55+
func testCubic() {
56+
let path = LayerTree.Path([
57+
.move(to: Point(100, 100)),
58+
.cubic(to: Point(200, 100), control1: Point(110, 50), control2: Point(190, 50)),
59+
.line(to: Point(200, 300)),
60+
.cubic(to: Point(100, 300), control1: Point(190, 350), control2: Point(110, 350)),
61+
.close
62+
])
63+
64+
XCTAssertEqual(
65+
path.reversed.segments,
66+
[.move(to: Point(100, 300)),
67+
.cubic(to: Point(200, 300), control1: Point(110, 350), control2: Point(190, 350)),
68+
.line(to: Point(200, 100)),
69+
.cubic(to: Point(100, 100), control1: Point(190, 50), control2: Point(110, 50)),
70+
.close]
71+
)
72+
}
73+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//
2+
// LayerTreet.Path+SubpathTests.swift
3+
// SwiftDraw
4+
//
5+
// Created by Simon Whitty on 21/8/22.
6+
// Copyright 2022 Simon Whitty
7+
//
8+
// Distributed under the permissive zlib license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/SwiftDraw
12+
//
13+
// This software is provided 'as-is', without any express or implied
14+
// warranty. In no event will the authors be held liable for any damages
15+
// arising from the use of this software.
16+
//
17+
// Permission is granted to anyone to use this software for any purpose,
18+
// including commercial applications, and to alter it and redistribute it
19+
// freely, subject to the following restrictions:
20+
//
21+
// 1. The origin of this software must not be misrepresented; you must not
22+
// claim that you wrote the original software. If you use this software
23+
// in a product, an acknowledgment in the product documentation would be
24+
// appreciated but is not required.
25+
//
26+
// 2. Altered source versions must be plainly marked as such, and must not be
27+
// misrepresented as being the original software.
28+
//
29+
// 3. This notice may not be removed or altered from any source distribution.
30+
//
31+
32+
@testable import SwiftDraw
33+
import XCTest
34+
35+
final class LayerTreePathSubpathTests: XCTestCase {
36+
37+
typealias Point = LayerTree.Point
38+
typealias Rect = LayerTree.Rect
39+
40+
func testIgnoresRedundantMoves() {
41+
let path = LayerTree.Path([
42+
.move(to: Point(-50, -50)),
43+
.move(to: Point(100, 100)),
44+
.line(to: Point(200, 200)),
45+
.move(to: Point(-50, -50)),
46+
.move(to: Point(0, 0)),
47+
.line(to: Point(0, 200))
48+
])
49+
50+
XCTAssertEqual(
51+
path.subpaths.count, 1
52+
)
53+
54+
XCTAssertEqual(
55+
path.subpaths[0].segments,
56+
[.move(to: Point(100, 100)),
57+
.line(to: Point(200, 200)),
58+
.move(to: Point(0, 0)),
59+
.line(to: Point(0, 200))]
60+
)
61+
}
62+
63+
func testClosedPathsStartNewSubpath() {
64+
let path = LayerTree.Path([
65+
.move(to: Point(100, 100)),
66+
.line(to: Point(200, 200)),
67+
.line(to: Point(200, 100)),
68+
.close,
69+
.line(to: Point(0, 200)),
70+
.line(to: Point(0, 100)),
71+
.close,
72+
.move(to: Point(500, 500))
73+
])
74+
75+
XCTAssertEqual(
76+
path.subpaths.count, 2
77+
)
78+
79+
XCTAssertEqual(
80+
path.subpaths[0].segments.direction,
81+
.anticlockwise
82+
)
83+
XCTAssertEqual(
84+
path.subpaths[0].segments,
85+
[.move(to: Point(100, 100)),
86+
.line(to: Point(200, 200)),
87+
.line(to: Point(200, 100)),
88+
.close]
89+
)
90+
91+
XCTAssertEqual(
92+
path.subpaths[1].segments.direction,
93+
.clockwise
94+
)
95+
XCTAssertEqual(
96+
path.subpaths[1].segments,
97+
[.move(to: Point(100, 100)),
98+
.line(to: Point(0, 200)),
99+
.line(to: Point(0, 100)),
100+
.close]
101+
)
102+
}
103+
}

0 commit comments

Comments
 (0)