├── 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 | []()
2 | [](https://developer.apple.com/swift)
3 | [](https://www.swift.org/documentation/package-manager/)
4 | [](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 |
--------------------------------------------------------------------------------