1. ルービックキューブ 展開図の描画 と 回転を実装

<img width="100" alt="" src="https://bandainamco-am.co.jp/official_shop_images/product/4975430516680.jpg" />

ここで扱うのは、3×3×3 の ルービックキューブ です。




# ルービックキューブを描画

最終的には、ルービックキューブを3D描画したいのですが、その前に _すべての面が俯瞰できる_ 展開図 を描画します。

併せて、ルービックキューブを回転させるコードも書いて、自在に回転させようと思います。




# 1. 展開図の配置

#### ・配置

6面を以下のように配置します。
　<b>U</b>p <br>　<b>L</b>eft　<b>F</b>ront　<b>R</b>ight　<b>B</b>ack <br>　<b>D</b>own

```text
     UUU
     U0U
     UUU
 LLL FFF RRR BBB
 L1L F2F R3R B4B
 LLL FFF RRR BBB
     DDD
     D5D
     DDD
```

#### ・データ構造

3x3の面が6面の三次元配列とします。

```swift
//Rubik's Cube face
typealias RCFace = [[Int]] //3x3

//Rubik's Cube data
typealias RCData = [RCFace] //6x3x3

//initial pattern
let RCDataInitialPattern = [
    [ //Up
        [ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8],
    ],
    [ //Left
        [ 9, 10, 11],
        [12, 13, 14],
        [15, 16, 17],
    ],
    [ //Front
        [18, 19, 20],
        [21, 22, 23],
        [24, 25, 26],
    ],
    [ //Right
        [27, 28, 29],
        [30, 31, 32],
        [33, 34, 35],
    ],
    [ //Back
        [36, 37, 38],
        [39, 40, 41],
        [42, 43, 44],
    ],
    [ //Down
        [45, 46, 47],
        [48, 49, 50],
        [51, 52, 53],
    ],
]
```



# 2. 配色

世界基準配色に従い、次の配色とします。

### 　<font color="#F0F0F0">■</font><br><font color="#F7931E">■</font><font color="#00A800">■</font><font color="#FF2A00">■</font><font color="#0065FF">■</font><br>　<font color="#FFF61A">■</font>

（RGB値は独自）


```swift
//Rubik's cube - world color scheme
extension UIColor {
    static let rcWhite  = UIColor(r: 255, g: 255, b: 255) //U
    static let rcOrange = UIColor(r: 247, g: 147, b:  30) //L
    static let rcGreen  = UIColor(r:   0, g: 168, b:   0) //F
    static let rcRed    = UIColor(r: 255, g:  42, b:   0) //R
    static let rcBlue   = UIColor(r:   0, g: 101, b: 255) //B
    static let rcYellow = UIColor(r: 255, g: 246, b:  26) //D
}

#if os(macOS)
typealias UIColor = NSColor
#endif

extension UIColor {
    //specifies an RGB values 0 ~ 255
    convenience init(r: Int, g: Int, b: Int) {
        self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: 1)
    }
}

extension Color {
    static func RCColor(_ index: Int) -> Color {
        //                    Up        Right   Front     Down       Left       Back
        let colors = [UIColor.rcWhite, .rcRed, .rcGreen, .rcYellow, .rcOrange, .rcBlue]
        return Color(uiColor: colors[index])
    }
    static func RCColorPosition(_ index: Int) -> Color {
        //                   U  L  F  R  B  D
        let ColorPosition = [0, 4, 2, 1, 5, 3]
        return RCColor(ColorPosition[index])
    }
    
#if os(macOS)
    init(uiColor: UIColor) {
        self.init(uiColor)
    }
#endif
}
```



# 3. 展開図の描画

先の データ構造 と 配色情報 に基づいて、次の様に描画します。

_回転したときに、キューブのどの面がどこに移動したのか を分かり易くするため、数字を入れます。_

![展開図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/463d6c1e-21c9-43a7-bc87-1e5fdfc51808.png)




・描画の考え方

下図の様に、3つのパートに分けて 縦に並べて描画します。
各パートは、ダミーも含めて 複数の面を横に並べて描画します。よって、N×3×3 の三重ループとなります。

![描画.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/ce908be1-8baa-4fba-b5e0-8acccea0575f.png)




