├── Sources └── Deltille │ ├── Grid.swift │ ├── Scale.swift │ ├── Edge.swift │ ├── Extensions │ ├── Double.swift │ └── Vector.swift │ ├── Corner.swift │ ├── Rotation.swift │ ├── Vertex.swift │ ├── Axis.swift │ ├── Tile.swift │ ├── Coordinate.swift │ ├── Footprint.swift │ ├── Sieve.swift │ ├── Stencil.swift │ ├── Hexagon.swift │ └── Triangle.swift ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tests └── DeltilleTests │ ├── Extensions │ ├── Double.swift │ └── Vector.swift │ ├── Hexagon │ ├── HexagonFootprintTests.swift │ ├── HexagonTests.swift │ └── HexagonVertexTests.swift │ └── Triangle │ ├── TriangleFootprintTests.swift │ ├── TriangleVertexTests.swift │ └── TriangleTests.swift ├── Package.resolved ├── Package.swift ├── LICENSE.md ├── CHANGELOG.md └── README.md /Sources/Deltille/Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid.swift 3 | // 4 | // Created by Zack Brown on 23/05/2024. 5 | // 6 | 7 | // MARK: Grid 8 | 9 | public enum Grid { } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/DeltilleTests/Extensions/Double.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double.swift 3 | // 4 | // Created by Zack Brown on 25/05/2024. 5 | // 6 | 7 | extension Double { 8 | 9 | func isEqual(to other: Double, 10 | withPrecision p: Double) -> Bool { 11 | 12 | self == other || 13 | abs(self - other) < p 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Deltille/Scale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scale.swift 3 | // 4 | // Created by Zack Brown on 23/05/2024. 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: Scale 10 | 11 | public protocol Scale: CaseIterable, 12 | Codable, 13 | Hashable, 14 | Identifiable, 15 | Sendable { 16 | 17 | var edgeLength: Double { get } 18 | } 19 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "09dd89dedf6ced49eb81146d73a32b5e9604fccb82461142b02c8eae235bb493", 3 | "pins" : [ 4 | { 5 | "identity" : "euclid", 6 | "kind" : "remoteSourceControl", 7 | "location" : "git@github.com:nicklockwood/Euclid.git", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "1aeb8a7c36c22d31c74d4bd2c7e4529e3d43c097" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Deltille/Edge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Edge.swift 3 | // 4 | // Created by Zack Brown on 24/05/2024. 5 | // 6 | 7 | // MARK: Edge 8 | 9 | public protocol Edge: CaseIterable, 10 | Codable, 11 | Hashable, 12 | Identifiable, 13 | Sendable { 14 | 15 | associatedtype C = Corner 16 | 17 | var corners: [C] { get } 18 | 19 | var edges: [Self] { get } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Deltille/Extensions/Double.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double.swift 3 | // 4 | // Created by Zack Brown on 23/05/2024. 5 | // 6 | 7 | public extension Double { 8 | 9 | static let sqrt3 = 1.7320508076 // sqrt(3.0) 10 | static let sqrt3d2 = 0.8660254038 // sqrt(3.0) / 2.0 11 | static let sqrt3d3 = 0.5773502692 // sqrt(3.0) / 3.0 12 | static let sqrt3d6 = 0.2886751346 // sqrt(3.0) / 6.0 13 | 14 | static let tau = .pi * 2.0 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Deltille/Corner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Corner.swift 3 | // 4 | // Created by Zack Brown on 23/05/2024. 5 | // 6 | 7 | // MARK: Corner 8 | 9 | public protocol Corner: CaseIterable, 10 | Codable, 11 | Hashable, 12 | Identifiable, 13 | Sendable { 14 | 15 | associatedtype E = Edge 16 | 17 | var corners: [Self] { get } 18 | 19 | var edges: [E] { get } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/DeltilleTests/Extensions/Vector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Vector.swift 3 | // 4 | // Created by Zack Brown on 25/05/2024. 5 | // 6 | 7 | import Euclid 8 | 9 | extension Vector { 10 | 11 | func isEqual(to other: Vector, 12 | withPrecision p: Double = 1e-8) -> Bool { 13 | 14 | x.isEqual(to: other.x, withPrecision: p) && 15 | y.isEqual(to: other.y, withPrecision: p) && 16 | z.isEqual(to: other.z, withPrecision: p) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Deltille/Rotation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rotation.swift 3 | // 4 | // Created by Zack Brown on 24/05/2024. 5 | // 6 | 7 | // MARK: Rotation 8 | 9 | public protocol Rotation: CaseIterable, 10 | Codable, 11 | Hashable, 12 | Identifiable, 13 | Sendable { 14 | 15 | static var inverse: Double { get } 16 | static var step: Double { get } 17 | } 18 | 19 | // MARK: Rotatable 20 | 21 | public protocol Rotatable { 22 | 23 | associatedtype R = Rotation 24 | 25 | func rotate(_ rotation: R) -> Self 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Deltille/Vertex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Vertex.swift 3 | // 4 | // Created by Zack Brown on 27/11/2024. 5 | // 6 | 7 | import Euclid 8 | 9 | // MARK: Vertex 10 | 11 | public protocol Vertex: Codable, 12 | Hashable, 13 | Identifiable, 14 | Sendable { 15 | 16 | associatedtype S = Scale 17 | associatedtype T = Tile 18 | 19 | var position: Grid.Coordinate { get } 20 | 21 | var tiles: [T] { get } 22 | var vertices: [Self] { get } 23 | 24 | func position(_ scale: S) -> Vector 25 | } 26 | 27 | extension Vertex { 28 | 29 | public var id: String { position.id } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Deltille/Axis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Axis.swift 3 | // 4 | // Created by Zack Brown on 23/05/2024. 5 | // 6 | 7 | // MARK: Axis 8 | 9 | extension Grid { 10 | 11 | enum Axis: CaseIterable, 12 | Codable, 13 | Hashable, 14 | Identifiable, 15 | Sendable { 16 | 17 | case x, y, z 18 | 19 | public var id: String { unit.id } 20 | 21 | public var unit: Grid.Coordinate { 22 | 23 | switch self { 24 | 25 | case .x: .unitX 26 | case .y: .unitY 27 | case .z: .unitZ 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Deltille", 8 | platforms: [.macOS(.v14), 9 | .iOS(.v17)], 10 | products: [ 11 | .library(name: "Deltille", 12 | targets: ["Deltille"]), 13 | ], 14 | dependencies: [ 15 | .package(url: "git@github.com:nicklockwood/Euclid.git", 16 | branch: "main"), 17 | ], 18 | targets: [ 19 | .target(name: "Deltille", 20 | dependencies: ["Euclid"]), 21 | .testTarget(name: "DeltilleTests", 22 | dependencies: ["Deltille", 23 | "Euclid"]), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Zack Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Deltille/Tile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tile.swift 3 | // 4 | // Created by Zack Brown on 27/11/2024. 5 | // 6 | 7 | import Euclid 8 | 9 | // MARK: Tile 10 | 11 | public protocol Tile: Codable, 12 | Hashable, 13 | Identifiable, 14 | Sendable { 15 | 16 | associatedtype C = Corner 17 | associatedtype E = Edge 18 | associatedtype R = Rotation 19 | associatedtype S = Scale 20 | associatedtype V = Vertex 21 | 22 | var vertex: V { get } 23 | 24 | var vertices: [V] { get } 25 | var corners: [C] { get } 26 | var edges: [E] { get } 27 | 28 | var adjacent: [Self] { get } 29 | var perimeter: [Self] { get } 30 | 31 | func position(_ scale: S) -> Vector 32 | 33 | func rotate(_ rotation: R) -> Self 34 | 35 | func vertex(_ corner: C) -> V 36 | func corner(_ vertex: V) -> C? 37 | 38 | func neighbour(_ edge: E) -> Self 39 | 40 | func translation(_ along: E) -> Grid.Coordinate 41 | 42 | func contains(_ vector: Vector, 43 | _ scale: S) -> Bool 44 | 45 | func mesh(_ scale: S) -> Mesh 46 | 47 | func closest(_ vector: Vector, 48 | _ scale: S) -> V 49 | } 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## [0.2.0](https://github.com/zilmarinen/Deltille/releases/tag/0.2.0) (31/08/2025) 4 | 5 | - Introduce `Hexagon` type 6 | - Add `Scale` enum to define hexagon edge lengths 7 | 8 | - Introduce `Corner` protocol 9 | 10 | - Introduce `Edge` protocol 11 | 12 | - Introduce `Rotation` protocol 13 | 14 | - Introduce `Scale` protocol 15 | 16 | - Introduce `Tile` protocol 17 | 18 | - Introduce `Vertex` protocol 19 | 20 | - Introduce `Sieve` type for triangle subdivisions 21 | 22 | - Introduce `Hexagon` unit tests 23 | 24 | - Remove `Canopy` enum 25 | 26 | - Remove `Kite` enum 27 | 28 | - Remove `Septomino` enum 29 | 30 | ## [0.1.0](https://github.com/zilmarinen/Deltille/releases/tag/0.1.0) (27/05/2024) 31 | 32 | - Introduce `Grid` enum namespace 33 | - Add `Axis` enum to define world axis 34 | - Add `Scale` enum to define triangle edge lengths 35 | 36 | - Introduce `Coordinate` type 37 | - Add `Corner` enum 38 | - Add `Edge` enum 39 | - Add `Rotation` enum 40 | 41 | - Introduce `Triangle` type 42 | - Add `Stencil` type for triangle subdivisions 43 | 44 | - Introduce `Footprint` protocol 45 | - Add `Canopy` enum with wound edge perimeters 46 | - Add `Septomino` enum with known patterns 47 | 48 | - Introduce `Kite` enum 49 | - Add `Pattern` enum to define `Kite` tessellations 50 | 51 | - Introduce `Coordinate` unit tests 52 | - Introduce `Triangle` unit tests 53 | - Introduce `Footprint` unit tests 54 | -------------------------------------------------------------------------------- /Sources/Deltille/Coordinate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinate.swift 3 | // 4 | // Created by Zack Brown on 23/05/2024. 5 | // 6 | 7 | // MARK: Coordinate 8 | 9 | extension Grid { 10 | 11 | public struct Coordinate: Codable, 12 | Hashable, 13 | Identifiable, 14 | Sendable { 15 | 16 | public static let zero = Self(0, 0, 0) 17 | public static let one = Self(1, 1, 1) 18 | public static let unitX = Self(1, 0, 0) 19 | public static let unitY = Self(0, 1, 0) 20 | public static let unitZ = Self(0, 0, 1) 21 | 22 | public let x: Int 23 | public let y: Int 24 | public let z: Int 25 | 26 | public var id: String { "[\(x), \(y), \(z)]" } 27 | 28 | public var sum: Int { x + y + z } 29 | 30 | public var equalToZero: Bool { sum == 0 } 31 | public var equalToOne: Bool { sum == 1 } 32 | public var equalToNegativeOne: Bool { sum == -1 } 33 | 34 | public init(_ x: Int, 35 | _ y: Int, 36 | _ z: Int) { 37 | 38 | self.x = x 39 | self.y = y 40 | self.z = z 41 | } 42 | } 43 | } 44 | 45 | extension Grid.Coordinate { 46 | 47 | public static func -(lhs: Self, 48 | rhs: Self) -> Self { 49 | 50 | .init(lhs.x - rhs.x, 51 | lhs.y - rhs.y, 52 | lhs.z - rhs.z) 53 | } 54 | 55 | public static func +(lhs: Self, 56 | rhs: Self) -> Self { 57 | 58 | .init(lhs.x + rhs.x, 59 | lhs.y + rhs.y, 60 | lhs.z + rhs.z) 61 | } 62 | 63 | public static prefix func -(rhs: Self) -> Self { 64 | 65 | .init(-rhs.x, 66 | -rhs.y, 67 | -rhs.z) 68 | } 69 | 70 | public static func *(lhs: Self, 71 | rhs: Int) -> Self { 72 | 73 | .init(lhs.x * rhs, 74 | lhs.y * rhs, 75 | lhs.z * rhs) } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Deltille/Extensions/Vector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Vector.swift 3 | // 4 | // Created by Zack Brown on 24/05/2024. 5 | // 6 | 7 | import Euclid 8 | import Foundation 9 | 10 | // MARK: Vector 11 | 12 | extension Vector: @retroactive Identifiable { 13 | 14 | public var id: String { "[\(x), \(y), \(z)]" } 15 | } 16 | 17 | extension Vector { 18 | 19 | public func mid(_ lhs: Self) -> Self { 20 | 21 | lerp(lhs, 0.5) 22 | } 23 | } 24 | 25 | extension Array where Element == Vector { 26 | 27 | public func firstIndexOf(closest vector: Vector) -> Int { 28 | 29 | var distance = Double.greatestFiniteMagnitude 30 | var closestIndex = 0 31 | 32 | for index in indices { 33 | 34 | let other = self[index] 35 | 36 | let length = (other - vector).length 37 | 38 | if length < distance { 39 | 40 | closestIndex = index 41 | 42 | distance = length 43 | } 44 | } 45 | 46 | return closestIndex 47 | } 48 | } 49 | 50 | // MARK: Hexagon 51 | 52 | extension Vector { 53 | 54 | public init(_ vertex: Grid.Hexagon.Vertex, 55 | _ scale: Grid.Hexagon.Scale) { 56 | 57 | let dx = Double(vertex.position.x) 58 | let dy = Double(vertex.position.y) 59 | let dz = Double(vertex.position.z) 60 | 61 | self.init(((.sqrt3d2 * dy) - (.sqrt3d2 * dz)) * scale.edgeLength, 62 | 0.0, 63 | (dx - 0.5 * dy - 0.5 * dz) * scale.edgeLength) 64 | } 65 | } 66 | 67 | // MARK: Triangle 68 | 69 | extension Vector { 70 | 71 | public init(_ vertex: Grid.Triangle.Vertex, 72 | _ scale: Grid.Triangle.Scale) { 73 | 74 | let dx = Double(vertex.position.y) 75 | let dy = Double(vertex.position.x) 76 | let dz = Double(vertex.position.z) 77 | 78 | self.init((dx - 0.5 * dy - 0.5 * dz) * scale.edgeLength, 79 | 0.0, 80 | ((.sqrt3d2 * dy) - (.sqrt3d2 * dz)) * scale.edgeLength) 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /Sources/Deltille/Footprint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Footprint.swift 3 | // 4 | // Created by Zack Brown on 24/05/2024. 5 | // 6 | 7 | import Euclid 8 | 9 | // MARK: Footprint 10 | 11 | public class Footprint: Codable, 15 | Hashable, 16 | Rotatable where T.R == R, 17 | T.S == S, 18 | T.V == V { 19 | 20 | public let origin: T 21 | public let tiles: [T] 22 | 23 | public required init(_ origin: T, 24 | _ tiles: [T]) { 25 | 26 | self.origin = origin 27 | self.tiles = tiles 28 | } 29 | 30 | open func rotate(_ rotation: R) -> Self { self } 31 | } 32 | 33 | extension Footprint { 34 | 35 | public var perimeter: [T] { 36 | 37 | Array(Set(tiles.flatMap { $0.perimeter })) 38 | } 39 | 40 | public var vertices: [V] { 41 | 42 | Array(Set(tiles.flatMap { $0.vertices })) 43 | } 44 | } 45 | 46 | extension Footprint { 47 | 48 | public func hash(into hasher: inout Hasher) { 49 | 50 | hasher.combine(origin) 51 | hasher.combine(tiles) 52 | } 53 | 54 | public static func == (lhs: Footprint, 55 | rhs: Footprint) -> Bool { 56 | 57 | lhs.origin == rhs.origin && 58 | lhs.tiles == rhs.tiles 59 | } 60 | 61 | public func center(_ scale: S) -> Vector { 62 | 63 | let vector = tiles.reduce(into: Vector.zero) { result, tile in 64 | 65 | result += tile.position(scale) 66 | } 67 | 68 | return vector / Double(tiles.count) 69 | } 70 | 71 | public func intersects(_ footprint: Footprint) -> Bool { 72 | 73 | for tile in footprint.tiles { 74 | 75 | guard !intersects(tile) else { return true } 76 | } 77 | 78 | return intersects(footprint.origin) 79 | } 80 | 81 | public func intersects(_ tile: T) -> Bool { 82 | 83 | tiles.contains(tile) || 84 | tile == origin 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/DeltilleTests/Hexagon/HexagonFootprintTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HexagonFootprintTests.swift 3 | // 4 | // Created by Zack Brown on 19/12/2024. 5 | // 6 | 7 | import Euclid 8 | import XCTest 9 | @testable import Deltille 10 | 11 | final class HexagonFootprintTests: XCTestCase { 12 | 13 | typealias Coordinate = Grid.Coordinate 14 | typealias Hexagon = Grid.Hexagon 15 | typealias Footprint = Hexagon.Footprint 16 | 17 | private let hexagon = Hexagon(Coordinate(1, -2, 1)) 18 | 19 | private let offsets: [Coordinate] = [.init(0, 0, 0), 20 | .init(1, 0, -1), 21 | .init(2, 0, -2), 22 | .init(1, 1, -2)] 23 | 24 | // MARK: Translation 25 | 26 | func testFootprintTranslation() throws { 27 | 28 | let footprint = Footprint(hexagon, 29 | offsets) 30 | 31 | let tiles: [Hexagon] = [.init(1, -2, 1), 32 | .init(2, -2, 0), 33 | .init(3, -2, -1), 34 | .init(2, -1, -1)] 35 | 36 | XCTAssertEqual(footprint.tiles, 37 | tiles) 38 | } 39 | 40 | // MARK: Rotation 41 | 42 | func testFootprintRotation() throws { 43 | 44 | let footprint = Footprint(hexagon, 45 | offsets) 46 | 47 | let clockwiseRotation = footprint.rotate(.clockwise) 48 | let counterClockwiseRotation = footprint.rotate(.counterClockwise) 49 | 50 | let clockwiseTiles: [Hexagon] = [.init(1, -2, 1), 51 | .init(2, -3, 1), 52 | .init(3, -4, 1), 53 | .init(3, -3, 0)] 54 | 55 | let counterClockwiseTiles: [Hexagon] = [.init(1, -2, 1), 56 | .init(1, -1, 0), 57 | .init(1, 0, -1), 58 | .init(0, 0, 0)] 59 | 60 | XCTAssertEqual(clockwiseRotation.tiles, 61 | clockwiseTiles) 62 | XCTAssertEqual(counterClockwiseRotation.tiles, 63 | counterClockwiseTiles) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/DeltilleTests/Hexagon/HexagonTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HexagonTests.swift 3 | // 4 | // Created by Zack Brown on 12/06/2024. 5 | // 6 | 7 | import Euclid 8 | import XCTest 9 | @testable import Deltille 10 | 11 | final class HexagonTests: XCTestCase { 12 | 13 | typealias Coordinate = Grid.Coordinate 14 | typealias Hexagon = Grid.Hexagon 15 | typealias Vertex = Hexagon.Vertex 16 | 17 | private let hexagon = Hexagon(Coordinate(2, -1, -1)) 18 | 19 | // MARK: Contains Vector 20 | 21 | func testHexagonContainsVector() throws { 22 | 23 | XCTAssertTrue(hexagon.contains(hexagon.position(.tile), 24 | .tile)) 25 | XCTAssertTrue(hexagon.contains(hexagon.vertex(.c0).position(.tile), 26 | .tile)) 27 | XCTAssertTrue(hexagon.contains(hexagon.vertex(.c1).position(.tile), 28 | .tile)) 29 | XCTAssertTrue(hexagon.contains(hexagon.vertex(.c1).position(.tile), 30 | .tile)) 31 | XCTAssertTrue(hexagon.contains(hexagon.vertex(.c2).position(.tile), 32 | .tile)) 33 | XCTAssertTrue(hexagon.contains(hexagon.vertex(.c3).position(.tile), 34 | .tile)) 35 | XCTAssertTrue(hexagon.contains(hexagon.vertex(.c4).position(.tile), 36 | .tile)) 37 | 38 | XCTAssertFalse(hexagon.contains(.zero, .tile)) 39 | } 40 | 41 | // MARK: Neighbours / Adjacency / Perimeter 42 | 43 | func testNeighbours() throws { 44 | 45 | let tiles: [Hexagon] = [.init(3, -1, -2), 46 | .init(2, 0, -2), 47 | .init(1, 0, -1), 48 | .init(1, -1, 0), 49 | .init(2, -2, 0), 50 | .init(3, -2, -1)] 51 | 52 | let neighbours = hexagon.edges.map { hexagon.neighbour($0) } 53 | 54 | XCTAssertEqual(neighbours, tiles) 55 | XCTAssertEqual(hexagon.adjacent, tiles) 56 | XCTAssertEqual(hexagon.perimeter, tiles) 57 | } 58 | 59 | // MARK: Vertices / Corners 60 | 61 | func testVertices() throws { 62 | 63 | let vertices: [Vertex] = [.init(3, -1, -1), 64 | .init(2, -1, -2), 65 | .init(2, 0, -1), 66 | .init(1, -1, -1), 67 | .init(2, -1, 0), 68 | .init(2, -2, -1)] 69 | 70 | let hexagonCorners = vertices.map { hexagon.corner($0) } 71 | let hexagonVertices = hexagon.corners.map { hexagon.vertex($0) } 72 | 73 | XCTAssertEqual(hexagonCorners, hexagon.corners) 74 | XCTAssertEqual(hexagonVertices, vertices) 75 | XCTAssertEqual(hexagonVertices, hexagon.vertices) 76 | XCTAssertEqual(nil, hexagon.corner(.zero)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Deltille/Sieve.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sieve.swift 3 | // 4 | // Created by Zack Brown on 01/07/2024. 5 | // 6 | 7 | import Euclid 8 | import Foundation 9 | 10 | // MARK: Sieve 11 | 12 | extension Grid.Triangle { 13 | 14 | /// 15 | /// A sieve subdivides a triangle of a given scale into 16 | /// a set of smaller, inner triangles and their vertices. 17 | /// 18 | /// v-------v-------v-------v-------v 19 | /// \ t / t \ t / t \ t / t \ t / 20 | /// v-------v-------v-------v 21 | /// \ t / t \ t / t \ t / 22 | /// v-------v-------v 23 | /// \ t / t \ t / 24 | /// v-------v 25 | /// \ t / 26 | /// v 27 | /// 28 | 29 | public struct Sieve { 30 | 31 | public let origin: Grid.Triangle 32 | public let scale: Grid.Triangle.Scale 33 | public let triangles: [Grid.Triangle] 34 | public let vertices: [Grid.Triangle.Vertex] 35 | 36 | public init(_ origin: Grid.Triangle, 37 | _ scale: Grid.Triangle.Scale, 38 | _ triangles: [Grid.Triangle], 39 | _ vertices: [Grid.Triangle.Vertex]) { 40 | 41 | self.origin = origin 42 | self.scale = scale 43 | self.triangles = triangles 44 | self.vertices = vertices 45 | } 46 | } 47 | 48 | public func sieve(for scale: Scale) -> Sieve { 49 | 50 | let origin = Grid.Triangle(vertex.position(scale), 51 | .tile) 52 | 53 | let columns = Int(max(scale.edgeLength, 1.0)) 54 | let base = Int(floor(Double(columns) / 1.5)) 55 | let half = Int(floor(Double(base) / 2.0)) 56 | let pointy = isPointy 57 | 58 | var triangles: [Grid.Triangle] = [] 59 | var vertices: [Vertex] = [] 60 | 61 | for column in 0...columns { 62 | 63 | let rows = columns - column 64 | 65 | let x = half - column 66 | 67 | for row in 0...rows { 68 | 69 | let y = half - row 70 | let z = base + 1 - column - row 71 | 72 | let other = Grid.Triangle.Vertex(pointy ? -x : x + 1, 73 | pointy ? -y : y + 1, 74 | pointy ? z : -z + 1) 75 | 76 | vertices.append(.init(origin.vertex.position + other.position)) 77 | 78 | guard row != rows else { continue } 79 | 80 | let lhs = Grid.Coordinate(pointy ? -x : x, 81 | pointy ? -y : y, 82 | pointy ? z - 1 : -z + 1) 83 | 84 | triangles.append(.init(origin.vertex.position + lhs)) 85 | 86 | guard row < (rows - 1) else { continue } 87 | 88 | let rhs = Grid.Coordinate(pointy ? -x : x, 89 | pointy ? -y : y, 90 | pointy ? z - 2 : -z + 2) 91 | 92 | triangles.append(.init(origin.vertex.position + rhs)) 93 | } 94 | } 95 | 96 | return .init(.init(vertex.position), 97 | scale, 98 | triangles, 99 | vertices) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/Deltille/Stencil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stencil.swift 3 | // 4 | // Created by Zack Brown on 24/05/2024. 5 | // 6 | 7 | import Euclid 8 | 9 | // MARK: Stencil 10 | 11 | extension Grid.Triangle { 12 | 13 | /// 14 | /// Stencil defines a fixed set of points for the corners, edges 15 | /// and interior subdivisions of a triangle and its center. 16 | /// 17 | /// 0-------3-------5-------8-------1 18 | /// \ / \ / \ / \ / 19 | /// 4-------6-------9-------12 20 | /// \ / \ c / \ / 21 | /// 7------10-------13 22 | /// \ / \ / 23 | /// 11-------14 24 | /// \ / 25 | /// 2 26 | /// 27 | 28 | public struct Stencil { 29 | 30 | public enum Vertex: CaseIterable, 31 | Sendable { 32 | 33 | case v0, v1, v2 34 | case v5, v7, v13 35 | case v6, v9, v10 36 | case v3, v4, v8, v11, v12, v14 37 | case center 38 | } 39 | 40 | // Subdivided triangles 41 | public static let triangles: [[Vertex]] = [ 42 | 43 | [.v0, .v3, .v4], 44 | [.v3, .v6, .v4], 45 | [.v3, .v5, .v6], 46 | [.v5, .v9, .v6], 47 | [.v5, .v8, .v9], 48 | [.v8, .v12, .v9], 49 | [.v8, .v1, .v12], 50 | [.v4, .v6, .v7], 51 | [.v6, .v10, .v7], 52 | [.v6, .v9, .v10], 53 | [.v9, .v13, .v10], 54 | [.v9, .v12, .v13], 55 | [.v7, .v10, .v11], 56 | [.v10, .v14, .v11], 57 | [.v10, .v13, .v14], 58 | [.v11, .v14, .v2] 59 | ] 60 | 61 | // Triangle corners 62 | public let v0, v1, v2: Vector 63 | 64 | // Edge midpoints 65 | public let v5, v7, v13: Vector 66 | 67 | // Inner subdivision 68 | public let v6, v9, v10: Vector 69 | 70 | // Outer subdivisions 71 | public let v3, v4, v8, v11, v12, v14: Vector 72 | 73 | public let scale: Scale 74 | 75 | public var center: Vector { (v0 + v1 + v2) / 3.0 } 76 | 77 | public func vertex(_ vertex: Vertex) -> Vector { 78 | 79 | switch vertex { 80 | 81 | case .v0: v0 82 | case .v1: v1 83 | case .v2: v2 84 | case .v3: v3 85 | case .v4: v4 86 | case .v5: v5 87 | case .v6: v6 88 | case .v7: v7 89 | case .v8: v8 90 | case .v9: v9 91 | case .v10: v10 92 | case .v11: v11 93 | case .v12: v12 94 | case .v13: v13 95 | case .v14: v14 96 | case .center: center 97 | } 98 | } 99 | } 100 | 101 | public func stencil(_ scale: Scale) -> Stencil { 102 | 103 | let v0 = Vector(vertex(.c0), 104 | scale) 105 | let v1 = Vector(vertex(.c1), 106 | scale) 107 | let v2 = Vector(vertex(.c2), 108 | scale) 109 | 110 | let v5 = v0.mid(v1) 111 | let v7 = v0.mid(v2) 112 | let v13 = v1.mid(v2) 113 | 114 | return .init(v0: v0, 115 | v1: v1, 116 | v2: v2, 117 | v5: v5, 118 | v7: v7, 119 | v13: v13, 120 | v6: v5.mid(v7), 121 | v9: v5.mid(v13), 122 | v10: v7.mid(v13), 123 | v3: v0.mid(v5), 124 | v4: v0.mid(v7), 125 | v8: v1.mid(v5), 126 | v11: v2.mid(v7), 127 | v12: v1.mid(v13), 128 | v14: v2.mid(v13), 129 | scale: scale) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Platforms](https://img.shields.io/badge/platforms-iOS%20|%20Mac-lightgray.svg)]() 2 | [![Swift 5.1](https://img.shields.io/badge/swift-5.1-red.svg?style=flat)](https://developer.apple.com/swift) 3 | [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-red?style=flat)](https://www.swift.org/documentation/package-manager/) 4 | [![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://opensource.org/licenses/MIT) 5 | 6 | - [Introduction](#deltille) 7 | - [Installation](#installation) 8 | - [Implementation](#implementation) 9 | - [Examples](#examples) 10 | - [Credits](#credits) 11 | 12 | # Deltille 13 | Deltille is a Swift library for working with hexagonal and triangular grid systems. It provides robust data structures and utilities to make grid based development simple, accurate and efficient. 14 | 15 | Hexagonal and triangular grids are deeply related through duality: 16 | 17 | - Every hexagonal grid can be subdivided into equilateral triangles forming a triangular grid. 18 | - Conversely; The dual graph of a triangular grid is a hexagonal grid and vice versa. 19 | - This makes them complementary representations of the same underlying space. You can often solve problems easily by switching between them. 20 | 21 | By supporting both grid types in a unified API, Deltille lets you take advantage of this relationship making it easier to build flexible systems for geometry, navigation, or procedural generation. 22 | 23 | ## Features 24 | - Hexagonal & Triangular Grids: Supports both coordinate systems. 25 | - Coordinate Conversion: Easily convert between 2D Cartesian coordinates and 3D hexagonal grid coordinates. 26 | - Neighbour & Vertex Navigation: Convenient methods for traversing edge neighbours and corner vertices. 27 | - Scaling & Transformations: Built-in support for scaling grid space and handling grid math such as subdivision and rotation. 28 | - Composable API: Designed to integrate cleanly into games, simulations and visualization tools. 29 | 30 | # Installation 31 | To install using Swift Package Manager, add this to the `dependencies:` section in your Package.swift file: 32 | 33 | ```swift 34 | .package(url: "https://github.com/zilmarinen/Deltille.git", branch: "main"), 35 | ``` 36 | 37 | ## Dependencies 38 | [Euclid](https://github.com/nicklockwood/Euclid) is a Swift library for creating and manipulating 3D geometry and is used extensively within this project for mesh generation and vector operations. 39 | 40 | ## License 41 | 42 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 43 | 44 | # Implementation 45 | Deltille defines a `Tile` archetype of which both `Triangle` and `Hexagon` conform to provide high level utility methods for grid based operations. 46 | 47 | ## Triangles & Hexagons 48 | The basic building blocks of Deltille are both the `Triangle` and `Hexagon` `Tile` types which are used together to model vertex positions along the `xz` plane. 49 | 50 | ```swift 51 | // MARK: Triangle 52 | let triangle = Grid.Triangle(.zero) 53 | 54 | //generate triangle vertices for the desired scale 55 | let vertices = triangle.vertices.map { $0.position(.tile) } 56 | 57 | // MARK: Hexagon 58 | let hexagon = Grid.Hexagon(.zero) 59 | 60 | //generate hexagon vertices for the desired scale 61 | let vertices = hexagon.vertices.map { $0.position(.tile) } 62 | ``` 63 | 64 | ## Stencils & Sieves 65 | Both a `Stencil` or `Sieve` can be used to subdivide a triangle into individual sub-triangles mapped to a specific grid scale. 66 | - `Stencil` defines a discrete set of subdivided triangles and their vertices in world space. 67 | - `Sieve` calculates a dynamic range of subdivided triangles and their vertices in grid space. 68 | 69 | ```swift 70 | // MARK: Stencil 71 | let stencil = triangle.stencil(.tile) 72 | 73 | let triangles = stencil.triangles 74 | 75 | //MARK: Sieve 76 | let sieve = triangle.sieve(for: .chunk) 77 | 78 | let triangles = sieve.triangles 79 | ``` 80 | 81 | ## Footprints 82 | A `Footprint` defines a collection of `Tile` types centered around a given origin. 83 | 84 | ```swift 85 | //MARK: Footprint 86 | let coordinates: [Grid.Coordinate] = [.zero, 87 | -.unitX, 88 | -.unitY, 89 | -.unitZ] 90 | 91 | let footprint = Grid.Triangle.Footprint(.zero, 92 | coordinates) 93 | 94 | //rotate footprint around its origin 95 | let rotated = footprint.rotate(.clockwise) 96 | ``` 97 | 98 | # Examples 99 | [Regolith](https://github.com/zilmarinen/Regolith/) makes use of the concepts introduced by Deltille to generate meshes for predefined tessellations of a triangle interior using [Ortho-Tiling](https://www.boristhebrave.com/2023/05/31/ortho-tiles/). 100 | 101 | [Verdure](https://github.com/zilmarinen/Verdure/) implements additional mesh generation on top of Deltille to create stylised foliage canopies constrained to a triangular grid. 102 | 103 | # Credits 104 | 105 | The Deltille framework is primarily the work of [Zack Brown](https://github.com/zilmarinen). 106 | 107 | Special thanks go to; 108 | 109 | - [Boris the Brave](https://www.boristhebrave.com) for his extensive articles on grid systems, dual contouring, marching cubes, ortho-tiling and so much more. 110 | - [Oskar Stalberg](https://t.co/qakKgmxfai) for inspiring posts on procedural generation, wave function collapse and dual grid systems. 111 | - [Amit Patel](https://www.redblobgames.com) for stimulating deep dives into hexagonal grids, coordinate systems, grid edge classifications and graph theory. 112 | -------------------------------------------------------------------------------- /Tests/DeltilleTests/Triangle/TriangleFootprintTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TriangleFootprintTests.swift 3 | // 4 | // Created by Zack Brown on 25/05/2024. 5 | // 6 | 7 | import Euclid 8 | import XCTest 9 | @testable import Deltille 10 | 11 | final class TriangleFootprintTests: XCTestCase { 12 | 13 | typealias Coordinate = Grid.Coordinate 14 | typealias Triangle = Grid.Triangle 15 | typealias Footprint = Triangle.Footprint 16 | 17 | private let pointyTriangle = Triangle(1, -2, 1) 18 | private let flatTriangle = Triangle(2, -1, -2) 19 | 20 | private let offsets: [Coordinate] = [.init(0, 0, 0), 21 | .init(0, 0, -1), 22 | .init(1, 0, -1), 23 | .init(1, 0, -2), 24 | .init(1, 1, -2), 25 | .init(1, 1, -3), 26 | .init(2, 1, -3)] 27 | 28 | // MARK: Translation 29 | 30 | func testFootprintTranslationPointy() throws { 31 | 32 | let footprint = Footprint(pointyTriangle, 33 | offsets) 34 | 35 | let tiles: [Triangle] = [.init(1, -2, 1), 36 | .init(1, -2, 0), 37 | .init(2, -2, 0), 38 | .init(2, -2, -1), 39 | .init(2, -1, -1), 40 | .init(2, -1, -2), 41 | .init(3, -1, -2)] 42 | 43 | XCTAssertEqual(footprint.tiles, 44 | tiles) 45 | } 46 | 47 | func testFootprintTranslationFlat() throws { 48 | 49 | let footprint = Footprint(flatTriangle, 50 | offsets) 51 | 52 | let tiles: [Triangle] = [.init(2, -1, -2), 53 | .init(2, -1, -1), 54 | .init(1, -1, -1), 55 | .init(1, -1, 0), 56 | .init(1, -2, 0), 57 | .init(1, -2, 1), 58 | .init(0, -2, 1)] 59 | 60 | XCTAssertEqual(footprint.tiles, 61 | tiles) 62 | } 63 | 64 | // MARK: Rotation 65 | 66 | func testFootprintRotationPointy() throws { 67 | 68 | let footprint = Footprint(pointyTriangle, 69 | offsets) 70 | 71 | let clockwiseRotation = footprint.rotate(.clockwise) 72 | let counterClockwiseRotation = footprint.rotate(.counterClockwise) 73 | 74 | let clockwiseTiles: [Triangle] = [.init(1, -2, 1), 75 | .init(1, -3, 1), 76 | .init(1, -3, 2), 77 | .init(1, -4, 2), 78 | .init(2, -4, 2), 79 | .init(2, -5, 2), 80 | .init(2, -5, 3)] 81 | 82 | let counterClockwiseTiles: [Triangle] = [.init(1, -2, 1), 83 | .init(0, -2, 1), 84 | .init(0, -1, 1), 85 | .init(-1, -1, 1), 86 | .init(-1, -1, 2), 87 | .init(-2, -1, 2), 88 | .init(-2, 0, 2)] 89 | 90 | XCTAssertEqual(clockwiseRotation.tiles, 91 | clockwiseTiles) 92 | XCTAssertEqual(counterClockwiseRotation.tiles, 93 | counterClockwiseTiles) 94 | } 95 | 96 | func testFootprintRotationFlat() throws { 97 | 98 | let footprint = Footprint(flatTriangle, 99 | offsets) 100 | 101 | let clockwiseRotation = footprint.rotate(.clockwise) 102 | let counterClockwiseRotation = footprint.rotate(.counterClockwise) 103 | 104 | let clockwiseTiles: [Triangle] = [.init(2, -1, -2), 105 | .init(2, 0, -2), 106 | .init(2, 0, -3), 107 | .init(2, 1, -3), 108 | .init(1, 1, -3), 109 | .init(1, 2, -3), 110 | .init(1, 2, -4)] 111 | 112 | let counterClockwiseTiles: [Triangle] = [.init(2, -1, -2), 113 | .init(3, -1, -2), 114 | .init(3, -2, -2), 115 | .init(4, -2, -2), 116 | .init(4, -2, -3), 117 | .init(5, -2, -3), 118 | .init(5, -3, -3)] 119 | 120 | XCTAssertEqual(clockwiseRotation.tiles, 121 | clockwiseTiles) 122 | XCTAssertEqual(counterClockwiseRotation.tiles, 123 | counterClockwiseTiles) 124 | } 125 | 126 | // MARK: Intersection 127 | 128 | func testFootprintIntersection() throws { 129 | 130 | let footprint = Footprint(.zero, 131 | [Coordinate.zero]) 132 | 133 | let lhs = Footprint(pointyTriangle, 134 | offsets) 135 | 136 | let rhs = Footprint(flatTriangle, 137 | offsets) 138 | 139 | XCTAssertFalse(footprint.intersects(lhs)) 140 | XCTAssertFalse(footprint.intersects(rhs)) 141 | XCTAssertTrue(lhs.intersects(rhs)) 142 | XCTAssertTrue(rhs.intersects(lhs)) 143 | } 144 | } 145 | 146 | -------------------------------------------------------------------------------- /Tests/DeltilleTests/Hexagon/HexagonVertexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HexagonVertexTests.swift 3 | // 4 | // Created by Zack Brown on 09/12/2024. 5 | // 6 | 7 | import Euclid 8 | import XCTest 9 | @testable import Deltille 10 | 11 | final class HexagonVertexTests: XCTestCase { 12 | 13 | typealias Coordinate = Grid.Coordinate 14 | typealias Hexagon = Grid.Hexagon 15 | typealias Vertex = Hexagon.Vertex 16 | 17 | private let vertex = Vertex(.init(2, -2, 1)) 18 | 19 | // MARK: Tiles 20 | 21 | func testVertexTiles() throws { 22 | 23 | let tiles: [Hexagon] = [.init(1, -2, 1), 24 | .init(2, -3, 1), 25 | .init(2, -2, 0)] 26 | 27 | XCTAssertEqual(vertex.tiles, tiles) 28 | } 29 | 30 | // MARK: Vertices 31 | 32 | func testVertexVertices() throws { 33 | 34 | let vertices: [Vertex] = [.init(2, -3, 0), 35 | .init(1, -2, 0), 36 | .init(1, -3, 1)] 37 | 38 | XCTAssertEqual(vertex.vertices, vertices) 39 | } 40 | 41 | // MARK: Vertex to Vector 42 | 43 | func testVertexToVectorTile() throws { 44 | 45 | XCTAssertTrue(testVertexToVector(.tile)) 46 | } 47 | 48 | func testVertexToVectorChunk() throws { 49 | 50 | XCTAssertTrue(testVertexToVector(.chunk)) 51 | } 52 | 53 | func testVertexToVectorRegion() throws { 54 | 55 | XCTAssertTrue(testVertexToVector(.region)) 56 | } 57 | 58 | // MARK: Vector to Hexagon 59 | 60 | func testVectorToHexagonTiile() throws { 61 | 62 | XCTAssertTrue(testVectorToHexagon(.tile)) 63 | } 64 | 65 | func testVectorToHexagonChunk() throws { 66 | 67 | XCTAssertTrue(testVectorToHexagon(.chunk)) 68 | } 69 | 70 | func testVectorToHexagonRegion() throws { 71 | 72 | XCTAssertTrue(testVectorToHexagon(.region)) 73 | } 74 | 75 | // MARK: Vertices 76 | 77 | func testTileVertices() throws { 78 | 79 | XCTAssertTrue(testVertices(.tile)) 80 | } 81 | 82 | func testChunkVertices() throws { 83 | 84 | XCTAssertTrue(testVertices(.chunk)) 85 | } 86 | 87 | func testRegionVertices() throws { 88 | 89 | XCTAssertTrue(testVertices(.region)) 90 | } 91 | } 92 | 93 | extension HexagonVertexTests { 94 | 95 | private func testVertices(_ scale: Hexagon.Scale) -> Bool { 96 | 97 | let zero = Hexagon.zero 98 | 99 | let edgeLength = scale.edgeLength 100 | let halfEdgeLength = edgeLength / 2.0 101 | let sqrt3d2 = .sqrt3d2 * edgeLength 102 | 103 | let v0 = Vector(0.0, 0.0, edgeLength) 104 | let v1 = Vector(sqrt3d2, 0.0, halfEdgeLength) 105 | let v2 = Vector(sqrt3d2, 0.0, -halfEdgeLength) 106 | let v3 = Vector(0.0, 0.0, -edgeLength) 107 | let v4 = Vector(-sqrt3d2, 0.0, -halfEdgeLength) 108 | let v5 = Vector(-sqrt3d2, 0.0, halfEdgeLength) 109 | 110 | guard Vector(zero.vertex, 111 | scale).isEqual(to: .zero), 112 | Vector(zero.vertex(.c0), 113 | scale).isEqual(to: v0), 114 | Vector(zero.vertex(.c1), 115 | scale).isEqual(to: v1), 116 | Vector(zero.vertex(.c2), 117 | scale).isEqual(to: v2), 118 | Vector(zero.vertex(.c3), 119 | scale).isEqual(to: v3), 120 | Vector(zero.vertex(.c4), 121 | scale).isEqual(to: v4), 122 | Vector(zero.vertex(.c5), 123 | scale).isEqual(to: v5) else { return false } 124 | 125 | return true 126 | } 127 | 128 | private func testVertexToVector(_ scale: Hexagon.Scale) -> Bool { 129 | 130 | let hexagons: [Hexagon] = [.zero, 131 | .init(.unitX - .unitZ), 132 | .init(.unitX - .unitY), 133 | .init(-.unitY + .unitZ), 134 | .init(-.unitX + .unitZ), 135 | .init(-.unitX + .unitY), 136 | .init(.unitY - .unitZ)] 137 | 138 | let delta = 0.9999 139 | 140 | for hexagon in hexagons { 141 | 142 | let position = Vector(hexagon.vertex, 143 | scale) 144 | 145 | for corner in Hexagon.Corner.allCases { 146 | 147 | let vertex = Vector(hexagon.vertex(corner), 148 | scale) 149 | 150 | let vector = position.lerp(vertex, delta) 151 | 152 | if Hexagon(vector, 153 | scale).vertex != hexagon.vertex { return false } 154 | } 155 | } 156 | 157 | return true 158 | } 159 | 160 | private func testVectorToHexagon(_ scale: Hexagon.Scale) -> Bool { 161 | 162 | let edgeLength = scale.edgeLength 163 | let halfEdgeLength = edgeLength / 2.0 164 | let sqrt3d2 = .sqrt3d2 * edgeLength 165 | 166 | let c0 = Coordinate(-1, 1, 0) 167 | let c1 = Coordinate(1, 0, -1) 168 | let c2 = Coordinate(0, -1, 1) 169 | 170 | let c3 = Coordinate(2, -2, 0) 171 | let c4 = Coordinate(-2, 0, 2) 172 | let c5 = Coordinate(0, 2, -2) 173 | let c6 = Coordinate.zero 174 | 175 | let v0 = Vector(sqrt3d2, 0.0, -(edgeLength + halfEdgeLength)) 176 | let v1 = Vector(sqrt3d2, 0.0, edgeLength + halfEdgeLength) 177 | let v2 = Vector(-sqrt3d2 * 2.0, 0.0, 0.0) 178 | let v3 = Vector(-sqrt3d2 * 2.0, 0.0, (edgeLength + halfEdgeLength) * 2.0) 179 | let v4 = Vector(-sqrt3d2 * 2.0, 0.0, -(edgeLength + halfEdgeLength) * 2.0) 180 | let v5 = Vector(sqrt3d2 * 4.0, 0.0, 0.0) 181 | let v6 = Vector.zero 182 | 183 | guard Hexagon(v0, scale).vertex.position == c0, 184 | Hexagon(v1, scale).vertex.position == c1, 185 | Hexagon(v2, scale).vertex.position == c2, 186 | Hexagon(v3, scale).vertex.position == c3, 187 | Hexagon(v4, scale).vertex.position == c4, 188 | Hexagon(v5, scale).vertex.position == c5, 189 | Hexagon(v6, scale).vertex.position == c6 else { return false } 190 | 191 | return true 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Tests/DeltilleTests/Triangle/TriangleVertexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TriangleVertexTests.swift 3 | // 4 | // Created by Zack Brown on 25/05/2024. 5 | // 6 | 7 | import Euclid 8 | import XCTest 9 | @testable import Deltille 10 | 11 | final class TriangleVertexTests: XCTestCase { 12 | 13 | typealias Coordinate = Grid.Coordinate 14 | typealias Triangle = Grid.Triangle 15 | typealias Vertex = Triangle.Vertex 16 | 17 | private let vertex = Vertex(.init(3, -1, -1)) 18 | 19 | // MARK: Tiles 20 | 21 | func testVertexTiles() throws { 22 | 23 | let tiles: [Triangle] = [.init(2, -1, -1), 24 | .init(2, -2, -1), 25 | .init(3, -2, -1), 26 | .init(3, -2, -2), 27 | .init(3, -1, -2), 28 | .init(2, -1, -2)] 29 | 30 | XCTAssertEqual(vertex.tiles, tiles) 31 | } 32 | 33 | // MARK: Vertices 34 | 35 | func testVertexVertices() throws { 36 | 37 | let vertices: [Vertex] = [.init(2, 0, -1), 38 | .init(2, -1, 0), 39 | .init(3, -2, 0), 40 | .init(4, -2, -1), 41 | .init(4, -1, -2), 42 | .init(3, 0, -2)] 43 | 44 | XCTAssertEqual(vertex.vertices, vertices) 45 | } 46 | 47 | func testVertexConversion() throws { 48 | 49 | let triangle0 = Triangle(Coordinate(-3, -2, 4)) 50 | let triangle1 = Triangle(Coordinate(-2, -2, 4)) 51 | 52 | let corner0 = triangle0.vertex(.c2) 53 | let center0 = Vector(triangle0.vertex, .tile) 54 | let target0 = Vector(corner0, .tile) 55 | let vector0 = center0.lerp(target0, 0.9) 56 | let result0 = Triangle(vector0, .tile) 57 | 58 | let corner1 = triangle1.vertex(.c1) 59 | let center1 = Vector(triangle1.vertex, .tile) 60 | let target1 = Vector(corner1, .tile) 61 | let vector1 = center1.lerp(target1, 0.9) 62 | let result1 = Triangle(vector1, .tile) 63 | 64 | XCTAssertEqual(triangle0.vertex, result0.vertex) 65 | XCTAssertEqual(triangle1.vertex, result1.vertex) 66 | } 67 | 68 | // MARK: Vertex to Vector 69 | 70 | func testVertexToVectorSierpinski() throws { 71 | 72 | XCTAssertTrue(testVertexToVector(.sierpinski)) 73 | } 74 | 75 | func testVertexToVectorTile() throws { 76 | 77 | XCTAssertTrue(testVertexToVector(.tile)) 78 | } 79 | 80 | func testVertexToVectorChunk() throws { 81 | 82 | XCTAssertTrue(testVertexToVector(.chunk)) 83 | } 84 | 85 | func testVertexToVectorRegion() throws { 86 | 87 | XCTAssertTrue(testVertexToVector(.region)) 88 | } 89 | 90 | // MARK: Vector to Triangle 91 | 92 | func testVectorToTriangleSierpinski() throws { 93 | 94 | XCTAssertTrue(testVectorToTriangle(.sierpinski)) 95 | } 96 | 97 | func testVectorToTriangleTile() throws { 98 | 99 | XCTAssertTrue(testVectorToTriangle(.tile)) 100 | } 101 | 102 | func testVectorToTriangleChunk() throws { 103 | 104 | XCTAssertTrue(testVectorToTriangle(.chunk)) 105 | } 106 | 107 | func testVectorToTriangleRegion() throws { 108 | 109 | XCTAssertTrue(testVectorToTriangle(.region)) 110 | } 111 | 112 | // MARK: Vertices 113 | 114 | func testSierpinskiVertices() throws { 115 | 116 | XCTAssertTrue(testVertices(.sierpinski)) 117 | } 118 | 119 | func testTileVertices() throws { 120 | 121 | XCTAssertTrue(testVertices(.tile)) 122 | } 123 | 124 | func testChunkVertices() throws { 125 | 126 | XCTAssertTrue(testVertices(.chunk)) 127 | } 128 | 129 | func testRegionVertices() throws { 130 | 131 | XCTAssertTrue(testVertices(.region)) 132 | } 133 | } 134 | 135 | extension TriangleVertexTests { 136 | 137 | private func testVertices(_ scale: Triangle.Scale) -> Bool { 138 | 139 | let zero = Triangle(Vertex.zero) 140 | let x = Triangle(-.unitX) 141 | let y = Triangle(-.unitY) 142 | let z = Triangle(-.unitZ) 143 | 144 | let edgeLength = scale.edgeLength 145 | let halfEdgeLength = edgeLength / 2.0 146 | let sqrt3d2 = .sqrt3d2 * edgeLength 147 | 148 | let v0 = Vector(-halfEdgeLength, 0.0, .sqrt3d2 * edgeLength) 149 | let v1 = Vector(edgeLength, 0.0, 0.0) 150 | let v2 = Vector(-halfEdgeLength, 0.0, -.sqrt3d2 * edgeLength) 151 | 152 | let v3 = Vector(edgeLength, 0.0, -sqrt3d2 * 2.0) 153 | let v4 = Vector(-edgeLength * 2.0, 0.0, 0.0) 154 | let v5 = Vector(edgeLength, 0.0, sqrt3d2 * 2.0) 155 | 156 | let px = Vector(halfEdgeLength, 0.0, -sqrt3d2) 157 | let py = Vector(-edgeLength, 0.0, 0.0) 158 | let pz = Vector(halfEdgeLength, 0.0, sqrt3d2) 159 | 160 | guard Vector(zero.vertex, 161 | scale).isEqual(to: .zero), 162 | Vector(zero.vertex(.c0), 163 | scale).isEqual(to: v0), 164 | Vector(zero.vertex(.c1), 165 | scale).isEqual(to: v1), 166 | Vector(zero.vertex(.c2), 167 | scale).isEqual(to: v2), 168 | 169 | Vector(x.vertex, 170 | scale).isEqual(to: px), 171 | Vector(x.vertex(.c0), 172 | scale).isEqual(to: v3), 173 | Vector(x.vertex(.c1), 174 | scale).isEqual(to: v2), 175 | Vector(x.vertex(.c2), 176 | scale).isEqual(to: v1), 177 | 178 | Vector(y.vertex, 179 | scale).isEqual(to: py), 180 | Vector(y.vertex(.c0), 181 | scale).isEqual(to: v2), 182 | Vector(y.vertex(.c1), 183 | scale).isEqual(to: v4), 184 | Vector(y.vertex(.c2), 185 | scale).isEqual(to: v0), 186 | 187 | Vector(z.vertex, 188 | scale).isEqual(to: pz), 189 | Vector(z.vertex(.c0), 190 | scale).isEqual(to: v1), 191 | Vector(z.vertex(.c1), 192 | scale).isEqual(to: v0), 193 | Vector(z.vertex(.c2), 194 | scale).isEqual(to: v5) else { return false } 195 | 196 | return true 197 | } 198 | 199 | private func testVertexToVector(_ scale: Triangle.Scale) -> Bool { 200 | 201 | let triangles: [Triangle] = [.zero, 202 | .init(-.unitX), 203 | .init(-.unitY), 204 | .init(-.unitZ)] 205 | 206 | let interpolation = 0.9999 207 | 208 | for triangle in triangles { 209 | 210 | let center = triangle.vertex.position(scale) 211 | 212 | for corner in triangle.corners { 213 | 214 | let vertex = triangle.vertex(corner) 215 | 216 | let position = center.lerp(vertex.position(scale), 217 | interpolation) 218 | 219 | let result = Triangle(position, 220 | scale) 221 | 222 | if result.vertex != triangle.vertex { 223 | 224 | return false 225 | } 226 | } 227 | } 228 | 229 | return true 230 | } 231 | 232 | private func testVectorToTriangle(_ scale: Triangle.Scale) -> Bool { 233 | 234 | let edgeLength = scale.edgeLength 235 | let sqrt3d2 = .sqrt3d2 * edgeLength 236 | 237 | let c0 = Coordinate(-2, -2, 4) 238 | let c1 = Coordinate(-2, 4, -2) 239 | let c2 = Coordinate(4, -2, -2) 240 | 241 | let c3 = Coordinate(-3, -3, 6) 242 | let c4 = Coordinate(-3, 6, -3) 243 | let c5 = Coordinate(6, -3, -3) 244 | 245 | let v0 = Vector(-edgeLength * 3.0, 0.0, -sqrt3d2 * 6.0) 246 | let v1 = Vector( edgeLength * 6.0, 0.0, 0.0) 247 | let v2 = Vector(-edgeLength * 3.0, 0.0, sqrt3d2 * 6.0) 248 | let v3 = Vector(-edgeLength * 4.5, 0.0, -sqrt3d2 * 9.0) 249 | let v4 = Vector( edgeLength * 9.0, 0.0, 0.0) 250 | let v5 = Vector(-edgeLength * 4.5, 0.0, sqrt3d2 * 9.0) 251 | 252 | guard Triangle(v0, scale).vertex.position == c0, 253 | Triangle(v1, scale).vertex.position == c1, 254 | Triangle(v2, scale).vertex.position == c2, 255 | Triangle(v3, scale).vertex.position == c3, 256 | Triangle(v4, scale).vertex.position == c4, 257 | Triangle(v5, scale).vertex.position == c5 else { return false } 258 | 259 | return true 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /Sources/Deltille/Hexagon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hexagon.swift 3 | // 4 | // Created by Zack Brown on 05/06/2024. 5 | // 6 | 7 | import Euclid 8 | import Foundation 9 | 10 | // MARK: Hexagon 11 | 12 | extension Grid { 13 | 14 | public struct Hexagon: Tile { 15 | 16 | public static let zero = Self(Vertex.zero) 17 | 18 | public let vertex: Vertex 19 | 20 | public init(_ position: Coordinate) { 21 | 22 | self.vertex = Vertex(position) 23 | } 24 | 25 | public init(_ vertex: Vertex) { 26 | 27 | self.vertex = vertex 28 | } 29 | 30 | public init(_ x: Int, 31 | _ y: Int, 32 | _ z: Int) { 33 | 34 | self.vertex = Vertex(x, y, z) 35 | } 36 | 37 | public init(_ vector: Vector, 38 | _ scale: Scale) { 39 | 40 | let i = ceil((vector.z - .sqrt3d3 * vector.x) / scale.edgeLength) 41 | let j = floor(( .sqrt3d3 * 2.0 * vector.x) / scale.edgeLength) + 1 42 | let k = ceil((-vector.z - .sqrt3d3 * vector.x) / scale.edgeLength) 43 | 44 | self.vertex = Vertex(Int(round((i - k) / 3.0)), 45 | Int(round((j - i) / 3.0)), 46 | Int(round((k - j) / 3.0))) 47 | } 48 | } 49 | } 50 | 51 | extension Grid.Hexagon { 52 | 53 | public var id: String { vertex.id } 54 | 55 | public var vertices: [Vertex] { 56 | 57 | [.init(vertex.position + .unitX), 58 | .init(vertex.position - .unitZ), 59 | .init(vertex.position + .unitY), 60 | .init(vertex.position - .unitX), 61 | .init(vertex.position + .unitZ), 62 | .init(vertex.position - .unitY)] 63 | } 64 | 65 | public var corners: [Corner] { 66 | 67 | Corner.allCases 68 | } 69 | 70 | public var edges: [Edge] { 71 | 72 | Edge.allCases 73 | } 74 | 75 | public var adjacent: [Self] { 76 | 77 | edges.map { 78 | 79 | .init(vertex.position + translation($0)) 80 | } 81 | } 82 | 83 | public var perimeter: [Self] { adjacent } 84 | } 85 | 86 | extension Grid.Hexagon { 87 | 88 | public func position(_ scale: Scale) -> Vector { 89 | 90 | vertex.position(scale) 91 | } 92 | 93 | public func vertex(_ corner: Corner) -> Vertex { 94 | 95 | vertices[corner.rawValue] 96 | } 97 | 98 | public func corner(_ vertex: Vertex) -> Corner? { 99 | 100 | guard let index = vertices.firstIndex(of: vertex) else { return nil } 101 | 102 | return Corner(rawValue: index) 103 | } 104 | 105 | public func neighbour(_ edge: Edge) -> Self { 106 | 107 | adjacent[edge.rawValue] 108 | } 109 | 110 | public func translation(_ along: Edge) -> Grid.Coordinate { 111 | 112 | switch along { 113 | 114 | case .e0: .unitX - .unitZ 115 | case .e1: .unitY - .unitZ 116 | case .e2: .unitY - .unitX 117 | case .e3: .unitZ - .unitX 118 | case .e4: .unitZ - .unitY 119 | case .e5: .unitX - .unitY 120 | } 121 | } 122 | 123 | public func contains(_ vector: Vector, 124 | _ scale: Scale) -> Bool { 125 | 126 | let center = position(scale) 127 | 128 | let dx = abs(vector.x - center.x) 129 | let dz = abs(vector.z - center.z) 130 | 131 | if dx > scale.edgeLength * 1.5 { return false } 132 | if dz > scale.edgeLength * .sqrt3 { return false } 133 | 134 | return (dz * 2.0 + dx * .sqrt3) <= .sqrt3 * scale.edgeLength * 2.0 135 | } 136 | 137 | public func mesh(_ scale: Scale) -> Mesh { 138 | 139 | let vertices = self.vertices.map { 140 | 141 | Euclid.Vertex($0.position(scale), 142 | .unitY) 143 | } 144 | 145 | guard let polygon = Polygon(vertices) else { fatalError("Degenerate tile vertices") } 146 | 147 | return .init([polygon]) 148 | } 149 | 150 | public func closest(_ vector: Vector, 151 | _ scale: Scale) -> Vertex { 152 | 153 | let vertices = vertices.map { $0.position(scale) } 154 | 155 | let index = vertices.firstIndexOf(closest: vector) 156 | 157 | return self.vertices[index] 158 | } 159 | } 160 | 161 | // MARK: Corner 162 | 163 | extension Grid.Hexagon { 164 | 165 | public enum Corner: Int, 166 | Deltille.Corner { 167 | 168 | case c0, c1, c2, c3, c4, c5 169 | 170 | public var id: String { "\(rawValue)" } 171 | 172 | public var corners: [Corner] { 173 | 174 | switch self { 175 | 176 | case .c0: [.c1, .c5] 177 | case .c1: [.c2, .c0] 178 | case .c2: [.c3, .c1] 179 | case .c3: [.c4, .c2] 180 | case .c4: [.c5, .c3] 181 | case .c5: [.c0, .c4] 182 | } 183 | } 184 | 185 | public var edges: [Edge] { 186 | 187 | switch self { 188 | 189 | case .c0: [.e0, .e5] 190 | case .c1: [.e1, .e0] 191 | case .c2: [.e2, .e1] 192 | case .c3: [.e3, .e2] 193 | case .c4: [.e4, .e3] 194 | case .c5: [.e5, .e4] 195 | } 196 | } 197 | } 198 | } 199 | 200 | // MARK: Edge 201 | 202 | extension Grid.Hexagon { 203 | 204 | public enum Edge: Int, 205 | Deltille.Edge { 206 | 207 | case e0, e1, e2, e3, e4, e5 208 | 209 | public var id: String { "\(rawValue)" } 210 | 211 | public var corners: [Corner] { 212 | 213 | switch self { 214 | 215 | case .e0: [.c1, .c0] 216 | case .e1: [.c2, .c1] 217 | case .e2: [.c3, .c2] 218 | case .e3: [.c4, .c3] 219 | case .e4: [.c5, .c4] 220 | case .e5: [.c0, .c5] 221 | } 222 | } 223 | 224 | public var edges: [Edge] { 225 | 226 | switch self { 227 | 228 | case .e0: [.e1, .e5] 229 | case .e1: [.e2, .e0] 230 | case .e2: [.e3, .e1] 231 | case .e3: [.e4, .e2] 232 | case .e4: [.e5, .e3] 233 | case .e5: [.e0, .e4] 234 | } 235 | } 236 | } 237 | } 238 | 239 | // MARK: Footprint 240 | 241 | extension Grid.Hexagon { 242 | 243 | final class Footprint: Deltille.Footprint { 247 | 248 | public convenience init(_ origin: Grid.Hexagon, 249 | _ coordinates: [Grid.Coordinate]) { 250 | 251 | self.init(origin, 252 | coordinates.map { 253 | 254 | .init(origin.vertex.position + $0) 255 | }) 256 | } 257 | 258 | public override func rotate(_ rotation: Rotation) -> Self { 259 | 260 | let hexagons = tiles.map { 261 | 262 | let hexagon = Grid.Hexagon($0.vertex.position - origin.vertex.position) 263 | 264 | let rotated = hexagon.rotate(rotation) 265 | 266 | return Grid.Hexagon(rotated.vertex.position + origin.vertex.position) 267 | } 268 | 269 | return Self(origin, 270 | hexagons) 271 | } 272 | } 273 | } 274 | 275 | // MARK: Rotation 276 | 277 | extension Grid.Hexagon: Rotatable { 278 | 279 | public enum Rotation: String, 280 | Deltille.Rotation { 281 | 282 | public static let inverse: Double = .pi 283 | public static let step: Double = .tau / 6.0 284 | 285 | case clockwise 286 | case counterClockwise 287 | 288 | public var id: String { rawValue } 289 | } 290 | 291 | public func rotate(_ rotation: Rotation) -> Self { 292 | 293 | switch rotation { 294 | 295 | case .clockwise: 296 | 297 | .init(-vertex.position.z, 298 | -vertex.position.x, 299 | -vertex.position.y) 300 | 301 | case .counterClockwise: 302 | 303 | .init(-vertex.position.y, 304 | -vertex.position.z, 305 | -vertex.position.x) 306 | } 307 | } 308 | } 309 | 310 | // MARK: Scale 311 | 312 | extension Grid.Hexagon { 313 | 314 | public enum Scale: String, 315 | Deltille.Scale { 316 | 317 | case tile 318 | case chunk 319 | case region 320 | 321 | public var id: String { rawValue.capitalized } 322 | 323 | public var edgeLength: Double { 324 | 325 | switch self { 326 | 327 | case .tile: 0.5 328 | case .chunk: 3.5 329 | case .region: 14.0 330 | } 331 | } 332 | } 333 | } 334 | 335 | // MARK: Vertex 336 | 337 | extension Grid.Hexagon { 338 | 339 | public struct Vertex: Deltille.Vertex { 340 | 341 | public static let zero = Self(.zero) 342 | 343 | public let position: Grid.Coordinate 344 | 345 | public var tiles: [Grid.Hexagon] { 346 | 347 | Grid.Axis.allCases.map { 348 | 349 | .init(position + ($0.unit * (position.equalToOne ? -1 : 1))) 350 | } 351 | } 352 | 353 | public var vertices: [Vertex] { 354 | 355 | Grid.Axis.allCases.map { 356 | 357 | .init(position + ((.one - $0.unit) * (position.equalToOne ? -1 : 1))) 358 | } 359 | } 360 | 361 | public init(_ position: Grid.Coordinate) { 362 | 363 | self.position = position 364 | } 365 | 366 | public init(_ x: Int, 367 | _ y: Int, 368 | _ z: Int) { 369 | 370 | self.position = .init(x, y, z) 371 | } 372 | 373 | public func position(_ scale: Scale) -> Vector { 374 | 375 | Vector(self, 376 | scale) 377 | } 378 | } 379 | } 380 | 381 | -------------------------------------------------------------------------------- /Sources/Deltille/Triangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Triangle.swift 3 | // 4 | // Created by Zack Brown on 23/05/2024. 5 | // 6 | 7 | import Euclid 8 | import Foundation 9 | 10 | // MARK: Triangle 11 | 12 | extension Grid { 13 | 14 | public struct Triangle: Tile { 15 | 16 | public static let zero = Self(Vertex.zero) 17 | 18 | public let vertex: Vertex 19 | 20 | public init(_ position: Coordinate) { 21 | 22 | self.vertex = Vertex(position) 23 | } 24 | 25 | public init(_ vertex: Vertex) { 26 | 27 | self.vertex = vertex 28 | } 29 | 30 | public init(_ x: Int, 31 | _ y: Int, 32 | _ z: Int) { 33 | 34 | self.vertex = .init(x, y, z) 35 | } 36 | 37 | public init(_ vector: Vector, 38 | _ scale: Scale) { 39 | 40 | let j = ceil((vector.x - .sqrt3d3 * vector.z) / scale.edgeLength) 41 | let i = floor(( .sqrt3d3 * 2.0 * vector.z) / scale.edgeLength) + 1 42 | let k = ceil((-vector.x - .sqrt3d3 * vector.z) / scale.edgeLength) 43 | 44 | let triangle = Grid.Triangle(Int(round((i - j) / 3.0)), 45 | Int(round((j - k) / 3.0)), 46 | Int(round((k - i) / 3.0))) 47 | 48 | let triangles = [triangle] + triangle.adjacent 49 | 50 | let closest = triangles.first { 51 | 52 | $0.contains(vector, 53 | scale) 54 | } ?? triangle 55 | 56 | self.init(closest.vertex) 57 | } 58 | } 59 | } 60 | 61 | extension Grid.Triangle { 62 | 63 | public var id: String { vertex.id } 64 | 65 | public var isPointy: Bool { 66 | 67 | vertex.position.equalToZero 68 | } 69 | 70 | public var rotation: Double { 71 | 72 | isPointy ? 0.0 : Rotation.inverse 73 | } 74 | 75 | public var vertices: [Vertex] { 76 | 77 | edges.map { 78 | 79 | .init(vertex.position + (isPointy ? -translation($0) : .one - translation($0))) 80 | } 81 | } 82 | 83 | public var corners: [Corner] { 84 | 85 | Corner.allCases 86 | } 87 | 88 | public var edges: [Edge] { 89 | 90 | Edge.allCases 91 | } 92 | 93 | public var adjacent: [Self] { 94 | 95 | edges.map { 96 | 97 | .init(vertex.position + translation($0)) 98 | } 99 | } 100 | 101 | public var perimeter: [Self] { 102 | 103 | Array(vertices.reduce(into: Set(), { result, vertex in 104 | 105 | for tile in vertex.tiles { 106 | 107 | guard tile.vertex != self.vertex else { continue } 108 | 109 | result.insert(tile) 110 | } 111 | })) 112 | } 113 | } 114 | 115 | extension Grid.Triangle { 116 | 117 | public func position(_ scale: Scale) -> Vector { 118 | 119 | vertex.position(scale) 120 | } 121 | 122 | public func vertex(_ corner: Corner) -> Vertex { 123 | 124 | vertices[corner.rawValue] 125 | } 126 | 127 | public func corner(_ vertex: Vertex) -> Corner? { 128 | 129 | guard let index = vertices.firstIndex(of: vertex) else { return nil } 130 | 131 | return Corner(rawValue: index) 132 | } 133 | 134 | public func neighbour(_ edge: Edge) -> Self { 135 | 136 | adjacent[edge.rawValue] 137 | } 138 | 139 | public func translation(_ along: Edge) -> Grid.Coordinate { 140 | 141 | switch along { 142 | 143 | case .e0: isPointy ? -.unitX : .unitX 144 | case .e1: isPointy ? -.unitY : .unitY 145 | case .e2: isPointy ? -.unitZ : .unitZ 146 | } 147 | } 148 | 149 | public func contains(_ vector: Vector, 150 | _ scale: Scale) -> Bool { 151 | 152 | let c0 = vertex(.c0).position(scale) 153 | let c1 = vertex(.c1).position(scale) 154 | let c2 = vertex(.c2).position(scale) 155 | 156 | let v0 = c2 - c0 157 | let v1 = c1 - c0 158 | let v2 = vector - c0 159 | 160 | let v0v0 = v0.dot(v0) 161 | let v0v1 = v0.dot(v1) 162 | let v0v2 = v0.dot(v2) 163 | let v1v1 = v1.dot(v1) 164 | let v1v2 = v1.dot(v2) 165 | 166 | let denominator = (v0v0 * v1v1 - v0v1 * v0v1) 167 | if abs(denominator) < 1e-8 { return false } 168 | 169 | let inverse = 1.0 / denominator 170 | let u = (v1v1 * v0v2 - v0v1 * v1v2) * inverse 171 | let v = (v0v0 * v1v2 - v0v1 * v0v2) * inverse 172 | 173 | return (u >= 0.0) && (v >= 0.0) && (u + v <= 1.0) 174 | } 175 | 176 | public func mesh(_ scale: Scale) -> Mesh { 177 | 178 | let vertices = self.vertices.map { 179 | 180 | Euclid.Vertex($0.position(scale), 181 | .unitY) 182 | } 183 | 184 | guard let polygon = Polygon(vertices) else { fatalError("Degenerate tile vertices") } 185 | 186 | return .init([polygon]) 187 | } 188 | 189 | public func closest(_ vector: Vector, 190 | _ scale: Scale) -> Vertex { 191 | 192 | let vertices = vertices.map { $0.position(scale) } 193 | 194 | let index = vertices.firstIndexOf(closest: vector) 195 | 196 | return self.vertices[index] 197 | } 198 | } 199 | 200 | // MARK: Corner 201 | 202 | extension Grid.Triangle { 203 | 204 | public enum Corner: Int, 205 | Deltille.Corner { 206 | 207 | case c0, c1, c2 208 | 209 | public var id: String { "\(rawValue)" } 210 | 211 | public var corners: [Corner] { 212 | 213 | switch self { 214 | 215 | case .c0: [.c1, .c2] 216 | case .c1: [.c2, .c0] 217 | case .c2: [.c0, .c1] 218 | } 219 | } 220 | 221 | public var edges: [Edge] { 222 | 223 | switch self { 224 | 225 | case .c0: [.e0, .e2] 226 | case .c1: [.e1, .e0] 227 | case .c2: [.e2, .e1] 228 | } 229 | } 230 | } 231 | } 232 | 233 | // MARK: Edge 234 | 235 | extension Grid.Triangle { 236 | 237 | public enum Edge: Int, 238 | Deltille.Edge { 239 | 240 | case e0, e1, e2 241 | 242 | public var id: String { "\(rawValue)" } 243 | 244 | public var corners: [Corner] { 245 | 246 | switch self { 247 | 248 | case .e0: [.c1, .c0] 249 | case .e1: [.c2, .c1] 250 | case .e2: [.c0, .c2] 251 | } 252 | } 253 | 254 | public var edges: [Edge] { 255 | 256 | switch self { 257 | 258 | case .e0: [.e1, .e2] 259 | case .e1: [.e2, .e0] 260 | case .e2: [.e0, .e1] 261 | } 262 | } 263 | } 264 | } 265 | 266 | // MARK: Footprint 267 | 268 | extension Grid.Triangle { 269 | 270 | final class Footprint: Deltille.Footprint { 274 | 275 | public convenience init(_ origin: Grid.Triangle, 276 | _ coordinates: [Grid.Coordinate]) { 277 | 278 | self.init(origin, 279 | coordinates.map { 280 | 281 | .init(origin.vertex.position + (origin.isPointy ? $0 : -$0)) 282 | }) 283 | } 284 | 285 | public override func rotate(_ rotation: Rotation) -> Self { 286 | 287 | let triangles = tiles.map { 288 | 289 | let triangle = Grid.Triangle($0.vertex.position - origin.vertex.position) 290 | 291 | let rotated = triangle.rotate(rotation) 292 | 293 | return Grid.Triangle(rotated.vertex.position + origin.vertex.position) 294 | } 295 | 296 | return Self(origin, 297 | triangles) 298 | } 299 | } 300 | } 301 | 302 | // MARK: Rotation 303 | 304 | extension Grid.Triangle: Rotatable { 305 | 306 | public enum Rotation: String, 307 | Deltille.Rotation { 308 | 309 | public static let inverse: Double = .pi 310 | public static let step: Double = .tau / 3.0 311 | 312 | case clockwise 313 | case counterClockwise 314 | 315 | public var id: String { rawValue } 316 | } 317 | 318 | public func rotate(_ rotation: Rotation) -> Self { 319 | 320 | switch rotation { 321 | 322 | case .clockwise: 323 | 324 | .init(vertex.position.y, 325 | vertex.position.z, 326 | vertex.position.x) 327 | 328 | case .counterClockwise: 329 | 330 | .init(vertex.position.z, 331 | vertex.position.x, 332 | vertex.position.y) 333 | } 334 | } 335 | } 336 | 337 | // MARK: Scale 338 | 339 | extension Grid.Triangle { 340 | 341 | public enum Scale: String, 342 | Deltille.Scale { 343 | 344 | case sierpinski 345 | case tile 346 | case chunk 347 | case region 348 | 349 | public var id: String { rawValue.capitalized } 350 | 351 | public var edgeLength: Double { 352 | 353 | switch self { 354 | 355 | case .sierpinski: 0.1428571429 // 1.0 / 7.0 356 | case .tile: 1.0 357 | case .chunk: 7.0 358 | case .region: 28.0 359 | } 360 | } 361 | } 362 | 363 | public func transpose(_ from: Grid.Triangle.Scale, 364 | _ to: Grid.Triangle.Scale) -> Self { 365 | 366 | guard from != to else { return self } 367 | 368 | let origin = Vector(vertex, 369 | from) 370 | 371 | return Self(origin, 372 | to) 373 | } 374 | } 375 | 376 | // MARK: Vertex 377 | 378 | extension Grid.Triangle { 379 | 380 | public struct Vertex: Deltille.Vertex { 381 | 382 | public static let zero = Self(.zero) 383 | 384 | public let position: Grid.Coordinate 385 | 386 | public var tiles: [Grid.Triangle] { 387 | 388 | [.init(position - .unitX), 389 | .init(position - (.unitX + .unitY)), 390 | .init(position - .unitY), 391 | .init(position - (.unitY + .unitZ)), 392 | .init(position - .unitZ), 393 | .init(position - (.unitX + .unitZ))] 394 | } 395 | 396 | public var vertices: [Vertex] { 397 | 398 | [.init(position + (-.unitX + .unitY)), 399 | .init(position + (-.unitX + .unitZ)), 400 | .init(position + (-.unitY + .unitZ)), 401 | .init(position + (-.unitY + .unitX)), 402 | .init(position + (-.unitZ + .unitX)), 403 | .init(position + (-.unitZ + .unitY))] 404 | } 405 | 406 | public init(_ position: Grid.Coordinate) { 407 | 408 | self.position = position 409 | } 410 | 411 | public init(_ x: Int, 412 | _ y: Int, 413 | _ z: Int) { 414 | 415 | self.position = .init(x, y, z) 416 | } 417 | 418 | public func position(_ scale: Scale) -> Vector { 419 | 420 | Vector(self, 421 | scale) 422 | } 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /Tests/DeltilleTests/Triangle/TriangleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TriangleTests.swift 3 | // 4 | // Created by Zack Brown on 25/05/2024. 5 | // 6 | 7 | import Euclid 8 | import XCTest 9 | @testable import Deltille 10 | 11 | final class TriangleTests: XCTestCase { 12 | 13 | typealias Coordinate = Grid.Coordinate 14 | typealias Triangle = Grid.Triangle 15 | typealias Vertex = Triangle.Vertex 16 | 17 | private let pointyTriangle = Triangle(4, -2, -2) 18 | private let flatTriangle = Triangle(-2, -2, 3) 19 | private let unitTriangle = Triangle(Vertex.zero) 20 | private let x = Triangle(-.unitX) 21 | private let y = Triangle(-.unitY) 22 | private let z = Triangle(-.unitZ) 23 | 24 | // MARK: Contains Vector 25 | 26 | func testTriangleContainsVector() throws { 27 | 28 | XCTAssertTrue(unitTriangle.contains(unitTriangle.position(.tile), 29 | .tile)) 30 | XCTAssertTrue(unitTriangle.contains(.zero, 31 | .tile)) 32 | XCTAssertTrue(unitTriangle.contains(unitTriangle.vertex(.c0).position(.tile), 33 | .tile)) 34 | XCTAssertTrue(unitTriangle.contains(unitTriangle.vertex(.c1).position(.tile), 35 | .tile)) 36 | XCTAssertTrue(unitTriangle.contains(unitTriangle.vertex(.c1).position(.tile), 37 | .tile)) 38 | 39 | XCTAssertTrue(flatTriangle.contains(flatTriangle.position(.tile), 40 | .tile)) 41 | XCTAssertFalse(flatTriangle.contains(.zero, 42 | .tile)) 43 | 44 | XCTAssertTrue(pointyTriangle.contains(pointyTriangle.position(.tile), 45 | .tile)) 46 | XCTAssertFalse(pointyTriangle.contains(.zero, 47 | .tile)) 48 | } 49 | 50 | // MARK: Pointy / Flat 51 | 52 | func testUnitTriangleIsPointy() throws { 53 | 54 | XCTAssertTrue(unitTriangle.isPointy) 55 | XCTAssertTrue(pointyTriangle.isPointy) 56 | XCTAssertFalse(flatTriangle.isPointy) 57 | } 58 | 59 | // MARK: Neighbours / Adjacency 60 | 61 | func testPointyNeighboursAdjacency() throws { 62 | 63 | let tiles: [Triangle] = [.init(3, -2, -2), 64 | .init(4, -3, -2), 65 | .init(4, -2, -3)] 66 | 67 | let neighbours = pointyTriangle.edges.map { pointyTriangle.neighbour($0) } 68 | 69 | XCTAssertEqual(neighbours, tiles) 70 | XCTAssertEqual(pointyTriangle.adjacent, tiles) 71 | } 72 | 73 | func testFlatNeighboursAdjacency() throws { 74 | 75 | let tiles: [Triangle] = [.init(-1, -2, 3), 76 | .init(-2, -1, 3), 77 | .init(-2, -2, 4)] 78 | 79 | let neighbours = flatTriangle.edges.map { flatTriangle.neighbour($0) } 80 | 81 | XCTAssertEqual(neighbours, tiles) 82 | XCTAssertEqual(flatTriangle.adjacent, tiles) 83 | } 84 | 85 | // MARK: Perimeter 86 | 87 | func testPointyPerimeter() throws { 88 | 89 | let perimeter: [Triangle] = [.init(pointyTriangle.vertex.position + .init(-1, 0, 0)), 90 | .init(pointyTriangle.vertex.position + .init(-1, 1, 0)), 91 | .init(pointyTriangle.vertex.position + .init(-1, 1, -1)), 92 | .init(pointyTriangle.vertex.position + .init(0, 1, -1)), 93 | .init(pointyTriangle.vertex.position + .init(0, 0, -1)), 94 | .init(pointyTriangle.vertex.position + .init(1, 0, -1)), 95 | .init(pointyTriangle.vertex.position + .init(1, -1, -1)), 96 | .init(pointyTriangle.vertex.position + .init(1, -1, 0)), 97 | .init(pointyTriangle.vertex.position + .init(0, -1, 0)), 98 | .init(pointyTriangle.vertex.position + .init(0, -1, 1)), 99 | .init(pointyTriangle.vertex.position + .init(-1, -1, 1)), 100 | .init(pointyTriangle.vertex.position + .init(-1, 0, 1))] 101 | 102 | let lhs = Set(perimeter) 103 | let rhs = Set(pointyTriangle.perimeter) 104 | 105 | XCTAssertTrue(lhs.isSubset(of: rhs)) 106 | XCTAssertEqual(perimeter.count, pointyTriangle.perimeter.count) 107 | } 108 | 109 | func testFlatPerimeter() throws { 110 | 111 | let perimeter: [Triangle] = [.init(flatTriangle.vertex.position + .init(1, 0, 0)), 112 | .init(flatTriangle.vertex.position + .init(1, -1, 0)), 113 | .init(flatTriangle.vertex.position + .init(1, -1, 1)), 114 | .init(flatTriangle.vertex.position + .init(0, -1, 1)), 115 | .init(flatTriangle.vertex.position + .init(0, 0, 1)), 116 | .init(flatTriangle.vertex.position + .init(-1, 0, 1)), 117 | .init(flatTriangle.vertex.position + .init(-1, 1, 1)), 118 | .init(flatTriangle.vertex.position + .init(-1, 1, 0)), 119 | .init(flatTriangle.vertex.position + .init(0, 1, 0)), 120 | .init(flatTriangle.vertex.position + .init(0, 1, -1)), 121 | .init(flatTriangle.vertex.position + .init(1, 1, -1)), 122 | .init(flatTriangle.vertex.position + .init(1, 0, -1))] 123 | 124 | let lhs = Set(perimeter) 125 | let rhs = Set(flatTriangle.perimeter) 126 | 127 | XCTAssertTrue(lhs.isSubset(of: rhs)) 128 | XCTAssertEqual(perimeter.count, flatTriangle.perimeter.count) 129 | } 130 | 131 | // MARK: Vertices / Corners 132 | 133 | func testPointyVertices() throws { 134 | 135 | let vertices: [Vertex] = [.init(5, -2, -2), 136 | .init(4, -1, -2), 137 | .init(4, -2, -1)] 138 | 139 | let center = Vector(-3.0, 0.0, 5.1961) 140 | let triangle = Triangle(center, .tile) 141 | 142 | let triangleCorners = vertices.map { pointyTriangle.corner($0) } 143 | 144 | XCTAssertEqual(triangleCorners, pointyTriangle.corners) 145 | XCTAssertEqual(triangle.vertex, pointyTriangle.vertex) 146 | } 147 | 148 | func testFlatVertices() throws { 149 | 150 | let vertices: [Vertex] = [.init(-2, -1, 4), 151 | .init(-1, -2, 4), 152 | .init(-1, -1, 3)] 153 | 154 | let center = Vector(-2.5, 0.0, -4.3301) 155 | let triangle = Triangle(center, .tile) 156 | 157 | let triangleCorners = vertices.map { flatTriangle.corner($0) } 158 | 159 | XCTAssertEqual(triangleCorners, flatTriangle.corners) 160 | XCTAssertEqual(triangle.vertex, flatTriangle.vertex) 161 | 162 | } 163 | 164 | // MARK: Transposing 165 | 166 | func testTransposeRegionToTile() throws { 167 | 168 | let regions = [unitTriangle, 169 | x, 170 | y, 171 | z].map { $0.transpose(.region, 172 | .tile) } 173 | 174 | let triangles: [Triangle] = [.init(0, 0, 0), 175 | .init(-19, 9, 9), 176 | .init(9, -19, 9), 177 | .init(9, 9, -19)] 178 | 179 | XCTAssertEqual(regions, 180 | triangles) 181 | } 182 | 183 | func testTransposeRegionToChunk() throws { 184 | 185 | let regions = [unitTriangle, 186 | x, 187 | y, 188 | z].map { $0.transpose(.region, 189 | .chunk) } 190 | 191 | let triangles: [Triangle] = [.init(0, 0, 0), 192 | .init(-3, 1, 1), 193 | .init(1, -3, 1), 194 | .init(1, 1, -3)] 195 | 196 | XCTAssertEqual(regions, 197 | triangles) 198 | } 199 | 200 | func testTransposeChunkToRegion() throws { 201 | 202 | let regions = [Triangle(0, 0, 0), 203 | x, 204 | y, 205 | z, 206 | Triangle(-3, 1, 1), 207 | Triangle(1, -3, 1), 208 | Triangle(1, 1, -3)].map { $0.transpose(.chunk, 209 | .region) } 210 | 211 | let triangles = [unitTriangle, 212 | unitTriangle, 213 | unitTriangle, 214 | unitTriangle, 215 | x, 216 | y, 217 | z] 218 | 219 | XCTAssertEqual(regions, triangles) 220 | 221 | XCTAssertEqual(.zero, 222 | Triangle(2, -1, -1).transpose(.chunk, 223 | .region)) 224 | XCTAssertEqual(.zero, 225 | Triangle(-1, 2, -1).transpose(.chunk, 226 | .region)) 227 | XCTAssertEqual(.zero, 228 | Triangle(-1, -1, 2).transpose(.chunk, 229 | .region)) 230 | 231 | XCTAssertEqual(Triangle(-1, -1, 1), 232 | Triangle(-2, -2, 3).transpose(.chunk, 233 | .region)) 234 | XCTAssertEqual(Triangle(-1, 1, -1), 235 | Triangle(-2, 3, -2).transpose(.chunk, 236 | .region)) 237 | XCTAssertEqual(Triangle(1, -1, -1), 238 | Triangle(3, -2, -2).transpose(.chunk, 239 | .region)) 240 | } 241 | 242 | func testTransposeChunkToTile() throws { 243 | 244 | let chunks = [unitTriangle, 245 | x, 246 | y, 247 | z].map { $0.transpose(.chunk, 248 | .tile) } 249 | 250 | let triangles = [Triangle(0, 0, 0), 251 | Triangle(-5, 2, 2), 252 | Triangle(2, -5, 2), 253 | Triangle(2, 2, -5)] 254 | 255 | XCTAssertEqual(chunks, 256 | triangles) 257 | } 258 | 259 | func testTransposeTileToChunk() throws { 260 | 261 | let tiles = [Triangle(0, 0, 0), 262 | x, 263 | y, 264 | z, 265 | Triangle(-5, 2, 2), 266 | Triangle(2, -5, 2), 267 | Triangle(2, 2, -5)].map { $0.transpose(.tile, 268 | .chunk) } 269 | 270 | let triangles = [unitTriangle, 271 | unitTriangle, 272 | unitTriangle, 273 | unitTriangle, 274 | x, 275 | y, 276 | z] 277 | 278 | XCTAssertEqual(tiles, triangles) 279 | 280 | XCTAssertEqual(.zero, 281 | Triangle(2, -1, -1).transpose(.tile, 282 | .chunk)) 283 | XCTAssertEqual(.zero, 284 | Triangle(-1, 2, -1).transpose(.tile, 285 | .chunk)) 286 | XCTAssertEqual(.zero, 287 | Triangle(-1, -1, 2).transpose(.tile, 288 | .chunk)) 289 | 290 | XCTAssertEqual(.zero, 291 | Triangle(-2, -2, 4).transpose(.tile, 292 | .chunk)) 293 | XCTAssertEqual(.zero, 294 | Triangle(-2, 4, -2).transpose(.tile, 295 | .chunk)) 296 | XCTAssertEqual(.zero, 297 | Triangle(4, -2, -2).transpose(.tile, 298 | .chunk)) 299 | } 300 | 301 | func testTransposeTileToRegion() throws { 302 | 303 | let tiles = [Triangle(0, 0, 0), 304 | x, 305 | y, 306 | z, 307 | Triangle(-19, 9, 9), 308 | Triangle(9, -19, 9), 309 | Triangle(9, 9, -19)].map { $0.transpose(.tile, 310 | .region) } 311 | 312 | let triangles = [unitTriangle, 313 | unitTriangle, 314 | unitTriangle, 315 | unitTriangle, 316 | x, 317 | y, 318 | z] 319 | 320 | XCTAssertEqual(tiles, 321 | triangles) 322 | } 323 | 324 | // MARK: Sieve 325 | 326 | func testPointySierpinskiSieve() throws { 327 | 328 | let sieve = pointyTriangle.sieve(for: .sierpinski) 329 | 330 | XCTAssertEqual(sieve.origin, 331 | pointyTriangle) 332 | XCTAssertEqual(sieve.scale, .sierpinski) 333 | XCTAssertEqual(sieve.triangles.count, 1) 334 | XCTAssertEqual(sieve.vertices.count, 3) 335 | } 336 | 337 | func testFlatSierpinskiSieve() throws { 338 | 339 | let sieve = flatTriangle.sieve(for: .sierpinski) 340 | 341 | XCTAssertEqual(sieve.origin, 342 | flatTriangle) 343 | XCTAssertEqual(sieve.scale, .sierpinski) 344 | XCTAssertEqual(sieve.triangles.count, 1) 345 | XCTAssertEqual(sieve.vertices.count, 3) 346 | } 347 | 348 | func testPointyTileSieve() throws { 349 | 350 | let sieve = pointyTriangle.sieve(for: .tile) 351 | 352 | XCTAssertEqual(sieve.origin, 353 | pointyTriangle) 354 | XCTAssertEqual(sieve.scale, .tile) 355 | XCTAssertEqual(sieve.triangles.count, 1) 356 | XCTAssertEqual(sieve.vertices.count, 3) 357 | } 358 | 359 | func testFlatTileSieve() throws { 360 | 361 | let sieve = flatTriangle.sieve(for: .tile) 362 | 363 | XCTAssertEqual(sieve.origin, 364 | flatTriangle) 365 | XCTAssertEqual(sieve.scale, .tile) 366 | XCTAssertEqual(sieve.triangles.count, 1) 367 | XCTAssertEqual(sieve.vertices.count, 3) 368 | } 369 | 370 | func testPointyChunkSieve() throws { 371 | 372 | let sieve = pointyTriangle.sieve(for: .chunk) 373 | 374 | XCTAssertEqual(sieve.origin, 375 | pointyTriangle) 376 | XCTAssertEqual(sieve.scale, .chunk) 377 | XCTAssertEqual(sieve.triangles.count, 49) 378 | XCTAssertEqual(sieve.vertices.count, 36) 379 | } 380 | 381 | func testFlatChunkSieve() throws { 382 | 383 | let sieve = flatTriangle.sieve(for: .chunk) 384 | 385 | XCTAssertEqual(sieve.origin, 386 | flatTriangle) 387 | XCTAssertEqual(sieve.scale, .chunk) 388 | XCTAssertEqual(sieve.triangles.count, 49) 389 | XCTAssertEqual(sieve.vertices.count, 36) 390 | } 391 | 392 | func testPointyRegionSieve() throws { 393 | 394 | let sieve = pointyTriangle.sieve(for: .region) 395 | 396 | XCTAssertEqual(sieve.origin, 397 | pointyTriangle) 398 | XCTAssertEqual(sieve.scale, .region) 399 | XCTAssertEqual(sieve.triangles.count, 784) 400 | XCTAssertEqual(sieve.vertices.count, 435) 401 | } 402 | 403 | func testFlatRegionSieve() throws { 404 | 405 | let sieve = flatTriangle.sieve(for: .region) 406 | 407 | XCTAssertEqual(sieve.origin, 408 | flatTriangle) 409 | XCTAssertEqual(sieve.scale, .region) 410 | XCTAssertEqual(sieve.triangles.count, 784) 411 | XCTAssertEqual(sieve.vertices.count, 435) 412 | } 413 | } 414 | 415 | --------------------------------------------------------------------------------