1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-14

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

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

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

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

1. 展開図の配置

・配置

6面を以下のように配置します。
 Up
 Left Front Right Back
 Down

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

・データ構造

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

//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. 配色

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

 

 

(RGB値は独自)

//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

・描画の考え方

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

描画.png

展開図の描画
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)
        }
    }
}

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

//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)を実装します。

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
(対象Layer:0~2)
X軸 回転x.png
Y軸 回転y.png
Z軸 回転z.png

・rotate

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

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

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

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

//import PlaygroundSupport

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

回転記号

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

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

以上

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?