three-rubiks-cube
Version:
a extension THREE.CSS3DObject for making rubik's cube
658 lines (615 loc) • 24.8 kB
JavaScript
import * as THREE from 'three';
import {CSS3DObject} from 'three/examples/jsm/renderers/CSS3DRenderer'
const NNNCube = function NNNCube(){
THREE.Group.apply(this);
};
NNNCube.prototype = Object.create(THREE.Group.prototype);
NNNCube.prototype.constructor = NNNCube;
const Cube333 = function Cube333(options){
NNNCube.apply(this);
console.log("**********************************")
console.log("%c THREE JS RUBIKS CUBE!", 'background: #222; color: #bada55')
console.log("**********************************")
this.options = options;
this._coordInfo = {
x : 0,
l : -1,
r : 1,
f : 1,
b : -1,
u : -1,
d : 1,
};
this._blocks = [
["luf", "ruf", "rub", "lub"],
["ldf", "rdf", "rdb", "ldb"],
["lxf", "rxf", "rxb", "lxb"],
["xdf", "rdx", "xdb", "ldx"],
["xuf", "rux", "xub", "lux"],
["xxf", "rxx", "xxb", "lxx"],
["xux"],
["xdx"]
];
this._blocks.forEach(function(coordsArr){
coordsArr.forEach(function(coordString){
const coords = coordString.split("");
const coordVector = new THREE.Vector3(this._coordInfo[coords[0]], this._coordInfo[coords[1]], this._coordInfo[coords[2]]);
const block = this._createBlock(this.options, coordVector);
this.add(block);
block.userData = {
clicked : false,
origin : coordString
}
block.name = coordString; //init coord String
block.position.x = coordVector.x * this.options.size.width;
block.position.y = -coordVector.y * this.options.size.height;
block.position.z = coordVector.z * this.options.size.depth;
this._initMouseEventListener(block);
}.bind(this))
}.bind(this));
this._operationsArray = [];
this.addEventListener("operation", function(event){
//그룹 없애기
let tempOperationGroup = this.parent.getObjectByName("tempOperationGroup");
if(tempOperationGroup){
if(tempOperationGroup.children.length){
while(tempOperationGroup.children.length){
this.attach(tempOperationGroup.children[tempOperationGroup.children.length - 1])
}
}
this.parent.remove(tempOperationGroup);
}
if(event.index > this._operationsArray.length - 1) {
this._operationsArray = [];
this.dispatchEvent({type : "operationCompleted"});
return
}
const operationInfo = this._makeOperationInfo(this._operationsArray[event.index]);
tempOperationGroup = this.parent.getObjectByName("tempOperationGroup");
function inOutQuad(n){
n *= 2;
if (n < 1) return 0.5 * n * n;
return - 0.5 * (--n * (n - 2) - 1);
}
let start = null;
function animation(timestamp){
if (!start) start = timestamp;
const progress = timestamp - start;
if (progress <= this.options.animateDuration) {
tempOperationGroup.setRotationFromAxisAngle(operationInfo.axis, inOutQuad(progress / 1000) * operationInfo.angle * Math.PI / 180);
window.requestAnimationFrame(animation.bind(this));
}else{
this._operator(this._operationsArray[event.index], tempOperationGroup);
this.dispatchEvent({type : "operation", index : event.index + 1});
return;
}
}
window.requestAnimationFrame(animation.bind(this));
}.bind(this))
};
Cube333.prototype = Object.create(NNNCube.prototype);
Cube333.prototype.constructor = Cube333;
Cube333.prototype._createBlock = function _createBlock(options, orientation){
const commonStyle = {
position: "absolute",
backgroundColor: this.options.blockColor,
borderRadius : "30px",
width: options.size.width + "px",
height: options.size.height + "px",
};
const blockElement = document.createElement("div");
blockElement.style.position = "absolute";
blockElement.style.display = "block";
blockElement.style.transformStyle = "preserve-3d";
const faceElement = document.createElement("div");
Object.assign(faceElement.style, commonStyle);
/** top - down face **/
const top = faceElement.cloneNode(true);
top.className = "u";
top.style.transform = "translateX("+ (-options.size.width / 2) + "px)" + "translateY(" + (-options.size.height) + "px)" + "rotate3d(1, 0, 0, -90deg) ";
blockElement.appendChild(top);
const down = faceElement.cloneNode(true);
down.className = "d";
down.style.transform = "translateX("+ (-options.size.width / 2) + "px)" + "rotate3d(1, 0, 0, 90deg) ";
blockElement.appendChild(down);
/** left - right face **/
const left = faceElement.cloneNode(true);
left.className = "l";
left.style.transform = "translateX(" + (options.size.width * -1) + "px)"+ "translateY(" + (-options.size.height/ 2) + "px)" + "rotate3d(0, 1, 0, 90deg)";
blockElement.appendChild(left);
const right = faceElement.cloneNode(true);
right.className = "r";
right.style.transform =" rotate3d(0, 1, 0, 90deg)"+ "translateY(" + (-options.size.height / 2) + "px)";
blockElement.appendChild(right);
/** front - back face **/
const front = faceElement.cloneNode(true);
front.className = "f";
front.style.transform = "translateX("+ (-options.size.width / 2) + "px)" +"translateZ(" + (options.size.depth / 2) + "px)" + "translateY(" + (-options.size.height / 2) + "px)" + "rotate3d(0, 1, 0, 0deg)";
blockElement.appendChild(front);
const back = faceElement.cloneNode(true);
back.className = "b";
back.style.transform = "translateX("+ (-options.size.width / 2) + "px)" + "translateZ(" + (options.size.depth * -1 / 2) + "px)" + "translateY(" + (-options.size.height / 2) + "px)" + "rotate3d(0, 1, 0, -180deg)";
blockElement.appendChild(back);
// see through block plane
const xplane = faceElement.cloneNode(true);
xplane.className = "z";
xplane.style.borderRadius = "0px";
xplane.style.width = options.size.width * 0.98 + "px";
xplane.style.height = options.size.height * 0.98 + "px";
xplane.style.transform = "translateX("+ (-options.size.width / 2) + "px)" + "translateY(" + (-options.size.height / 2) + "px)" + "rotate3d(1, 0, 0, -90deg) ";
blockElement.appendChild(xplane);
const yplane = faceElement.cloneNode(true);
yplane.className = "y";
yplane.style.borderRadius = "0px";
yplane.style.transform = "translateX("+ (-options.size.width / 2) + "px)" + "translateY(" + (-options.size.height / 2) + "px)";
yplane.style.width = options.size.width + "px";
yplane.style.height = options.size.height * 0.98 + "px";
blockElement.appendChild(yplane);
const zplane = faceElement.cloneNode(true);
zplane.className = "x";
zplane.style.borderRadius = "0px";
zplane.style.transform = "translateX("+ (-options.size.width / 2) + "px)" + "translateY(" + (-options.size.height / 2) + "px)" + "rotate3d(0, 1, 0, 90deg) ";
zplane.style.width = options.size.width * 0.98 + "px";
zplane.style.height = options.size.height * 0.98 + "px";
blockElement.appendChild(zplane);
const m_top = faceElement.cloneNode(true);
m_top.style.backgroundColor = '';
m_top.style.visibility = options.mirror ? 'visible' : 'hidden';
m_top.className = "mu";
m_top.style.transform = "translateX("+ (-options.size.width / 2) + "px)" + "translateY(" + (-options.size.height * 4) + "px)" + "rotate3d(1, 0, 0, -90deg) ";
blockElement.appendChild(m_top);
const m_down = faceElement.cloneNode(true);
m_down.style.backgroundColor = '';
m_down.style.visibility = options.mirror ? 'visible' : 'hidden';
m_down.className = "md";
m_down.style.transform = "translateX("+ (-options.size.width / 2) + "px)"+ "translateY(" + (options.size.height * 3) + "px)" + "rotate3d(1, 0, 0, 90deg) ";
blockElement.appendChild(m_down);
const m_left = faceElement.cloneNode(true);
m_left.style.backgroundColor = '';
m_left.style.visibility = options.mirror ? 'visible' : 'hidden';
m_left.className = "ml";
m_left.style.transform = "translateX(" + (options.size.width * -4) + "px)"+ "translateY(" + (-options.size.height/ 2) + "px)" + "rotate3d(0, 1, 0, 90deg)";
blockElement.appendChild(m_left);
const m_right = faceElement.cloneNode(true);
m_right.style.backgroundColor = '';
m_right.style.visibility = options.mirror ? 'visible' : 'hidden';
m_right.className = "mr";
m_right.style.transform = "translateX(" + (options.size.width * 4) + "px)"+ "translateY(" + (-options.size.height / 2) + "px)" + "rotate3d(0, 1, 0, 90deg)";
blockElement.appendChild(m_right);
const m_front = faceElement.cloneNode(true);
m_front.className = "mf";
m_front.style.visibility = options.mirror ? 'visible' : 'hidden';
m_front.style.backgroundColor = '';
m_front.style.transform = "translateX("+ (-options.size.width / 2) + "px)" +"translateZ(" + (options.size.depth * 3.5) + "px)" + "translateY(" + (-options.size.height / 2) + "px)" + "rotate3d(0, 1, 0, 0deg)";
blockElement.appendChild(m_front);
const m_back = faceElement.cloneNode(true);
m_back.className = "mb";
m_back.style.backgroundColor = '';
m_back.style.visibility = options.mirror ? 'visible' : 'hidden';
m_back.style.transform = "translateX("+ (-options.size.width / 2) + "px)" + "translateZ(" + (options.size.depth * -3.5) + "px)" + "translateY(" + (-options.size.height / 2) + "px)" + "rotate3d(0, 1, 0, -180deg)";
blockElement.appendChild(m_back);
const block = new CSS3DObject(blockElement);
block.rotateOnAxis(new THREE.Vector3(0, 1, 0), this.children.length * 90 * Math.PI / 180);
return block;
};
Cube333.prototype._operator = function _operator(operation, operationGroup){
/***
* U operation, Y operation. E operation
* r -> f - > l -> b
* D operation
* f -> r -> b -> l
* R operation, X operation, S operation
* f -> u -> b -> d
* L operation
* f -> d -> b - > u
* F operation, Z operation, M operation
* l -> u -> r -> d
* B operation
* u -> r -> d -> l
*/
const operations = {
"U" : ["r", "f", "l", "b"],
"u" : ["r", "f", "l", "b"],
"y" : ["r", "f", "l", "b"],
"E" : ["f", "r", "b", "l"],
"D" : ["f", "r", "b", "l"],
"d" : ["f", "r", "b", "l"],
"R" : ["f", "u", "b", "d"],
"r" : ["f", "u", "b", "d"],
"x" : ["f", "u", "b", "d"],
"S" : ["r", "u", "l", "d"],
"L" : ["f", "d", "b", "u"],
"l" : ["f", "d", "b", "u"],
"F" : ["l", "u", "r", "d"],
"f" : ["l", "u", "r", "d"],
"Z" : ["l", "u", "r", "d"],
"M" : ["u", "f", "d", "b"],
"B" : ["d", "r", "u", "l"]
};
const oprs = operations[operation.replace('2', "").replace("'", "")];
const isDouble = operation.includes("2") ? 2 : 1;
const isAntiCock = operation.includes("'") ? -1 : 1;
operationGroup.children.forEach(function(block){
let name = "";
Array.from(block.name).forEach(function(string, i){
const index = oprs.indexOf(string);
if(index > -1){
const newIndex = (index + (isDouble * isAntiCock));
const finalIndex = newIndex % oprs.length;
name += finalIndex >= 0 ? oprs[finalIndex] : oprs[oprs.length + finalIndex]
}else{
name += string
}
});
block.name = name;
})
};
Cube333.prototype._parseOperations = function _parseOperations(operations){
function parse(operation) {
const array = []
for (let i = 0; i < operation.length; i++) {
if (operation[i] === "'" || operation[i] === '2') array[array.length - 1] += operation[i]
else array.push(operation[i])
}
return array;
}
return parse(operations);
};
Cube333.prototype._makeOperationInfo = function getOperationBlockGroup(operationString){
const tempOperationGroup = new THREE.Group();
tempOperationGroup.name = "tempOperationGroup";
this.parent.add(tempOperationGroup);
let axis;
let angle = 90;
if(operationString.includes("R")){
this.children
.filter(function(child){ return Array.from(child.name).includes("r")})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(1, 0, 0);
angle = -90;
}else if(operationString.includes("r")){
this.children
.filter(function(child){ return !Array.from(child.name).includes("l")})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(1, 0, 0);
angle = -90;
}else if(operationString.includes("L")){
this.children
.filter(function(child){ return Array.from(child.name).includes("l");})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(-1, 0, 0);
angle = -90;
}else if(operationString.includes("l")){
this.children
.filter(function(child){ return !Array.from(child.name).includes("r");})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(-1, 0, 0);
angle = -90;
}else if(operationString.includes("F")){
this.children
.filter(function(child){ return Array.from(child.name).includes("f");})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, 0, -1);
angle = 90;
}else if(operationString.includes("f")){
this.children
.filter(function(child){ return !Array.from(child.name).includes("b");})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, 0, -1);
angle = 90;
}else if(operationString.includes("B")){
this.children
.filter(function(child){ return Array.from(child.name).includes("b");})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, 0, 1);
angle = 90;
}else if(operationString.includes("b")){
this.children
.filter(function(child){ return !Array.from(child.name).includes("f");})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, 0, 1);
angle = 90;
}else if(operationString.includes("U")){
this.children
.filter(function(child){ return Array.from(child.name).includes("u")})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, 1, 0);
angle = -90;
}else if(operationString.includes("u")){
this.children
.filter(function(child){ return !Array.from(child.name).includes("d")})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, 1, 0);
angle = -90;
}else if(operationString.includes("D")){
this.children
.filter(function(child){ return Array.from(child.name).includes("d");})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, -1, 0);
angle = -90;
}else if(operationString.includes("d")){
this.children
.filter(function(child){ return !Array.from(child.name).includes("u");})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, -1, 0);
angle = -90;
}else if(operationString.includes("M")){
this.children
.filter(function(child){
return (child.name.match(/x/g) || []).length === 1
&& (child.name.match(/f|u|b|d/g) || []).length === 2
|| ((child.name.match(/x/g) || []).length === 2 && (/f|u|b|d/.test(child.name)))})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(1, 0, 0);
angle = 90;
}else if(operationString.includes("E")){
this.children
.filter(function(child){
return (child.name.match(/x/g) || []).length === 1
&& (child.name.match(/r|b|l|f/g) || []).length === 2
|| ((child.name.match(/x/g) || []).length === 2 && (/r|b|l|f/.test(child.name)))})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, 1, 0);
angle = 90;
}else if(operationString.includes("S")){
this.children
.filter(function(child){
return (child.name.match(/x/g) || []).length === 1
&& (child.name.match(/r|u|l|d/g) || []).length === 2
|| ((child.name.match(/x/g) || []).length === 2 && (/r|u|l|d/.test(child.name)))})
.forEach(function(child){tempOperationGroup.add(child)}.bind(this));
axis = new THREE.Vector3(0, 0, 1);
angle = 90;
}else if(operationString.includes("x")){
while(this.children.length){
tempOperationGroup.add(this.children[this.children.length - 1])
}
axis = new THREE.Vector3(1, 0, 0);
angle = -90;
}
else if(operationString.includes("y")){
while(this.children.length){
tempOperationGroup.add(this.children[this.children.length - 1])
}
axis = new THREE.Vector3(0, 1, 0);
angle = 90;
}else if(operationString.includes("z")){
while(this.children.length){
tempOperationGroup.add(this.children[this.children.length - 1])
}
axis = new THREE.Vector3(0, 0, 1);
angle = 90;
}
if(operationString.includes("'")){
angle = angle * -1;
}
if(operationString.includes("2")){
angle = angle * 2;
}
return {
angle : angle,
axis : axis
}
};
Cube333.prototype.animate = function animate(operations){
this._operationsArray = this._parseOperations(operations);
this.dispatchEvent({ type: 'operation', index: 0 })
};
Cube333.prototype.operate = function operate(operations, animation){
this._operationsArray = this._parseOperations(operations);
this._operationsArray.forEach((operation)=>{
let tempOperationGroup = this.parent.getObjectByName("tempOperationGroup");
if(tempOperationGroup){
if(tempOperationGroup.children.length){
while(tempOperationGroup.children.length){
this.attach(tempOperationGroup.children[tempOperationGroup.children.length - 1])
}
}
this.parent.remove(tempOperationGroup);
}
const operationInfo = this._makeOperationInfo(operation);
tempOperationGroup = this.parent.getObjectByName("tempOperationGroup");
tempOperationGroup.setRotationFromAxisAngle(operationInfo.axis, operationInfo.angle * Math.PI / 180);
this._operator(operation, tempOperationGroup);
})
this._operationsArray = [];
}
Cube333.prototype._refreshBlocks = function _refreshBlocks(){
while(this.children.length){
this.remove(this.children[this.children.length - 1])
}
this._blocks.forEach(function(coordsArr){
coordsArr.forEach(function(coordString){
const coords = coordString.split("");
const coordVector = new THREE.Vector3(this._coordInfo[coords[0]], this._coordInfo[coords[1]], this._coordInfo[coords[2]]);
const block = this._createBlock(this.options, coordVector);
this.add(block);
block.userData = {
clicked : false,
origin : coordString
};
block.name = coordString; //init coord String
block.position.x = coordVector.x * this.options.size.width;
block.position.y = -coordVector.y * this.options.size.height;
block.position.z = coordVector.z * this.options.size.depth;
this._initMouseEventListener(block);
}.bind(this))
}.bind(this));
}
Cube333.prototype.toggleMirror = function toggleMirror(toggle){
const faces = ["f", "b", "u", "d", "l", "r"]
this.children.forEach((child)=>{
faces.forEach((face)=>{
const result = child.element.getElementsByClassName("m" + face);
if(result.length){
result[0].style.visibility = toggle ? 'visible' : 'hidden';
}
})
})
}
Cube333.prototype._initMouseEventListener = function _initMouseEventListener(block){
block.element.addEventListener('mouseover', (event) => {
if(!this.options.hoverEnabled) return;
if(block.userData.clicked) return;
Array.from(block.element.children).forEach((child)=>{
if(!child.className.includes('m'))
child.style.backgroundColor = this.options.hoverColor;
});
})
block.element.addEventListener('mouseout', (event)=>{
if(!this.options.hoverEnabled) return;
if(block.userData.clicked) return;
Array.from(block.element.children).forEach((child)=>{
if(!child.className.includes('m'))
child.style.backgroundColor = this.options.blockColor;
})
})
block.element.addEventListener('mousedown', (event) => {
if(!this.options.clickEnabled) return;
if(!block.userData.clicked){
Array.from(block.element.children).forEach((child)=>{
if(!child.className.includes('m'))
child.style.backgroundColor = this.options.clickColor;
});
}else{
Array.from(block.element.children).forEach((child)=>{
if(!child.className.includes('m'))
child.style.backgroundColor = this.options.blockColor;
})
}
block.userData.clicked = !block.userData.clicked;
})
}
Cube333.prototype.unselectAllBlock = function unselectAllBlock(){
this.children.forEach((block)=>{
if(block.userData.clicked){
Array.from(block.element.children).forEach((child)=>{
if(!child.className.includes('m'))
child.style.backgroundColor = this.options.blockColor;
});
block.userData.clicked = false;
}
})
}
/**
* param
* blockColor : "black, white etc..",
* size : {
* width :
* height :
* depth :
* },
* fitment : ,
* stickerColorset : ,
* mirror : true / false
* */
const RubiksCube = function(options){
Cube333.apply(this, [options]);
this._stickers = {
"luf" : ["borderBottomRightRadius"],
"ldf" : ["borderTopRightRadius"],
"lxf" : ["borderTopRightRadius", "borderBottomRightRadius"],
"xdf" : ["borderTopRightRadius", "borderTopLeftRadius"],
"xuf" : ["borderBottomRightRadius", "borderBottomLeftRadius"]
};
this._blocks.forEach((arr) => {
arr.forEach((coord, i) => {
this._attachSticker(coord, arr[0], i);
})
})
};
RubiksCube.prototype = Object.create(Cube333.prototype);
RubiksCube.prototype.constructor = RubiksCube;
RubiksCube.prototype._attachSticker = function _attachSticker(realCoord, stickerCoord, idx){
function faceRotate(text, i){
const arr = ["f", "r", "b", "l"];
const index = arr.indexOf(text);
if(index >= 0){
const indexSum = index + i;
return arr[indexSum % arr.length];
}
else return text;
}
const block = this.getObjectByName(realCoord); //real coord
const blockCoord = block.name.split("");
const sticker_coords = stickerCoord.split(""); //sticker coord -> must transform! by faceRotate function
const style = {
width : this.options.fitment === "fully_fitted" ? "97%" : "90%",
height : this.options.fitment === "fully_fitted" ? "97%" : "90%",
margin : this.options.fitment === "fully_fitted" ? "5px" : "8px",
borderRadius : "30px"
};
const element = block.element;
sticker_coords.forEach(function(sc){
const face = element.getElementsByClassName(sc);
if(face.length){
Object.assign(style, {
backgroundColor : this.options.stickerColorSet[faceRotate(sc, idx % 4)],
})
if(this._stickers[stickerCoord]){
this._stickers[stickerCoord].forEach(function(radius){
style[radius] = this.options.size.width * 0.3 + "px";
}.bind(this))
}else{
style["borderRadius"] = "50% 50% 50% 50%";
}
const sticker = document.createElement("div");
sticker.className = "sticker_" + faceRotate(sc, idx % 4);
Object.assign(sticker.style, style)
face[0].appendChild(sticker);
const mirrorFace = element.getElementsByClassName('m'+sc);
if(mirrorFace.length){
const mirrorSticker = sticker.cloneNode();
mirrorFace[0].appendChild(mirrorSticker);
}
}
}.bind(this))
// remove empty mirror
const emptyMirror = Array.from(element.children)
.filter((el)=> el.className.includes('m') && el.children.length === 0)
.forEach((el)=> el.remove());
};
RubiksCube.prototype.refreshCube = function refreshCube(){
this._refreshBlocks();
this._blocks.forEach((arr) => {
arr.forEach((coord, i) => {
this._attachSticker(coord, arr[0], i);
})
})
}
RubiksCube.prototype.refreshStickers = function refreshStickers(){
const faces =this.children.map((child)=>{
return Array.from(child.element.children).filter((childEl)=> {
return !childEl.className.includes("x")
&& !childEl.className.includes("y")
&& !childEl.className.includes("z")
})
})
const facesString = ["f", "b", "u", "d", "r","l"];
faces.forEach((face)=>{
face.forEach((child)=>{
facesString.forEach((faceString)=>{
const sticker = child.getElementsByClassName("sticker_" + faceString)
if(sticker.length){
sticker[0].style.backgroundColor = this.options.stickerColorSet[faceString];
}
})
})
})
}
RubiksCube.prototype.refreshBlockColor = function refreshBlockColor(){
const faces =this.children.forEach((child)=>{
if(!child.userData.clicked){
Array.from(child.element.children).forEach((childEl)=> {
if(!childEl.className.includes('m'))
childEl.style.backgroundColor = this.options.blockColor;
})
}else{
Array.from(child.element.children).forEach((childEl)=> {
if(childEl.className.includes('x') || childEl.className.includes('y') || childEl.className.includes('z'))
childEl.style.backgroundColor = this.options.blockColor;
})
}
})
}
export default RubiksCube ;