Skip to content

Commit cd266c1

Browse files
authored
Merge pull request #73 from RougeWare/feature/Point-offset
Added Point2D offset functions
2 parents d9d8856 + 60b08c1 commit cd266c1

File tree

4 files changed

+162
-50
lines changed

4 files changed

+162
-50
lines changed

.github/workflows/iOS.yml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,22 @@ name: iOS
33
on: [pull_request]
44

55
jobs:
6-
build:
7-
6+
test:
87
timeout-minutes: 6
98
runs-on: macOS-latest
109

1110
steps:
12-
- name: Set up Swift 6
13-
uses: SwiftyLab/setup-swift@latest
14-
with:
15-
swift-version: '6.0'
16-
1711
- name: Swift version
1812
run: swift --version
1913

20-
- uses: actions/checkout@v3
21-
22-
- name: Build
23-
run: swift build
14+
- uses: actions/checkout@v4
2415

2516
- name: What schemes?
2617
run: xcodebuild build test -list
2718

2819
- name: Run iOS tests
29-
run: xcodebuild build test -destination 'name=iPhone 16' -scheme 'RectangleTools'
30-
20+
run: |
21+
xcodebuild test \
22+
-sdk iphonesimulator \
23+
-destination 'platform=iOS Simulator,name=iPhone 17' \
24+
-scheme RectangleTools

.github/workflows/watchOS.yml

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,20 @@ name: watchOS
33
on: [pull_request]
44

55
jobs:
6-
build:
7-
6+
test:
7+
runs-on: macos-latest
88
timeout-minutes: 4
9-
runs-on: macOS-latest
109

1110
steps:
12-
- name: Set up Swift 6
13-
uses: SwiftyLab/setup-swift@latest
14-
with:
15-
swift-version: '6.0'
16-
11+
- name: Checkout code
12+
uses: actions/checkout@v4
13+
1714
- name: Swift version
1815
run: swift --version
19-
20-
- uses: actions/checkout@v3
21-
22-
- name: Build
23-
run: swift build
24-
25-
- name: What schemes?
26-
run: xcodebuild build test -list
2716

2817
- name: Run watchOS tests
29-
run: xcodebuild build test -destination 'name=Apple Watch Ultra 2 (49mm)' -scheme 'RectangleTools'
30-
18+
run: |
19+
xcodebuild test \
20+
-sdk watchsimulator \
21+
-destination 'platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)' \
22+
-scheme RectangleTools

Sources/RectangleTools/Synthesized Conveniences/Point2D Extensions.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,41 @@ where Length: MultiplicativeArithmetic,
6161
)
6262
}
6363
}
64+
65+
66+
67+
// MARK: - Moving the point
68+
69+
public extension Point2D
70+
where Length: AdditiveArithmetic
71+
{
72+
/// Creates a new point which is offset from this one by the given amounts in each dimension
73+
///
74+
/// - Parameters:
75+
/// - dx: The amount by which to move the new point along the X axis, relative to this point (e.g. `-2` decreases the X value of the point by 2)
76+
/// - dy: The amount by which to move the new point along the Y axis, relative to this point (e.g. `3` increases the Y value of the point by 3)
77+
func offset(dx: Length, dy: Length) -> Self {
78+
Self(x: x + dx, y: y + dy)
79+
}
80+
81+
82+
/// Creates a new point which is offset from this one by the given amounts in each dimension
83+
///
84+
/// - Parameters:
85+
/// - offset: The amount by which to move the new point along the X and Y axes, relative to this point (e.g. `(x: -2, y: 3)` decreases the X value of the returned point by 2 and increases the Y value by 3)
86+
func offset<Other>(by offset: Other) -> Self
87+
where Other: TwoDimensional, Other.Length == Self.Length
88+
{
89+
self.offset(dx: offset.measurementX, dy: offset.measurementY)
90+
}
91+
92+
93+
/// Creates a new point which is offset from the left one by the given amounts of the right one in each dimension
94+
///
95+
/// - Parameters:
96+
/// - lhs: The initial origin point
97+
/// - rhs: The amount by which to move the resulting point along the X and Y axes, relative to the origin point `lhs` (e.g. `(x: -2, y: 3)` decreases the X value of the returned point by 2 and increases the Y value by 3)
98+
static func + (lhs: Self, rhs: Self) -> Self {
99+
lhs.offset(by: rhs)
100+
}
101+
}

