From dcb344ff467193e6673d7eca73033b0c68e24802 Mon Sep 17 00:00:00 2001 From: Ky Leggiero Date: Fri, 10 Nov 2023 17:35:12 -0700 Subject: [PATCH] Changed `Size2DCollection` to `Collection2D` And We did it without breaking existing code! Woo! This change makes that protocol more generic, so it can be applied to sizes, rectangles, anything which conforms. Part of this was removing the requirement for conforming types to also conform to `Size2D`, and creating a new conformance requirement: `CartesianMeasurable`. We then changed `Size2D` to require `CartesianMeasurable`, and that enabled this broadening. --- Package.resolved | 4 +- Package.swift | 2 +- .../Basic Protocols/CartesianMeasurable.swift | 99 ++++++++ .../Basic Protocols/DualTwoDimensional.swift | 8 - .../Basic Protocols/Point2D.swift | 2 + .../Basic Protocols/Rectangle.swift | 6 +- .../Basic Protocols/Size2D.swift | 4 +- .../Basic Protocols/TwoDimensional.swift | 3 + .../Fancy Protocols/Collection2D.swift | 212 ++++++++++++++++++ .../Size2D Extensions.swift | 2 + .../Size2DCollection.swift | 184 --------------- .../Collection2D Tests.swift | 79 +++++++ .../Size Position Tests.swift | 2 +- 13 files changed, 409 insertions(+), 198 deletions(-) create mode 100644 Sources/RectangleTools/Basic Protocols/CartesianMeasurable.swift create mode 100644 Sources/RectangleTools/Fancy Protocols/Collection2D.swift delete mode 100644 Sources/RectangleTools/Synthesized Conveniences/Size2DCollection.swift create mode 100644 Tests/RectangleToolsTests/Collection2D Tests.swift 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