```swift:展開図の描画
struct RCUnfoldedDiagramView: View {
    let size: CGFloat
    @Binding var rcData: RCData
    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 0) {
        //part1
                ForEach(-1 ..< 1, id: \.self) { face in
                    VStack(spacing: 0) {
                        ForEach(0 ..< 3, id: \.self) { row in
                            HStack(spacing: 0) {
                                ForEach(0 ..< 3, id: \.self) { col in
                                    CubePieceView(number: rcData[0][row][col], size: size, isDummy: face == -1)
                                }
                            }
                        }
                    }
                }
                Spacer()
            }
            HStack(spacing: 0) {
        //part2
                ForEach(1 ..< 5, id: \.self) { face in
                    VStack(spacing: 0) {
                        ForEach(0 ..< 3, id: \.self) { row in
                            HStack(spacing: 0) {
                                ForEach(0 ..< 3, id: \.self) { col in
                                    CubePieceView(number: rcData[face][row][col], size: size, isDummy: false)
                                }
                            }
                        }
                    }
                }
                Spacer()
            }
            HStack(spacing: 0) {
        //part3
                ForEach(4 ..< 6, id: \.self) { face in
                    VStack(spacing: 0) {
                        ForEach(0 ..< 3, id: \.self) { row in
                            HStack(spacing: 0) {
                                ForEach(0 ..< 3, id: \.self) { col in
                                    CubePieceView(number: rcData[face][row][col], size: size, isDummy: face == 4)
                                }
                            }
                        }
                    }
                }
                Spacer()
            }
            Spacer()
        }
        .padding()
    }
    struct CubePieceView: View {
        let number: Int, size: CGFloat, isDummy: Bool
        var body: some View {
            ZStack {
                if !isDummy {
                    RoundedRectangle(cornerRadius: 4)
                        .fill(Color.RCColorPosition(number / 9))
                        .stroke(.black, lineWidth: 2)
                    Text("\(number + 1)")
                        .foregroundColor(.black)
                } else {
                    EmptyView()
                }
            }
            .frame(width: size, height: size)
        }
    }
}
```

各パートのネストが深くなっていますが、やっていることは（以下のように）わりと単純です。

```swift
//part X
for face
    for row
        for col
            if not dummy
                number = rcData[face][row][col]
                color = Color.RCColorPosition(number / 9)
                RoundedRectangle().fill(color)
                Text(number + 1)
            else
                EmptyView
```



# 「回転」の実装

#### ・ 面の回転

先の 3x3面の`RCFace`型を拡張して、
時計回り(`cw`)に90°、反時計回り(`ccw`)に90°回転させる関数（`computed property`）を実装します。

```swift
extension RCFace {
    //clockwise turn
    var cw: Self { 
        var result = self
        for row in self.indices {
            for col in self[row].indices {
                result[col][self[row].count - 1 - row] = self[row][col]
            }
        }
        return result
    }
    //counterclockwise turn
    var ccw: Self {
        var result = self
        for row in self.indices {
            for col in self[row].indices {
                result[self[row].count - 1 - col][row] = self[row][col]
            }
        }
        return result
    }
}
```


#### ・ルービックキューブの回転

回転する軸、向き、回転対象(Layer) を以下の様に定義します。

||CW　│　CCW<br>（対象Layer：0~2）|
|-|:-:|
|X軸|![回転x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/241b34e2-e242-4db0-aa71-b55dfd9b33ba.png)|
|Y軸|![回転y.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/48bde66d-7281-4bf3-96a4-072c9ea3f814.png)|
|Z軸|![回転z.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/9be72776-aefa-4c8b-a0cd-a1c9b4c2976f.png)|


#### ・rotate

回転させる関数を実装します。

```swift
func rotate(_ data: RCData, _ xyz: Int, _ layer: Int, _ direction: Int) -> RCData {
    //xyz       0:x, 1:y, 2:z
    //layer     0~2
    //direction 0:cw, 1:ccw
    var cube = data
    switch (xyz, direction) {
        case (0, 0): //x cw
            if layer == 0 {
                cube[1] = cube[1].ccw
            } else if layer == 2 {
                cube[3] = cube[3].cw
            }
            let v0 = cube[0]
            for r in 0 ..< 3 {
                cube[0][r][layer] = cube[2][r][layer]
            }
            for r in 0 ..< 3 {
                cube[2][2 - r][layer] = cube[5][2 - r][layer]
            }
            for r in 0 ..< 3 {
                cube[5][r][layer] = cube[4][2 - r][2 - layer]
            }
            for r in 0 ..< 3 {
                cube[4][r][2 - layer] = v0[2 - r][layer]
            }
            
        case (0, 1): //x ccw
            if layer == 0 {
                cube[1] = cube[1].cw
            } else if layer == 2 {
                cube[3] = cube[3].ccw
            }
            let v0 = cube[0]
            for r in 0 ..< 3 {
                cube[0][r][layer] = cube[4][2 - r][2 - layer]
            }
            for r in 0 ..< 3 {
                cube[4][r][2 - layer] = cube[5][2 - r][layer]
            }
            for r in 0 ..< 3 {
                cube[5][r][layer] = cube[2][r][layer]
            }
            for r in 0 ..< 3 {
                cube[2][2 - r][layer] = v0[2 - r][layer]
            }
            
        case (1, 0): //y cw
            if layer == 0 {
                cube[0] = cube[0].cw
            } else if layer == 2 {
                cube[5] = cube[5].ccw
            }
            let v2 = cube[2]
            for r in 0 ..< 3 {
                cube[2][layer][r] = cube[3][layer][r]
            }
            for r in 0 ..< 3 {
                cube[3][layer][r] = cube[4][layer][r]
            }
            for r in 0 ..< 3 {
                cube[4][layer][r] = cube[1][layer][r]
            }
            for r in 0 ..< 3 {
                cube[1][layer][r] = v2[layer][r]
            }
            
        case (1, 1): //y ccw
            if layer == 0 {
                cube[0] = cube[0].ccw
            } else if layer == 2 {
                cube[5] = cube[5].cw
            }
            let v2 = cube[2]
            for r in 0 ..< 3 {
                cube[2][layer][r] = cube[1][layer][r]
            }
            for r in 0 ..< 3 {
                cube[1][layer][r] = cube[4][layer][r]
            }
            for r in 0 ..< 3 {
                cube[4][layer][r] = cube[3][layer][r]
            }
            for r in 0 ..< 3 {
                cube[3][layer][r] = v2[layer][r]
            }
            
        case (2, 0): //z cw
            if layer == 0 {
                cube[2] = cube[2].cw
            } else if layer == 2 {
                cube[4] = cube[4].ccw
            }
            let v0 = cube[0]
            for r in 0 ..< 3 {
                cube[0][2 - layer][2 - r] = cube[1][r][2 - layer]
            }
            for r in 0 ..< 3 {
                cube[1][2 - r][2 - layer] = cube[5][layer][2 - r]
            }
            for r in 0 ..< 3 {
                cube[5][layer][r] = cube[3][2 - r][layer]
            }
            for r in 0 ..< 3 {
                cube[3][r][layer] = v0[2 - layer][r]
            }
            
        case (2, 1): //z ccw
            if layer == 0 {
                cube[2] = cube[2].ccw
            } else if layer == 2 {
                cube[4] = cube[4].cw
            }
            let v0 = cube[0]
            for r in 0 ..< 3 {
                cube[0][2 - layer][r] = cube[3][r][layer]
            }
            for r in 0 ..< 3 {
                cube[3][r][layer] = cube[5][layer][2 - r]
            }
            for r in 0 ..< 3 {
                cube[5][layer][r] = cube[1][r][2 - layer]
            }
            for r in 0 ..< 3 {
                cube[1][2 - r][2 - layer] = v0[2 - layer][r]
            }
            
        default:
            assertionFailure("invalid rotate (xyz:\(xyz), layer:\(layer), dir:\(direction))")
            break
    }
    return cube
}
```


