diff --git a/Package.resolved b/Package.resolved index e555c9e..138db9c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/RougeWare/Swift-MultiplicativeArithmetic.git", "state": { "branch": null, - "revision": "38eee08151bbbefbbb422d02ff2c8da1a8b8bfe1", - "version": "1.3.0" + "revision": "c45199953ac680dfdcaefff5f8743f1126fe8a4e", + "version": "1.4.1" } } ] diff --git a/Package.swift b/Package.swift index ec09c36..161242b 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/RougeWare/Swift-MultiplicativeArithmetic.git", from: "1.0.0"), + .package(url: "https://github.com/RougeWare/Swift-MultiplicativeArithmetic.git", from: "1.4.1"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/RectangleTools/Basic Protocols/CartesianMeasurable.swift b/Sources/RectangleTools/Basic Protocols/CartesianMeasurable.swift new file mode 100644 index 0000000..b350df4 --- /dev/null +++ b/Sources/RectangleTools/Basic Protocols/CartesianMeasurable.swift @@ -0,0 +1,99 @@ +// +// CartesianMeasurable.swift +// +// +// Created by The Northstar✨ System on 2023-11-06. +// + +import Foundation + + + +/// Represents something that can be measured on a Cartesian coordinate plane +public protocol CartesianMeasurable { + + /// The type to use for measurements of length (x/y, width/height, etc.). + /// + /// This is typically a numeric type like `Int`, `Float64`, or `Decimal`. + associatedtype Length + + + + /// The lowest x value when measuring this + /// + /// This will always be less than or equal to ``maxX`` + /// + /// ``` + /// 6 │ + /// 5.0 maxY → 5 │ ╭─╮ + /// 4 │ ╰╮╰───┬╮ + /// 3 │ │ ╭╮ │╵ + /// 2.0 minY → 2 │ ╰─╯╰─╯ + /// 1 │ + /// 0 ┼──────────── + /// 0 1 2 3 4 5 6 + /// ↑ ↑ + /// minX maxX + /// 1.5 5.0 + /// ``` + var minX: Length { get } + + /// The lowest y value when measuring this + /// + /// This will always be less than or equal to ``maxY`` + /// + /// ``` + /// 6 │ + /// 5.0 maxY → 5 │ ╭─╮ + /// 4 │ ╰╮╰───┬╮ + /// 3 │ │ ╭╮ │╵ + /// 2.0 minY → 2 │ ╰─╯╰─╯ + /// 1 │ + /// 0 ┼──────────── + /// 0 1 2 3 4 5 6 + /// ↑ ↑ + /// minX maxX + /// 1.5 5.0 + /// ``` + var minY: Length { get } + + + + /// The highest x value when measuring this + /// + /// This will always be greater than or equal to ``minX`` + /// + /// ``` + /// 6 │ + /// 5.0 maxY → 5 │ ╭─╮ + /// 4 │ ╰╮╰───┬╮ + /// 3 │ │ ╭╮ │╵ + /// 2.0 minY → 2 │ ╰─╯╰─╯ + /// 1 │ + /// 0 ┼──────────── + /// 0 1 2 3 4 5 6 + /// ↑ ↑ + /// minX maxX + /// 1.5 5.0 + /// ``` + var maxX: Length { get } + + /// The highest y value when measuring this + /// + /// This will always be greater than or equal to ``minY`` + /// + /// ``` + /// 6 │ + /// 5.0 maxY → 5 │ ╭─╮ + /// 4 │ ╰╮╰───┬╮ + /// 3 │ │ ╭╮ │╵ + /// 2.0 minY → 2 │ ╰─╯╰─╯ + /// 1 │ + /// 0 ┼──────────── + /// 0 1 2 3 4 5 6 + /// ↑ ↑ + /// minX maxX + /// 1.5 5.0 + /// ``` + var maxY: Length { get } +} diff --git a/Sources/RectangleTools/Basic Protocols/DualTwoDimensional.swift b/Sources/RectangleTools/Basic Protocols/DualTwoDimensional.swift index bb19a61..68ed9d5 100644 --- a/Sources/RectangleTools/Basic Protocols/DualTwoDimensional.swift +++ b/Sources/RectangleTools/Basic Protocols/DualTwoDimensional.swift @@ -83,11 +83,3 @@ public extension DualTwoDimensional where Self: Rectangle { self.init(origin: firstDimensionPair, size: secondDimensionPair) } } - - - -// MARK: - Default conformances - -extension BinaryIntegerRectangle: DualTwoDimensional {} -extension DecimalRectangle: DualTwoDimensional {} -extension CGRect: DualTwoDimensional {} diff --git a/Sources/RectangleTools/Basic Protocols/Point2D.swift b/Sources/RectangleTools/Basic Protocols/Point2D.swift index 04a82d3..f7f4f8a 100644 --- a/Sources/RectangleTools/Basic Protocols/Point2D.swift +++ b/Sources/RectangleTools/Basic Protocols/Point2D.swift @@ -45,6 +45,8 @@ public protocol MutablePoint2D: Point2D, MutableTwoDimensional { // MARK: - Synthesis +// MARK: TwoDimensional + public extension Point2D { var measurementX: Length { diff --git a/Sources/RectangleTools/Basic Protocols/Rectangle.swift b/Sources/RectangleTools/Basic Protocols/Rectangle.swift index f4f82bb..87e5af2 100644 --- a/Sources/RectangleTools/Basic Protocols/Rectangle.swift +++ b/Sources/RectangleTools/Basic Protocols/Rectangle.swift @@ -10,8 +10,10 @@ import Foundation +// MARK: - Rectangle + /// A two-dimensional rectangle -public protocol Rectangle { +public protocol Rectangle: DualTwoDimensional, CartesianMeasurable { /// The unit in which the origin and size are defined associatedtype Length @@ -47,6 +49,8 @@ public protocol Rectangle { +// MARK: - Mutable Rectangle + /// A two-dimensional rectangle which can be mutated public protocol MutableRectangle: Rectangle where diff --git a/Sources/RectangleTools/Basic Protocols/Size2D.swift b/Sources/RectangleTools/Basic Protocols/Size2D.swift index 9d64949..43512c6 100644 --- a/Sources/RectangleTools/Basic Protocols/Size2D.swift +++ b/Sources/RectangleTools/Basic Protocols/Size2D.swift @@ -11,7 +11,7 @@ import Foundation /// A size in two dimensions -public protocol Size2D: TwoDimensional { +public protocol Size2D: TwoDimensional, CartesianMeasurable { /// The unit in which the size is defined associatedtype Length @@ -45,6 +45,8 @@ public protocol MutableSize2D: Size2D, MutableTwoDimensional { // MARK: - Synthesis +// MARK: General + public extension Size2D { var measurementX: Length { diff --git a/Sources/RectangleTools/Basic Protocols/TwoDimensional.swift b/Sources/RectangleTools/Basic Protocols/TwoDimensional.swift index ff032fb..4ba4f14 100644 --- a/Sources/RectangleTools/Basic Protocols/TwoDimensional.swift +++ b/Sources/RectangleTools/Basic Protocols/TwoDimensional.swift @@ -13,6 +13,9 @@ import Foundation /// Something which can be measured using only two dimensions public protocol TwoDimensional { + /// The type to use for measurements of length (x/y, width/height, etc.). + /// + /// This is typically a numeric type like `Int`, `Float64`, or `Decimal`. associatedtype Length diff --git a/Sources/RectangleTools/Fancy Protocols/Collection2D.swift b/Sources/RectangleTools/Fancy Protocols/Collection2D.swift new file mode 100644 index 0000000..a3d18f4 --- /dev/null +++ b/Sources/RectangleTools/Fancy Protocols/Collection2D.swift @@ -0,0 +1,212 @@ +// +// Integer Size Extensions.swift +// RectangleTools +// +// Created by Ben Leggiero on 2019-12-04. +// Copyright © 2019 Ben Leggiero BH-1-PS. +// + +import Foundation + + + +/// Allows you to treat an integer-size 2D object as a grid which scans from min-x/min-y to max-x/max-y in x scanlines. +/// +/// - Attention: Indices outside the bounds of the grid are treated as its last element (max-x, max-y) +public protocol Collection2D: Collection + where Index == Collection2DIndex +{ + associatedtype Length: BinaryInteger where Length.Stride: SignedInteger + associatedtype Element = Index + + + + /// Returns the last index which addresses a real element. + /// + /// Any indices after this are not in this grid and thus should not be addressed. + var lastValidIndex: Index { get } + + /// Returns the last element in this grid + var lastValidElement: Element { get } + + + /// Maps this grid to a 2D array of values, using the given transformer to generate them. + /// + /// This will scan the size from min-x/min-y to max-x/max-y in left-to-right horizontal scanlines. + /// + /// - Parameters: + /// - transformer: The function which will transform each element to the new value/type + /// - element: Each position in a scanline of this Size + /// - Returns: A 2D array of values as generated by the given transformer + /// - Throws: Anything the given transformer function throws + func map2D(_ transformer: (_ element: Element) throws -> ElementOfResult) rethrows -> [[ElementOfResult]] +} + + + +@available(*, renamed: "Collection2D", message: "Size2DCollection was removed in 2.12, replaced by the more generic and powerful `Collection2D`. The behavior is the same, just usable on more types") +public typealias Size2DCollection = Collection2D & Size2D + + + +/// This functions as a basic point index in a 2D collection. +/// +/// - Note: Though We wanted this to be a `BinaryIntegerPoint`, it can't be because it strictly acts as a point in a scanline pattern, whereas a `BinaryIntegerPoint` can act as any point in an integer plane. +public struct Collection2DIndex { + + /// The position within a row of the scanline in a 2D collection + public let x: Length + + /// The row of a scanline in a 2D collection + public let y: Length + + + /// Creates a new index for a 2D collection + /// + /// - Parameters: + /// - x: The position within a row of the scanline in a 2D collection + /// - y: The row of a scanline in a 2D collection + public init(x: Length, y: Length) { + self.x = x + self.y = y + } +} + + + +extension Collection2DIndex: Comparable { + public static func < (lhs: Collection2DIndex, rhs: Collection2DIndex) -> Bool { + lhs.y == rhs.y + ? lhs.x < rhs.x + : lhs.y < rhs.y + } +} + + + +extension Collection2DIndex: Point2D { +} + + + +public extension Collection2D + where Element == Index, + Self: CartesianMeasurable +{ + + var startIndex: Index { Index(x: minX, y: minY) } + var endIndex: Index { Index(x: maxX, y: maxY) } + var lastValidIndex: Index { Index(x: maxX - 1, y: maxY - 1) } + + var lastValidElement: Element { lastValidIndex } + + + func index(after i: Index) -> Index { + if i.y > lastValidIndex.y { + return endIndex + } + else if i.x >= lastValidIndex.x { + if i.y >= lastValidIndex.y { + return endIndex + } + else { + return Index(x: startIndex.x, y: i.y.advanced(by: 1)) + } + } + else { + return Index(x: i.x.advanced(by: 1), y: i.y) + } + } + + + subscript(position: Index) -> Element { + if position.x >= maxX + || position.y >= maxY { + return lastValidElement + } + else { + return position + } + } +} + + + +public extension Collection2D where Element == Index { + func map2D(_ transformer: (Element) throws -> ElementOfResult) rethrows -> [[ElementOfResult]] { + let startIndex = self.startIndex + let lastValidIndex = self.lastValidIndex + + return try (startIndex.y ... lastValidIndex.y).map { y in + try (startIndex.x ... lastValidIndex.x).map { x in + try transformer(.init(x: x, y: y)) + } + } + } +} + + + +// MARK: - Conforming `BinaryIntegerSize` to this + +extension BinaryIntegerSize: Sequence + where + Length: BinaryInteger, + Length.Stride: SignedInteger +{ + public typealias Iterator = IndexingIterator +} + + + +extension BinaryIntegerSize: Collection + where + Length: BinaryInteger, + Length.Stride: SignedInteger +{ + // Empty on-purpose; All witnesses synthesized +} + + + +extension BinaryIntegerSize: Collection2D + where + Length: BinaryInteger, + Length.Stride: SignedInteger +{ + public typealias Index = Collection2DIndex + public typealias Element = Index +} + + + +// MARK: - Conforming `BinaryIntegerRectangle` to this + +extension BinaryIntegerRectangle: Sequence + where + Length: BinaryInteger, + Length.Stride: SignedInteger +{ + public typealias Iterator = IndexingIterator +} + + + +extension BinaryIntegerRectangle: Collection + where + Length: BinaryInteger, + Length.Stride: SignedInteger +{ + // Empty on-purpose; All witnesses synthesized +} + + + +extension BinaryIntegerRectangle: Collection2D + where + Length: BinaryInteger, + Length.Stride: SignedInteger +{ + public typealias Index = Collection2DIndex + public typealias Element = Index +} diff --git a/Sources/RectangleTools/Synthesized Conveniences/Size2D Extensions.swift b/Sources/RectangleTools/Synthesized Conveniences/Size2D Extensions.swift index 4b8074b..596e016 100644 --- a/Sources/RectangleTools/Synthesized Conveniences/Size2D Extensions.swift +++ b/Sources/RectangleTools/Synthesized Conveniences/Size2D Extensions.swift @@ -23,6 +23,8 @@ public extension Size2D { +// MARK: - CartesianMeasurable + public extension Size2D where Length: Comparable, diff --git a/Sources/RectangleTools/Synthesized Conveniences/Size2DCollection.swift b/Sources/RectangleTools/Synthesized Conveniences/Size2DCollection.swift deleted file mode 100644 index 45313ec..0000000 --- a/Sources/RectangleTools/Synthesized Conveniences/Size2DCollection.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// Integer Size Extensions.swift -// RectangleTools -// -// Created by Ben Leggiero on 2019-12-04. -// Copyright © 2019 Ben Leggiero BH-1-PS. -// - -import Foundation - - - -/// Allows you to treat an integer size as a grid which scans from top-left to bottom-right in left-to-right horizontal -/// scanlines. -/// -/// - Attention: Indices outside the bounds of the size are treated as its last element (bottom-rightmost) -public protocol Size2DCollection: Collection, Size2D - where - Length: BinaryInteger, - Length.Stride: SignedInteger, - Index == Size2DCollectionIndex -{ - associatedtype Element = Size2DCollectionIndex - - - - /// Returns the index of the topmost-leftmost element in this size - var startIndex: Index { get } - - /// Returns the index of the bottommost-rightmost element in this size - var endIndex: Index { get } - - /// Returns the last index which addresses a real element. - /// - /// Any indices after this are not in this size and thus should not be addressed. - var lastValidIndex: Index { get } - - /// Returns the last element in this size - var lastValidElement: Element { get } - - - subscript(position: Index) -> Element { get } - - func index(after i: Index) -> Index - - - /// Maps this size to a 2D array of values, using the given transformer to generate them. - /// - /// This will scan the size from top-left to bottom-right in left-to-right horizontal scanlines. - /// - /// - Parameters: - /// - transformer: The function which will transform each element to the new value/type - /// - element: Each position in a scanline of this Size - /// - Returns: A 2D array of values as generated by the given transformer - /// - Throws: Anything the given transformer function changes - func map2D(_ transformer: (_ element: Element) throws -> ElementOfResult) rethrows -> [[ElementOfResult]] -} - - - -/// This functions as a basic point index in a 2D size collection. -/// -/// - Note: This can't be a `BinaryIntegerPoint` since it strictly acts as a point in a scanline pattern, whereas a -/// `BinaryIntegerPoint` can act as any point in an integer plane. -public struct Size2DCollectionIndex { - - /// The position within a row of the scanline in a Size - public let x: Length - - /// The row of a scanline in a Size - public let y: Length - - - /// Creates a new index for a `Size2D` - /// - /// - Parameters: - /// - x: The position within a row of the scanline in a Size - /// - y: The row of a scanline in a Size - public init(x: Length, y: Length) { - self.x = x - self.y = y - } -} - - - -extension Size2DCollectionIndex: Comparable { - public static func < (lhs: Size2DCollectionIndex, rhs: Size2DCollectionIndex) -> Bool { - return lhs.y < rhs.y - || (lhs.y == rhs.y - && lhs.x < rhs.x - ) - } -} - - - -extension Size2DCollectionIndex: Point2D { -} - - - -public extension Size2DCollection where Element == Index { - - var startIndex: Index { Index(x: 0, y: 0) } - var endIndex: Index { Index(x: width, y: height) } - var lastValidIndex: Index { Index(x: width - 1, y: height - 1) } - - var lastValidElement: Element { lastValidIndex } - - - func index(after i: Index) -> Index { - if i.y > lastValidIndex.y { - return endIndex - } - else if i.x >= lastValidIndex.x { - if i.y >= lastValidIndex.y { - return endIndex - } - else { - return Index(x: 0, y: i.y.advanced(by: 1)) - } - } - else { - return Index(x: i.x.advanced(by: 1), y: i.y) - } - } - - - subscript(position: Index) -> Element { - if position.x >= width - || position.y >= height { - return lastValidElement - } - else { - return position - } - } -} - - - -public extension Size2DCollection where Element == Index { - func map2D(_ transformer: (Element) throws -> ElementOfResult) rethrows -> [[ElementOfResult]] { - try (0.. -} - - - -extension BinaryIntegerSize: Collection - where - Length: BinaryInteger, - Length.Stride: SignedInteger -{ - // Empty on-purpose; All witnesses synthesized -} - - - -extension BinaryIntegerSize: Size2DCollection - where - Length: BinaryInteger, - Length.Stride: SignedInteger -{ - public typealias Index = Size2DCollectionIndex - public typealias Element = Index -} diff --git a/Tests/RectangleToolsTests/Collection2D Tests.swift b/Tests/RectangleToolsTests/Collection2D Tests.swift new file mode 100644 index 0000000..b5cc22d --- /dev/null +++ b/Tests/RectangleToolsTests/Collection2D Tests.swift @@ -0,0 +1,79 @@ +// +// Collection2DTests.swift +// +// +// Created by Ky Leggiero on 11/17/21. +// + +import XCTest +import RectangleTools + + + +final class Collection2DTests: XCTestCase { + + func testUIntSize() { + let threeByThree = UIntSize(width: 3, height: 3) + .map(IntPoint.init) + + XCTAssertEqual(threeByThree.count, 9) + XCTAssertEqual(threeByThree, [ + .init(x: 0, y: 0), + .init(x: 1, y: 0), + .init(x: 2, y: 0), + + .init(x: 0, y: 1), + .init(x: 1, y: 1), + .init(x: 2, y: 1), + + .init(x: 0, y: 2), + .init(x: 1, y: 2), + .init(x: 2, y: 2), + ]) + } + + func testUIntRect() { + let threeByThree = UIntRect(x: 0, y: 0, width: 3, height: 3) + .map(IntPoint.init) + + XCTAssertEqual(threeByThree.count, 9) + XCTAssertEqual(threeByThree, [ + .init(x: 0, y: 0), + .init(x: 1, y: 0), + .init(x: 2, y: 0), + + .init(x: 0, y: 1), + .init(x: 1, y: 1), + .init(x: 2, y: 1), + + .init(x: 0, y: 2), + .init(x: 1, y: 2), + .init(x: 2, y: 2), + ]) + + + let distantThreeByThree = UIntRect(x: 10, y: 10, width: 3, height: 3) + .map(IntPoint.init) + + XCTAssertEqual(distantThreeByThree.count, 9) + XCTAssertEqual(distantThreeByThree, [ + .init(x: 10, y: 10), + .init(x: 11, y: 10), + .init(x: 12, y: 10), + + .init(x: 10, y: 11), + .init(x: 11, y: 11), + .init(x: 12, y: 11), + + .init(x: 10, y: 12), + .init(x: 11, y: 12), + .init(x: 12, y: 12), + ]) + } + + + static let allTests = [ + ("testUIntSize", testUIntSize), + ("testUIntRect", testUIntRect), + ] +} diff --git a/Tests/RectangleToolsTests/Size Position Tests.swift b/Tests/RectangleToolsTests/Size Position Tests.swift index fd731e7..ba1cfc1 100644 --- a/Tests/RectangleToolsTests/Size Position Tests.swift +++ b/Tests/RectangleToolsTests/Size Position Tests.swift @@ -7,7 +7,7 @@ // import XCTest -@testable import RectangleTools +import RectangleTools