Tests/RectangleToolsTests/Point Tests.swift

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,156 @@
55
// Created by The Northstar✨ System on 2023-10-26.
66
//
77

8-
import XCTest
8+
import Foundation
9+
import CoreGraphics
10+
#if canImport(CoreImage)
11+
import CoreImage
12+
#endif
13+
import Testing
14+
915
import RectangleTools
1016

1117

1218

13-
final class Point_Tests: XCTestCase {
19+
@Suite("Point Tests")
20+
final class Point_Tests {
1421

22+
@Test
1523
func testPointToPointDistance() {
16-
XCTAssertEqual(CGPoint(x: 0, y: 0).distance(to: CGPoint(x: 1, y: 1)), sqrt(2))
17-
XCTAssertEqual(CGPoint(x: 0, y: 0).distance(to: CGPoint(x: -1, y: 1)), sqrt(2))
18-
XCTAssertEqual(CGPoint(x: 0, y: 0).distance(to: CGPoint(x: 1, y: -1)), sqrt(2))
19-
XCTAssertEqual(CGPoint(x: 0, y: 0).distance(to: CGPoint(x: -1, y: -1)), sqrt(2))
24+
#expect(CGPoint(x: 0, y: 0).distance(to: CGPoint(x: 1, y: 1)) == CGFloat(sqrt(2)))
25+
#expect(CGPoint(x: 0, y: 0).distance(to: CGPoint(x: -1, y: 1)) == CGFloat(sqrt(2)))
26+
#expect(CGPoint(x: 0, y: 0).distance(to: CGPoint(x: 1, y: -1)) == CGFloat(sqrt(2)))
27+
#expect(CGPoint(x: 0, y: 0).distance(to: CGPoint(x: -1, y: -1)) == CGFloat(sqrt(2)))
2028
}
2129

2230

31+
@Test
2332
func testSizeExtremitiesDistance() {
2433
var cgSize = CGSize(width: 1, height: 1)
25-
XCTAssertEqual(cgSize.minXminY().distance(to: cgSize.maxXmaxY()), sqrt(2))
34+
#expect(cgSize.minXminY().distance(to: cgSize.maxXmaxY()) == CGFloat(sqrt(2)))
2635

2736
cgSize = CGSize(width: -1, height: 1)
28-
XCTAssertEqual(cgSize.minXminY().distance(to: cgSize.maxXmaxY()), sqrt(2))
37+
#expect(cgSize.minXminY().distance(to: cgSize.maxXmaxY()) == CGFloat(sqrt(2)))
2938

3039
cgSize = CGSize(width: 1, height: -1)
31-
XCTAssertEqual(cgSize.minXminY().distance(to: cgSize.maxXmaxY()), sqrt(2))
40+
#expect(cgSize.minXminY().distance(to: cgSize.maxXmaxY()) == CGFloat(sqrt(2)))
3241

3342
cgSize = CGSize(width: -1, height: -1)
34-
XCTAssertEqual(cgSize.minXminY().distance(to: cgSize.maxXmaxY()), sqrt(2))
43+
#expect(cgSize.minXminY().distance(to: cgSize.maxXmaxY()) == CGFloat(sqrt(2)))
3544
}
3645

3746

47+
@Test
3848
func testRectExtremitiesDistance() {
3949
var cgRect = CGRect(x: 0, y: 0, width: 1, height: 1)
40-
XCTAssertEqual(cgRect.minXminY.distance(to: cgRect.maxXmaxY), sqrt(2))
50+
#expect(cgRect.minXminY.distance(to: cgRect.maxXmaxY) == CGFloat(sqrt(2)))
4151

4252
cgRect = CGRect(x: 0, y: 0, width: -1, height: 1)
43-
XCTAssertEqual(cgRect.minXminY.distance(to: cgRect.maxXmaxY), sqrt(2))
53+
#expect(cgRect.minXminY.distance(to: cgRect.maxXmaxY) == CGFloat(sqrt(2)))
4454

4555
cgRect = CGRect(x: 0, y: 0, width: 1, height: -1)
46-
XCTAssertEqual(cgRect.minXminY.distance(to: cgRect.maxXmaxY), sqrt(2))
56+
#expect(cgRect.minXminY.distance(to: cgRect.maxXmaxY) == CGFloat(sqrt(2)))
4757

4858
cgRect = CGRect(x: 0, y: 0, width: -1, height: -1)
49-
XCTAssertEqual(cgRect.minXminY.distance(to: cgRect.maxXmaxY), sqrt(2))
59+
#expect(cgRect.minXminY.distance(to: cgRect.maxXmaxY) == CGFloat(sqrt(2)))
5060

5161

5262
cgRect = CGRect(x: .random(in: -1000 ... 1000), y: .random(in: -1000 ... 1000), width: 1, height: 1)
53-
XCTAssertEqual(cgRect.minXminY.distance(to: cgRect.maxXmaxY), sqrt(2))
63+
#expect(cgRect.minXminY.distance(to: cgRect.maxXmaxY) == CGFloat(sqrt(2)))
5464

5565
cgRect = CGRect(x: .random(in: -1000 ... 1000), y: .random(in: -1000 ... 1000), width: -1, height: 1)
56-
XCTAssertEqual(cgRect.minXminY.distance(to: cgRect.maxXmaxY), sqrt(2))
66+
#expect(cgRect.minXminY.distance(to: cgRect.maxXmaxY) == CGFloat(sqrt(2)))
5767

5868
cgRect = CGRect(x: .random(in: -1000 ... 1000), y: .random(in: -1000 ... 1000), width: 1, height: -1)
59-
XCTAssertEqual(cgRect.minXminY.distance(to: cgRect.maxXmaxY), sqrt(2))
69+
#expect(cgRect.minXminY.distance(to: cgRect.maxXmaxY) == CGFloat(sqrt(2)))
6070

6171
cgRect = CGRect(x: .random(in: -1000 ... 1000), y: .random(in: -1000 ... 1000), width: -1, height: -1)
62-
XCTAssertEqual(cgRect.minXminY.distance(to: cgRect.maxXmaxY), sqrt(2))
72+
#expect(cgRect.minXminY.distance(to: cgRect.maxXmaxY) == CGFloat(sqrt(2)))
6373
}
6474

6575

76+
@Test
6677
func testMagnitude() {
6778
// https://www.wolframalpha.com/input?i=distance+from+%28-2%2C-1%29+to+%285%2C6%29
6879
#if canImport(CoreImage)
69-
XCTAssertEqual(CIVector(x: -2, y: -1, z: 5, w: 6).magnitude, 7 * sqrt(2))
80+
#expect(CIVector(x: -2, y: -1, z: 5, w: 6).magnitude == 7 * sqrt(2))
7081
#endif
7182
}
83+
84+
85+
@Test
86+
func testPointOffset() {
87+
// Basics
88+
#expect(CGPoint(x: 0, y: 0).offset(dx: 10, dy: 20) == .init(x: 10, y: 20))
89+
#expect(CGPoint(x: 0, y: 0).offset(dx: -10, dy: -20) == .init(x: -10, y: -20))
90+
91+
#expect(CGPoint.zero.offset(by: CGPoint(x: 10, y: 20)) == .init(x: 10, y: 20))
92+
#expect(CGPoint.zero + CGPoint(x: 10, y: 20) == .init(x: 10, y: 20))
93+
#expect(CGPoint.zero.offset(by: CGPoint(x: -10, y: -20)) == .init(x: -10, y: -20))
94+
#expect(CGPoint.zero + CGPoint(x: -10, y: -20) == .init(x: -10, y: -20))
95+
96+
97+
// Fuzzing
98+
for _ in 0 ..< 20 {
99+
let point = CGPoint(x: .random(in: -1000 ... 1000), y: .random(in: -1000 ... 1000))
100+
let offset = CGPoint(x: .random(in: -1000 ... 1000), y: .random(in: -1000 ... 1000))
101+
let expected = CGPoint(x: point.x + offset.x, y: point.y + offset.y)
102+
103+
#expect(point.offset(dx: offset.x, dy: offset.y) == expected)
104+
#expect(point.offset(by: offset) == expected)
105+
#expect(point + offset == expected)
106+
}
107+
108+
// LLM-generated tests
109+
do {
110+
// Zero offset preserves the point
111+
#expect(CGPoint(x: 5, y: -3).offset(dx: 0, dy: 0) == CGPoint(x: 5, y: -3))
112+
#expect(CGPoint(x: 5, y: -3).offset(by: CGPoint.zero) == CGPoint(x: 5, y: -3))
113+
#expect(CGPoint(x: 5, y: -3) + .zero == CGPoint(x: 5, y: -3))
114+
#expect(CGPoint.zero.offset(by: CGPoint(x: 0, y: 0)) == CGPoint.zero)
115+
116+
// Offset by identical point doubles coordinates
117+
let point = CGPoint(x: 2, y: 3)
118+
#expect(point.offset(by: point) == CGPoint(x: 4, y: 6))
119+
#expect(point + point == CGPoint(x: 4, y: 6))
120+
121+
// Offset that results in the origin
122+
#expect(CGPoint(x: -1, y: -2).offset(by: CGPoint(x: 1, y: 2)) == CGPoint.zero)
123+
124+
// Offset with negative zero (no‑op)
125+
#expect(CGPoint(x: 3, y: 4).offset(dx: -0, dy: -0) == CGPoint(x: 3, y: 4))
126+
let minusZero = CGFloat(-0.0)
127+
#expect(CGPoint(x: 5, y: 5).offset(dx: minusZero, dy: minusZero) == CGPoint(x: 5, y: 5))
128+
129+
// Combination of positive and negative components
130+
#expect(CGPoint(x: -10, y: 10).offset(dx: 15, dy: -5) == CGPoint(x: 5, y: 5))
131+
132+
// Edge cases: infinities
133+
let inf = CGFloat.infinity
134+
let pInf = CGPoint(x: 10, y: -20).offset(dx: inf, dy: -inf)
135+
#expect(pInf.x.isInfinite)
136+
#expect(pInf.y.isInfinite && pInf.y.sign == .minus)
137+
138+
// Edge cases: NaNs
139+
let nanPt = CGPoint(x: CGFloat.nan, y: .nan).offset(dx: 1, dy: 1)
140+
#expect(nanPt.x.isNaN)
141+
#expect(nanPt.y.isNaN)
142+
143+
// Offset a point containing a NaN
144+
var ptWithNan = CGPoint(x: 5, y: CGFloat.nan).offset(by: CGPoint(x: 0, y: 1))
145+
#expect(ptWithNan.y.isNaN)
146+
ptWithNan = CGPoint(x: 5, y: CGFloat.nan) + CGPoint(x: 0, y: 1)
147+
#expect(ptWithNan.y.isNaN)
148+
149+
// Extreme magnitude causing small additions to "snap back" to the value before addition
150+
let large = CGFloat.greatestFiniteMagnitude
151+
let ptLarge = CGPoint(x: large, y: large).offset(dx: 1, dy: -1)
152+
#expect(large == ptLarge.x)
153+
#expect(large == ptLarge.y)
154+
155+
// Very small values (close to zero)
156+
let tiny = CGFloat(1e-300)
157+
#expect(CGPoint.zero.offset(dx: tiny, dy: -tiny) == CGPoint(x: tiny, y: -tiny))
158+
}
159+
}
72160
}

0 commit comments

Comments
 (0)