# Playground で回転を試す

これまでのコードの動作確認をします。

_Xcode Playground の`Live View`を有効にします。_

![liveView.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/96ddadcf-2fbe-4ab4-96c5-dd99eee859fd.png)




```swift:Playground
import PlaygroundSupport
import SwiftUI

struct ContentView: View {
    @State var rcData = RCDataInitialPattern
    var body: some View {
        RCUnfoldedDiagramView(size: CGFloat(40), rcData: $rcData)
        Spacer()
        HStack {
            Button("x 0 cw ", action: { rcData = rotate(rcData, 0, 0, 0) })
            Button("x 1 cw ", action: { rcData = rotate(rcData, 0, 1, 0) })
            Button("x 2 cw ", action: { rcData = rotate(rcData, 0, 2, 0) })
            Button("x 0 ccw", action: { rcData = rotate(rcData, 0, 0, 1) })
            Button("x 1 ccw", action: { rcData = rotate(rcData, 0, 1, 1) })
            Button("x 2 ccw", action: { rcData = rotate(rcData, 0, 2, 1) })
        }
        HStack {
            Button("y 0 cw ", action: { rcData = rotate(rcData, 1, 0, 0) })
            Button("y 1 cw ", action: { rcData = rotate(rcData, 1, 1, 0) })
            Button("y 2 cw ", action: { rcData = rotate(rcData, 1, 2, 0) })
            Button("y 0 ccw", action: { rcData = rotate(rcData, 1, 0, 1) })
            Button("y 1 ccw", action: { rcData = rotate(rcData, 1, 1, 1) })
            Button("y 2 ccw", action: { rcData = rotate(rcData, 1, 2, 1) })
        }
        HStack {
            Button("z 0 cw ", action: { rcData = rotate(rcData, 2, 0, 0) })
            Button("z 1 cw ", action: { rcData = rotate(rcData, 2, 1, 0) })
            Button("z 2 cw ", action: { rcData = rotate(rcData, 2, 2, 0) })
            Button("z 0 ccw", action: { rcData = rotate(rcData, 2, 0, 1) })
            Button("z 1 ccw", action: { rcData = rotate(rcData, 2, 1, 1) })
            Button("z 2 ccw", action: { rcData = rotate(rcData, 2, 2, 1) })
        }
        Button("Reset", action: { rcData = RCDataInitialPattern })
    }
}


PlaygroundPage.current.setLiveView(ContentView())
```

iPad の Swift Playground でも問題なく動きます。

![ipad0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/2c7dd74f-9539-4134-be7e-729a17c48eda.png)



ただし、上記コードの先頭と末尾の次の2行を消してください。
（エラーが出るので、コメントアウトする）

```swift
//import PlaygroundSupport

//PlaygroundPage.current.setLiveView(ContentView())
```


# 回転記号

今後、『回転記号』 に対応した 回転を実装する予定です。

回転記号は、次のサイトの内容を参考にします。

https://tribox.com/3x3x3/solution/notation/





<b>以上</b>