
ここで扱うのは、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. 展開図の描画
先の データ構造 と 配色情報 に基づいて、次の様に描画します。
回転したときに、キューブのどの面がどこに移動したのか を分かり易くするため、数字を入れます。
・描画の考え方
下図の様に、3つのパートに分けて 縦に並べて描画します。
各パートは、ダミーも含めて 複数の面を横に並べて描画します。よって、N×3×3 の三重ループとなります。
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軸 | ![]() |
Y軸 | ![]() |
Z軸 | ![]() |
・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
を有効にします。
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 でも問題なく動きます。
ただし、上記コードの先頭と末尾の次の2行を消してください。
(エラーが出るので、コメントアウトする)
//import PlaygroundSupport
//PlaygroundPage.current.setLiveView(ContentView())
回転記号
今後、『回転記号』 に対応した 回転を実装する予定です。
回転記号は、次のサイトの内容を参考にします。
以上