Refactor: Implement SmartCube renderer, improve UI styling, and fix gaps

This commit is contained in:
2026-02-22 04:35:59 +00:00
parent 57abfd6b80
commit b5ddc21662
4168 changed files with 763782 additions and 1008 deletions

345
src/utils/CubeModel.js Normal file
View File

@@ -0,0 +1,345 @@
/**
* Dedicated 3x3x3 Rubik's Cube Model
*
* Representation:
* A collection of 27 Cubie objects, each with position (x, y, z) and face colors.
* Coordinate System:
* x: Left (-1) to Right (1)
* y: Bottom (-1) to Top (1)
* z: Back (-1) to Front (1)
*
* This logical model maintains the state of the cube and handles rotations.
*/
export const COLORS = {
WHITE: 'white',
YELLOW: 'yellow',
ORANGE: 'orange',
RED: 'red',
GREEN: 'green',
BLUE: 'blue',
BLACK: 'black'
};
export const FACES = {
UP: 'up',
DOWN: 'down',
LEFT: 'left',
RIGHT: 'right',
FRONT: 'front',
BACK: 'back',
};
// Standard Face Colors (Solved State)
const SOLVED_COLORS = {
[FACES.UP]: COLORS.WHITE,
[FACES.DOWN]: COLORS.YELLOW,
[FACES.LEFT]: COLORS.ORANGE,
[FACES.RIGHT]: COLORS.RED,
[FACES.FRONT]: COLORS.GREEN,
[FACES.BACK]: COLORS.BLUE,
};
class Cubie {
constructor(id, x, y, z) {
this.id = id;
this.x = x;
this.y = y;
this.z = z;
this.faces = {
[FACES.UP]: COLORS.BLACK,
[FACES.DOWN]: COLORS.BLACK,
[FACES.LEFT]: COLORS.BLACK,
[FACES.RIGHT]: COLORS.BLACK,
[FACES.FRONT]: COLORS.BLACK,
[FACES.BACK]: COLORS.BLACK,
};
this.initColors();
}
// Set initial colors based on position in solved state
initColors() {
if (this.y === 1) this.faces[FACES.UP] = SOLVED_COLORS[FACES.UP];
if (this.y === -1) this.faces[FACES.DOWN] = SOLVED_COLORS[FACES.DOWN];
if (this.x === -1) this.faces[FACES.LEFT] = SOLVED_COLORS[FACES.LEFT];
if (this.x === 1) this.faces[FACES.RIGHT] = SOLVED_COLORS[FACES.RIGHT];
if (this.z === 1) this.faces[FACES.FRONT] = SOLVED_COLORS[FACES.FRONT];
if (this.z === -1) this.faces[FACES.BACK] = SOLVED_COLORS[FACES.BACK];
}
}
export class CubeModel {
constructor() {
this.size = 3;
this.cubies = [];
this.init();
}
init() {
this.cubies = [];
let id = 0;
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
this.cubies.push(new Cubie(id++, x, y, z));
}
}
}
}
reset() {
this.init();
}
/**
* Rotates a layer around an axis.
* @param {string} axis - 'x', 'y', 'z'
* @param {number} index - -1 (Left/Bottom/Back), 0 (Middle), 1 (Right/Top/Front)
* @param {number} direction - 1 (CW), -1 (CCW) relative to axis positive direction
*/
rotateLayer(axis, index, direction) {
// Determine the relevant cubies in the slice
const slice = this.cubies.filter(c => c[axis] === index);
// Coordinate rotation (Matrix Logic)
// 90 deg CW rotation formulas:
// X-Axis: (y, z) -> (-z, y)
// Y-Axis: (x, z) -> (z, -x)
// Z-Axis: (x, y) -> (-y, x)
// Note: direction 1 is usually CCW in math (right hand rule around axis).
// Let's verify standard:
// Right Hand Rule with Thumb along Axis: Fingers curl in Positive Rotation direction.
// X (Right): Curl from Y (Up) to Z (Front). (0,1,0)->(0,0,1).
// y' = -z, z' = y?
// Let's check (0,1): y=1, z=0 -> y'=0, z'=1. Correct.
// So if direction is 1 (Positive/CW around axis):
// X: y' = -z, z' = y
// Y: z' = -x, x' = z
// Z: x' = -y, y' = x
// If direction is -1: Inverse.
slice.forEach(cubie => {
this._rotateCubieCoordinates(cubie, axis, direction);
this._rotateCubieFaces(cubie, axis, direction);
});
}
_rotateCubieCoordinates(cubie, axis, direction) {
const { x, y, z } = cubie;
if (axis === 'x') {
if (direction === 1) {
cubie.y = -z;
cubie.z = y;
} else {
cubie.y = z;
cubie.z = -y;
}
} else if (axis === 'y') {
if (direction === 1) {
cubie.z = -x;
cubie.x = z;
} else {
cubie.z = x;
cubie.x = -z;
}
} else if (axis === 'z') {
if (direction === 1) { // CW
cubie.x = -y;
cubie.y = x;
} else { // CCW
cubie.x = y;
cubie.y = -x;
}
}
}
_rotateCubieFaces(cubie, axis, direction) {
// When a cubie rotates, its faces move to new positions.
// We swap the COLORS on the faces.
// Example: Rotate X (Roll Forward). Up Face becomes Front Face.
// So new Front Color = old Up Color.
// cubie.faces[FRONT] = old_faces[UP]
const f = { ...cubie.faces };
if (axis === 'x') {
if (direction === 1) { // Up -> Front -> Down -> Back -> Up
cubie.faces[FACES.FRONT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.BACK];
// Left/Right unchanged in position, but might rotate? No, faces are solid colors.
} else { // Up -> Back -> Down -> Front -> Up
cubie.faces[FACES.BACK] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.FRONT];
}
} else if (axis === 'y') {
if (direction === 1) { // Front -> Right -> Back -> Left -> Front
cubie.faces[FACES.RIGHT] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.RIGHT];
cubie.faces[FACES.LEFT] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.LEFT];
} else { // Front -> Left -> Back -> Right -> Front
cubie.faces[FACES.LEFT] = f[FACES.FRONT];
cubie.faces[FACES.BACK] = f[FACES.LEFT];
cubie.faces[FACES.RIGHT] = f[FACES.BACK];
cubie.faces[FACES.FRONT] = f[FACES.RIGHT];
}
} else if (axis === 'z') {
if (direction === 1) { // Up -> Right -> Down -> Left -> Up (CW)
cubie.faces[FACES.RIGHT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.RIGHT];
cubie.faces[FACES.LEFT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.LEFT];
} else { // Up -> Left -> Down -> Right -> Up (CCW)
cubie.faces[FACES.LEFT] = f[FACES.UP];
cubie.faces[FACES.DOWN] = f[FACES.LEFT];
cubie.faces[FACES.RIGHT] = f[FACES.DOWN];
cubie.faces[FACES.UP] = f[FACES.RIGHT];
}
}
}
toCubies() {
// Return copy of state for rendering
// CubeCSS expects array of objects with x, y, z, faces
return this.cubies.map(c => ({
id: c.id,
x: c.x,
y: c.y,
z: c.z,
faces: { ...c.faces }
}));
}
/**
* Applies a standard Rubik's Cube move
* @param {string} move - e.g. "U", "R'", "F2"
*/
applyMove(move) {
const base = move[0];
const modifier = move.substring(1);
let direction = -1; // Standard CW is -1 for U, L, F, B? Let's check.
// Direction Mapping based on rotateLayer Math:
// X(1) = CW (Up->Front)
// Y(1) = CW (Right->Front)
// Z(1) = CCW (Right->Up)
// R (CW around X): 1
// L (CW around -X): -1
// U (CW around Y): 1
// D (CW around -Y): -1
// F (CW around Z): -1 (since Z(1) is CCW)
// B (CW around -Z): 1 (since Z(1) is CW around -Z)
switch (base) {
case 'U': direction = 1; break;
case 'D': direction = -1; break;
case 'L': direction = -1; break;
case 'R': direction = 1; break;
case 'F': direction = -1; break;
case 'B': direction = 1; break;
}
if (modifier === "'") direction *= -1;
if (modifier === '2') {
// 2 moves. Direction doesn't matter for 180, but let's keep it.
// We will call rotateLayer twice.
}
const count = modifier === '2' ? 2 : 1;
for (let i = 0; i < count; i++) {
switch (base) {
case 'U': this.rotateLayer('y', 1, direction); break;
case 'D': this.rotateLayer('y', -1, direction); break;
case 'L': this.rotateLayer('x', -1, direction); break;
case 'R': this.rotateLayer('x', 1, direction); break;
case 'F': this.rotateLayer('z', 1, direction); break;
case 'B': this.rotateLayer('z', -1, direction); break;
}
}
}
// Debug printer for tests
toString() {
let out = "Cube State (3x3x3):\n";
// We can print faces.
// Order: U, D, F, B, L, R
const printFace = (face, name) => {
out += `Face ${name}:\n`;
// Grid 3x3.
// U: y=1. x from -1 to 1. z from -1 to 1.
// Coordinate mapping depends on face.
// Let's iterate standard grid rows/cols.
for (let r = 0; r < 3; r++) {
let rowStr = "";
for (let c = 0; c < 3; c++) {
let cubie;
// Map r,c to x,y,z based on face
if (face === FACES.UP) { // y=1. r=0->z=-1 (Back), r=2->z=1 (Front). c=0->x=-1 (Left).
// Standard U face view: Top Left is Back Left (-1, 1, -1).
// Row 0 (Top of U face) is Back.
// Row 2 (Bottom of U face) is Front.
cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1)); // Wait.
// Back is z=-1. Front is z=1.
// Visual Top of U face is Back (z=-1).
// Visual Bottom of U face is Front (z=1).
cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1 - 2 * r)); // Complicated.
// Let's just find by strict coordinates
// r=0 -> z=-1. r=1 -> z=0. r=2 -> z=1.
// c=0 -> x=-1. c=1 -> x=0. c=2 -> x=1.
cubie = this.cubies.find(cu => cu.y === 1 && cu.x === (c - 1) && cu.z === (r - 1));
}
else if (face === FACES.DOWN) cubie = this.cubies.find(cu => cu.y === -1 && cu.x === (c - 1) && cu.z === (1 - r)); // Down View?
else if (face === FACES.FRONT) cubie = this.cubies.find(cu => cu.z === 1 && cu.x === (c - 1) && cu.y === (1 - r));
else if (face === FACES.BACK) cubie = this.cubies.find(cu => cu.z === -1 && cu.x === (1 - c) && cu.y === (1 - r));
else if (face === FACES.LEFT) cubie = this.cubies.find(cu => cu.x === -1 && cu.z === (1 - c) && cu.y === (1 - r)); // Left view z order?
else if (face === FACES.RIGHT) cubie = this.cubies.find(cu => cu.x === 1 && cu.z === (c - 1) && cu.y === (1 - r));
if (cubie) {
rowStr += cubie.faces[face][0].toUpperCase() + " ";
} else {
rowStr += "? ";
}
}
out += rowStr + "\n";
}
out += "\n";
};
printFace(FACES.UP, 'U');
printFace(FACES.DOWN, 'D');
printFace(FACES.FRONT, 'F');
printFace(FACES.BACK, 'B');
printFace(FACES.LEFT, 'L');
printFace(FACES.RIGHT, 'R');
return out;
}
scramble(n = 20) {
const axes = ['x', 'y', 'z'];
const indices = [-1, 1]; // Usually rotate outer layers for scramble
// Actually, scrambling usually involves random face moves (U, D, L, R, F, B)
// U: y=1, dir -1 (Standard CW)
// D: y=-1, dir 1
// L: x=-1, dir -1
// R: x=1, dir 1
// F: z=1, dir 1
// B: z=-1, dir -1
// We can just generate random rotateLayer calls
for (let i = 0; i < n; i++) {
const axis = axes[Math.floor(Math.random() * axes.length)];
// Allow middle layer? Standard Scramble usually doesnt.
const index = indices[Math.floor(Math.random() * indices.length)];
const dir = Math.random() > 0.5 ? 1 : -1;
this.rotateLayer(axis, index, dir);
}
}
}