diff --git a/Sources/OpenGestures/Util/AdditiveArithmetic.swift b/Sources/OpenGestures/Util/AdditiveArithmetic.swift new file mode 100644 index 0000000..96bd33e --- /dev/null +++ b/Sources/OpenGestures/Util/AdditiveArithmetic.swift @@ -0,0 +1,69 @@ +// +// AdditiveArithmetic.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +public import OpenCoreGraphicsShims + +// MARK: - _AdditiveArithmetic + +package protocol _AdditiveArithmetic: Equatable { + static var zero: Self { get } + + static func + (lhs: Self, rhs: Self) -> Self + static func += (lhs: inout Self, rhs: Self) + static func - (lhs: Self, rhs: Self) -> Self + static func -= (lhs: inout Self, rhs: Self) +} + +extension _AdditiveArithmetic { + package static func += (lhs: inout Self, rhs: Self) { + lhs = lhs + rhs + } + + package static func -= (lhs: inout Self, rhs: Self) { + lhs = lhs - rhs + } +} + +// MARK: - Double + _AdditiveArithmetic + +extension Double: _AdditiveArithmetic {} + +// MARK: - CGPoint + _AdditiveArithmetic + +extension CGPoint: _AdditiveArithmetic { + package static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { + CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } + + package static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { + CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) + } +} + +// MARK: - CGVector + _AdditiveArithmetic + +extension CGVector: _AdditiveArithmetic { + package static func + (lhs: CGVector, rhs: CGVector) -> CGVector { + CGVector(dx: lhs.dx + rhs.dx, dy: lhs.dy + rhs.dy) + } + + package static func - (lhs: CGVector, rhs: CGVector) -> CGVector { + CGVector(dx: lhs.dx - rhs.dx, dy: lhs.dy - rhs.dy) + } +} + +// MARK: - CGSize + _AdditiveArithmetic + +extension CGSize: _AdditiveArithmetic { + package static func + (lhs: CGSize, rhs: CGSize) -> CGSize { + CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) + } + + package static func - (lhs: CGSize, rhs: CGSize) -> CGSize { + CGSize(width: lhs.width - rhs.width, height: lhs.height - rhs.height) + } +} diff --git a/Sources/OpenGestures/Util/LocationContaining.swift b/Sources/OpenGestures/Util/LocationContaining.swift index 6cf3eb3..0c6588c 100644 --- a/Sources/OpenGestures/Util/LocationContaining.swift +++ b/Sources/OpenGestures/Util/LocationContaining.swift @@ -27,9 +27,26 @@ extension Never: LocationContaining { public var location: CGPoint { fatalError() } } -// MARK: - IdentifiableLocation [WIP] +// MARK: - IdentifiableLocation -package struct IdentifiableLocation where ID: Hashable { +package struct IdentifiableLocation: Identifiable where ID: Hashable { package var id: ID package var location: CGPoint } + +extension IdentifiableLocation: LocationContaining {} + +extension IdentifiableLocation: NestedCustomStringConvertible {} + +extension IdentifiableLocation: VectorContaining { + package typealias VectorType = CGPoint + + package var vector: CGPoint { + get { location } + set { location = newValue } + } +} + +extension IdentifiableLocation: ThresholdAdjustable { + package typealias Threshold = Double +} diff --git a/Sources/OpenGestures/Util/ThresholdAdjustable.swift b/Sources/OpenGestures/Util/ThresholdAdjustable.swift new file mode 100644 index 0000000..c4a83cd --- /dev/null +++ b/Sources/OpenGestures/Util/ThresholdAdjustable.swift @@ -0,0 +1,52 @@ +// +// ThresholdAdjustable.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +public import OpenCoreGraphicsShims + +// MARK: - ThresholdAdjustable + +/// A value whose backing vector can consume a threshold-sized movement. +package protocol ThresholdAdjustable: VectorContaining { + /// The scalar threshold type used to gate movement. + associatedtype Threshold + + /// Consumes up to `threshold` units from `movement`. + /// + /// When `movement` reaches the threshold, this subtracts the threshold-sized + /// portion of `movement` from `vector` and returns that consumed movement. + /// Returns `nil` without mutation when `threshold` is not positive or + /// `movement` is below threshold. + mutating func consume(_ threshold: Threshold, from movement: VectorType) -> VectorType? +} + +extension ThresholdAdjustable where Threshold == Double { + package mutating func consume(_ threshold: Double, from movement: VectorType) -> VectorType? { + guard threshold > 0 else { return nil } + + let movementMagnitude = movement.magnitude + guard movementMagnitude >= threshold else { return nil } + + let scale = threshold / movementMagnitude + let consumedMovement = movement.scaled(by: scale) + vector -= consumedMovement + return consumedMovement + } +} + +// MARK: - ThresholdAdjustable Conformance + +extension Double: ThresholdAdjustable { + package typealias Threshold = Double +} + +extension CGPoint: ThresholdAdjustable { + package typealias Threshold = Double +} + +extension CGVector: ThresholdAdjustable { + package typealias Threshold = Double +} diff --git a/Sources/OpenGestures/Util/VectorArithmetic.swift b/Sources/OpenGestures/Util/VectorArithmetic.swift new file mode 100644 index 0000000..4873d97 --- /dev/null +++ b/Sources/OpenGestures/Util/VectorArithmetic.swift @@ -0,0 +1,85 @@ +// +// VectorArithmetic.swift +// OpenGestures +// +// Audited for 9126.1.5 +// Status: Complete + +public import OpenCoreGraphicsShims + +// MARK: - VectorArithmetic + +package protocol VectorArithmetic: _AdditiveArithmetic { + var magnitude: Double { get } + + func scaled(by rhs: Double) -> Self +} + +// MARK: - VectorArithmetic Conformance + +extension Double: VectorArithmetic { + package var magnitude: Double { + abs(self) + } + + package func scaled(by rhs: Double) -> Double { + self * rhs + } +} + +extension CGPoint: VectorArithmetic { + package var magnitude: Double { + hypot(abs(x), abs(y)) + } + + package func scaled(by rhs: Double) -> CGPoint { + CGPoint(x: x * rhs, y: y * rhs) + } +} + +extension CGVector: VectorArithmetic { + package var magnitude: Double { + hypot(abs(dx), abs(dy)) + } + + package func scaled(by rhs: Double) -> CGVector { + CGVector(dx: dx * rhs, dy: dy * rhs) + } +} + +// MARK: - VectorContaining + +package protocol VectorContaining { + associatedtype VectorType: VectorArithmetic + + var vector: VectorType { get set } +} + +// MARK: - VectorContaining Conformance + +extension Double: VectorContaining { + package typealias VectorType = Double + + package var vector: Double { + get { self } + set { self = newValue } + } +} + +extension CGPoint: VectorContaining { + package typealias VectorType = CGPoint + + package var vector: CGPoint { + get { self } + set { self = newValue } + } +} + +extension CGVector: VectorContaining { + package typealias VectorType = CGVector + + package var vector: CGVector { + get { self } + set { self = newValue } + } +} diff --git a/Tests/OpenGesturesTests/Util/AdditiveArithmeticTests.swift b/Tests/OpenGesturesTests/Util/AdditiveArithmeticTests.swift new file mode 100644 index 0000000..d6f3626 --- /dev/null +++ b/Tests/OpenGesturesTests/Util/AdditiveArithmeticTests.swift @@ -0,0 +1,63 @@ +// +// AdditiveArithmeticTests.swift +// OpenGesturesTests + +import OpenCoreGraphicsShims +import OpenGestures +import Testing + +@Suite +struct AdditiveArithmeticTests { + @Test + func doubleOperations() { + #expect(Double.zero == 0.0) + #expect(3.0 + 2.0 == 5.0) + #expect(3.0 - 2.0 == 1.0) + + var value = 4.0 + value += 1.5 + #expect(value == 5.5) + value -= 2.5 + #expect(value == 3.0) + } + + @Test + func pointOperations() { + #expect(CGPoint.zero == CGPoint(x: 0, y: 0)) + #expect( + CGPoint(x: 3, y: 4) + CGPoint(x: 1, y: 2) + == CGPoint(x: 4, y: 6) + ) + #expect( + CGPoint(x: 3, y: 4) - CGPoint(x: 1, y: 2) + == CGPoint(x: 2, y: 2) + ) + + var point = CGPoint(x: 2, y: 3) + point += CGPoint(x: 4, y: 5) + #expect(point == CGPoint(x: 6, y: 8)) + point -= CGPoint(x: 1, y: 1) + #expect(point == CGPoint(x: 5, y: 7)) + } + + @Test + func vectorAndSizeOperations() { + #expect( + CGVector(dx: 5, dy: 7) + CGVector(dx: 1, dy: 2) + == CGVector(dx: 6, dy: 9) + ) + #expect( + CGVector(dx: 5, dy: 7) - CGVector(dx: 1, dy: 2) + == CGVector(dx: 4, dy: 5) + ) + + #expect( + CGSize(width: 8, height: 6) + CGSize(width: 2, height: 1) + == CGSize(width: 10, height: 7) + ) + #expect( + CGSize(width: 8, height: 6) - CGSize(width: 2, height: 1) + == CGSize(width: 6, height: 5) + ) + } +} diff --git a/Tests/OpenGesturesTests/Util/RingBufferTests.swift b/Tests/OpenGesturesTests/Util/RingBufferTests.swift index 669de40..d41f5e1 100644 --- a/Tests/OpenGesturesTests/Util/RingBufferTests.swift +++ b/Tests/OpenGesturesTests/Util/RingBufferTests.swift @@ -2,7 +2,7 @@ // RingBufferTests.swift // OpenGesturesTests -@_spi(Private) import OpenGestures +import OpenGestures import Testing // MARK: - RingBufferTests diff --git a/Tests/OpenGesturesTests/Util/ThresholdAdjustableTests.swift b/Tests/OpenGesturesTests/Util/ThresholdAdjustableTests.swift new file mode 100644 index 0000000..2515e55 --- /dev/null +++ b/Tests/OpenGesturesTests/Util/ThresholdAdjustableTests.swift @@ -0,0 +1,19 @@ +// +// ThresholdAdjustableTests.swift +// OpenGesturesTests + +import OpenGestures +import Testing + +@Suite +struct ThresholdAdjustableTests { + @Test + func consumeThresholdForDouble() { + var value = 10.0 + + let consumed = value.consume(3.0, from: 8.0) + + #expect(consumed == 3.0) + #expect(value == 7.0) + } +} diff --git a/Tests/OpenGesturesTests/Util/VectorArithmeticTests.swift b/Tests/OpenGesturesTests/Util/VectorArithmeticTests.swift new file mode 100644 index 0000000..ebdb716 --- /dev/null +++ b/Tests/OpenGesturesTests/Util/VectorArithmeticTests.swift @@ -0,0 +1,31 @@ +// +// VectorArithmeticTests.swift +// OpenGesturesTests + +import OpenCoreGraphicsShims +import OpenGestures +import Testing + +@Suite +struct VectorArithmeticTests { + @Test + func doubleMagnitudeAndScaling() { + #expect(12.0.magnitude == 12.0) + #expect((-12.0).magnitude == 12.0) + #expect(12.0.scaled(by: 0.25) == 3.0) + } + + @Test + func pointMagnitudeAndScaling() { + let point = CGPoint(x: 3, y: 4) + #expect(point.magnitude == 5.0) + #expect(point.scaled(by: 2) == CGPoint(x: 6, y: 8)) + } + + @Test + func vectorMagnitudeAndScaling() { + let vector = CGVector(dx: 5, dy: 12) + #expect(vector.magnitude == 13.0) + #expect(vector.scaled(by: 0.5) == CGVector(dx: 2.5, dy: 6.0)) + } +}