fix: invert rotation logic and add debug tools
This commit is contained in:
@@ -1,20 +1,16 @@
|
||||
import MatrixLib from 'matrix-js';
|
||||
|
||||
const Matrix = MatrixLib && MatrixLib.default ? MatrixLib.default : MatrixLib;
|
||||
|
||||
const mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
// Enum for colors/faces
|
||||
// Enum for colors
|
||||
export const COLORS = {
|
||||
WHITE: 0,
|
||||
YELLOW: 1,
|
||||
ORANGE: 2,
|
||||
RED: 3,
|
||||
GREEN: 4,
|
||||
BLUE: 5,
|
||||
WHITE: 'white',
|
||||
YELLOW: 'yellow',
|
||||
ORANGE: 'orange',
|
||||
RED: 'red',
|
||||
GREEN: 'green',
|
||||
BLUE: 'blue',
|
||||
BLACK: 'black'
|
||||
};
|
||||
|
||||
// Faces mapping
|
||||
// Faces enum
|
||||
export const FACES = {
|
||||
UP: 'up',
|
||||
DOWN: 'down',
|
||||
@@ -24,229 +20,481 @@ export const FACES = {
|
||||
BACK: 'back',
|
||||
};
|
||||
|
||||
// Initial state: Solved cube
|
||||
const INITIAL_STATE = {
|
||||
[FACES.UP]: Array(3).fill().map(() => Array(3).fill(COLORS.WHITE)),
|
||||
[FACES.DOWN]: Array(3).fill().map(() => Array(3).fill(COLORS.YELLOW)),
|
||||
[FACES.LEFT]: Array(3).fill().map(() => Array(3).fill(COLORS.ORANGE)),
|
||||
[FACES.RIGHT]: Array(3).fill().map(() => Array(3).fill(COLORS.RED)),
|
||||
[FACES.FRONT]: Array(3).fill().map(() => Array(3).fill(COLORS.GREEN)),
|
||||
[FACES.BACK]: Array(3).fill().map(() => Array(3).fill(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,
|
||||
};
|
||||
|
||||
// Assign initial colors based on position (Solved State)
|
||||
if (y === 1) this.faces[FACES.UP] = COLORS.WHITE;
|
||||
if (y === -1) this.faces[FACES.DOWN] = COLORS.YELLOW;
|
||||
if (x === -1) this.faces[FACES.LEFT] = COLORS.ORANGE;
|
||||
if (x === 1) this.faces[FACES.RIGHT] = COLORS.RED;
|
||||
if (z === 1) this.faces[FACES.FRONT] = COLORS.GREEN;
|
||||
if (z === -1) this.faces[FACES.BACK] = COLORS.BLUE;
|
||||
}
|
||||
}
|
||||
|
||||
export class Cube {
|
||||
constructor() {
|
||||
this.cubies = [];
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
// Deep copy initial state
|
||||
this.state = JSON.parse(JSON.stringify(INITIAL_STATE));
|
||||
}
|
||||
|
||||
// Rotate a 3x3 matrix 90 degrees clockwise
|
||||
_rotateMatrixCW(matrix) {
|
||||
// CW Rotation: Transpose -> Reverse Rows (or Reverse Cols -> Transpose?)
|
||||
// CW: (x,y) -> (y, -x).
|
||||
// Transpose: (x,y) -> (y,x).
|
||||
// Reverse rows?
|
||||
// 1 2 3 1 4 7 7 4 1
|
||||
// 4 5 6 -> 2 5 8 -> 8 5 2
|
||||
// 7 8 9 3 6 9 9 6 3
|
||||
// Transpose then reverse each row.
|
||||
// Matrix-js trans() returns new matrix.
|
||||
|
||||
const m = Matrix(matrix);
|
||||
const t = m.trans();
|
||||
// matrix-js doesn't have reverse rows method directly on instance usually,
|
||||
// but returns array of arrays on simple access? No, it returns object.
|
||||
// Let's use basic array ops on the transposed data.
|
||||
|
||||
// Matrix(m) creates a matrix object.
|
||||
// m.trans() returns a matrix object with transposed data.
|
||||
// We need to get data back to reverse rows.
|
||||
|
||||
// Check matrix-js API.
|
||||
// Usually it doesn't expose data directly property?
|
||||
// Let's assume we can get it via simple property or method.
|
||||
// Docs say: Matrix(data) -> data.
|
||||
// But let's check what trans() returns.
|
||||
|
||||
// Safe approach:
|
||||
// Transpose using matrix-js
|
||||
const transposed = t;
|
||||
|
||||
// Convert back to array if needed.
|
||||
// If matrix-js is just a wrapper, maybe it's iterable?
|
||||
// Or we assume `t` is the array? No, `Matrix` is a factory.
|
||||
// `Matrix(A).trans()` returns a new Matrix.
|
||||
|
||||
// If we look at matrix-js source or docs:
|
||||
// It seems `trans()` returns the array of arrays directly in some versions?
|
||||
// Or we need to access it.
|
||||
|
||||
// Let's assume standard behavior: we need to extract data.
|
||||
// But wait, the user asked to use `matrix-js`.
|
||||
// If I cannot verify API, I might break it.
|
||||
// `matrix-js` 1.x:
|
||||
// var Matrix = require("matrix-js");
|
||||
// var A = Matrix([[1,2],[3,4]]);
|
||||
// var B = A.trans();
|
||||
// B is a matrix-js object? Or array?
|
||||
// Actually, `matrix-js` often returns the array result for operations like trans().
|
||||
// Let's assume it returns the array of arrays.
|
||||
|
||||
// Verify by checking if it has .map
|
||||
if (Array.isArray(t)) {
|
||||
return t.map(row => [...row].reverse());
|
||||
}
|
||||
// If it's an object, we might need to find how to extract.
|
||||
// But since I installed it, I can assume standard usage.
|
||||
// Most lightweight libs return arrays.
|
||||
|
||||
// Let's try to use it as if it returns an array.
|
||||
return t.map(row => [...row].reverse());
|
||||
}
|
||||
|
||||
// Rotate a 3x3 matrix 90 degrees counter-clockwise
|
||||
_rotateMatrixCCW(matrix) {
|
||||
// CCW Rotation: Transpose -> Reverse Cols?
|
||||
// Or Reverse Rows -> Transpose?
|
||||
// 1 2 3 3 2 1 3 6 9
|
||||
// 4 5 6 -> 6 5 4 -> 2 5 8
|
||||
// 7 8 9 9 8 7 1 4 7
|
||||
// Reverse rows then transpose.
|
||||
|
||||
// Reverse rows first (manual)
|
||||
const reversed = matrix.map(row => [...row].reverse());
|
||||
// Then transpose using matrix-js
|
||||
return Matrix(reversed).trans();
|
||||
}
|
||||
|
||||
// Rotate a face (layer)
|
||||
// direction: 1 (CW), -1 (CCW)
|
||||
rotate(face, direction = 1) {
|
||||
const s = this.state;
|
||||
|
||||
// 1. Rotate the face matrix itself
|
||||
if (direction === 1) {
|
||||
s[face] = this._rotateMatrixCW(s[face]);
|
||||
} else {
|
||||
s[face] = this._rotateMatrixCCW(s[face]);
|
||||
}
|
||||
|
||||
// 2. Rotate adjacent strips
|
||||
let cycle = [];
|
||||
|
||||
// Helper to get index with mod (not strictly needed but good practice)
|
||||
// We can use mod(idx, 3) if we iterate.
|
||||
|
||||
switch (face) {
|
||||
case FACES.FRONT:
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'row', index: 2, reverse: false },
|
||||
{ face: FACES.RIGHT, type: 'col', index: 0, reverse: false },
|
||||
{ face: FACES.DOWN, type: 'row', index: 0, reverse: true }, // Reversed
|
||||
{ face: FACES.LEFT, type: 'col', index: 2, reverse: true } // Reversed col (bottom-to-top)
|
||||
];
|
||||
break;
|
||||
|
||||
case FACES.BACK:
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'row', index: 0, reverse: true },
|
||||
{ face: FACES.LEFT, type: 'col', index: 0, reverse: false },
|
||||
{ face: FACES.DOWN, type: 'row', index: 2, reverse: false },
|
||||
{ face: FACES.RIGHT, type: 'col', index: 2, reverse: false }
|
||||
];
|
||||
break;
|
||||
|
||||
case FACES.UP:
|
||||
cycle = [
|
||||
{ face: FACES.FRONT, type: 'row', index: 0, reverse: false },
|
||||
{ face: FACES.LEFT, type: 'row', index: 0, reverse: false },
|
||||
{ face: FACES.BACK, type: 'row', index: 0, reverse: false },
|
||||
{ face: FACES.RIGHT, type: 'row', index: 0, reverse: false }
|
||||
];
|
||||
break;
|
||||
|
||||
case FACES.DOWN:
|
||||
cycle = [
|
||||
{ face: FACES.FRONT, type: 'row', index: 2, reverse: false },
|
||||
{ face: FACES.RIGHT, type: 'row', index: 2, reverse: false },
|
||||
{ face: FACES.BACK, type: 'row', index: 2, reverse: false },
|
||||
{ face: FACES.LEFT, type: 'row', index: 2, reverse: false }
|
||||
];
|
||||
break;
|
||||
|
||||
case FACES.LEFT:
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'col', index: 0, reverse: false },
|
||||
{ face: FACES.FRONT, type: 'col', index: 0, reverse: false },
|
||||
{ face: FACES.DOWN, type: 'col', index: 0, reverse: false },
|
||||
{ face: FACES.BACK, type: 'col', index: 2, reverse: true }
|
||||
];
|
||||
break;
|
||||
|
||||
case FACES.RIGHT:
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'col', index: 2, reverse: false },
|
||||
{ face: FACES.BACK, type: 'col', index: 0, reverse: true },
|
||||
{ face: FACES.DOWN, type: 'col', index: 2, reverse: false },
|
||||
{ face: FACES.FRONT, type: 'col', index: 2, reverse: false }
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
if (direction === -1) {
|
||||
cycle.reverse();
|
||||
}
|
||||
|
||||
this._applyCycle(cycle, direction, face);
|
||||
}
|
||||
|
||||
_getSegment(face, type, index) {
|
||||
const s = this.state[face];
|
||||
if (type === 'row') {
|
||||
return [...s[index]];
|
||||
} else {
|
||||
return s.map(row => row[index]);
|
||||
}
|
||||
}
|
||||
|
||||
_setSegment(face, type, index, values) {
|
||||
const s = this.state[face];
|
||||
if (type === 'row') {
|
||||
s[index] = [...values];
|
||||
} else {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
s[i][index] = values[i];
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_applyCycle(cycle, direction, faceName) {
|
||||
const values = cycle.map(c => {
|
||||
let val = this._getSegment(c.face, c.type, c.index);
|
||||
return c.reverse ? val.reverse() : val;
|
||||
});
|
||||
|
||||
const newValues = [];
|
||||
|
||||
// Shift values
|
||||
// Last element moves to first position
|
||||
const last = values[values.length - 1];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
// Calculate previous index with modulo
|
||||
// i=0 -> prev=3. i=1 -> prev=0.
|
||||
const prevIdx = mod(i - 1, values.length);
|
||||
newValues[i] = values[prevIdx];
|
||||
// Perform a standard move (U, D, L, R, F, B, M, E, S, x, y, z)
|
||||
// Modifier: ' (prime) or 2 (double)
|
||||
move(moveStr) {
|
||||
let move = moveStr[0];
|
||||
let modifier = moveStr.length > 1 ? moveStr[1] : '';
|
||||
|
||||
let direction = 1; // CW
|
||||
let times = 1;
|
||||
|
||||
if (modifier === "'") {
|
||||
direction = -1;
|
||||
} else if (modifier === '2') {
|
||||
times = 2;
|
||||
}
|
||||
|
||||
// Apply new values with reverse logic if needed
|
||||
cycle.forEach((c, i) => {
|
||||
let val = newValues[i];
|
||||
if (c.reverse) val = val.reverse();
|
||||
this._setSegment(c.face, c.type, c.index, val);
|
||||
|
||||
// Standard Notation Mapping to (axis, index, direction)
|
||||
// Note: Direction 1 in rotateLayer is "Positive Axis Rotation".
|
||||
// We need to map Standard CW to Axis Direction.
|
||||
|
||||
// U (Up): y=1. Top face CW.
|
||||
// Looking from Top (y+), CW is Rotation around Y (-). Wait.
|
||||
// Right Hand Rule on Y axis: Thumb up, fingers curl CCW.
|
||||
// So Positive Y Rotation is CCW from Top.
|
||||
// So U (CW) is Negative Y Rotation.
|
||||
// Let's verify _rotateCubiePosition for 'y'.
|
||||
// dir > 0 (Pos): nx = z, nz = -x. (z, -x).
|
||||
// (1,0) -> (0,-1). Right -> Back.
|
||||
// Top View: Right is 3 o'clock. Back is 12 o'clock? No, Back is Up.
|
||||
// Top View:
|
||||
// B (z=-1)
|
||||
// L(x=-1) R(x=1)
|
||||
// F (z=1)
|
||||
// Right (x=1) -> Back (z=-1).
|
||||
// This is CCW.
|
||||
// So `direction > 0` (Positive Y) is CCW from Top.
|
||||
// Standard U is CW. So U is `direction = -1`.
|
||||
|
||||
// D (Down): y=-1. Bottom face CW.
|
||||
// Looking from Bottom (y-), CW.
|
||||
// If I look from bottom, Y axis points away.
|
||||
// Positive Y is CCW from Top -> CW from Bottom?
|
||||
// Let's check.
|
||||
// Pos Y: Right -> Back.
|
||||
// Bottom View: Right is Right. Back is "Down"?
|
||||
// It's confusing.
|
||||
// Let's use simple logic: D moves same direction as U' (visually from side?). No.
|
||||
// U and D turn "same way" if you hold cube? No, opposite layers turn opposite relative to axis.
|
||||
// D (CW) matches Y (Pos) ?
|
||||
// Let's check movement of Front face on D.
|
||||
// D moves Front -> Right.
|
||||
// Y (Pos) moves Front (z=1) -> Right (x=1)?
|
||||
// Pos Y: (0, 1) -> (1, 0). z=1 -> x=1.
|
||||
// Yes. Front -> Right.
|
||||
// So D (CW) = Y (Pos). `direction = 1`.
|
||||
|
||||
// L (Left): x=-1. Left face CW.
|
||||
// L moves Front -> Down.
|
||||
// X (Pos) moves Front (z=1) -> Up (y=1)?
|
||||
// _rotateCubiePosition 'x':
|
||||
// dir > 0: ny = -z. z=1 -> y=-1 (Down).
|
||||
// So X (Pos) moves Front -> Down.
|
||||
// So L (CW) = X (Pos). `direction = 1`.
|
||||
|
||||
// R (Right): x=1. Right face CW.
|
||||
// R moves Front -> Up.
|
||||
// X (Pos) moves Front -> Down.
|
||||
// So R (CW) = X (Neg). `direction = -1`.
|
||||
|
||||
// F (Front): z=1. Front face CW.
|
||||
// F moves Up -> Right.
|
||||
// Z (Pos) moves Up (y=1) -> Left (x=-1)?
|
||||
// _rotateCubiePosition 'z':
|
||||
// dir > 0: nx = -y. y=1 -> x=-1 (Left).
|
||||
// So Z (Pos) moves Up -> Left.
|
||||
// F (CW) moves Up -> Right.
|
||||
// So F (CW) = Z (Neg). `direction = -1`.
|
||||
// Wait. My `rotateLayer` logic for Z was flipped in previous turn to match Visual.
|
||||
// Let's re-read `_rotateCubieFaces` for Z.
|
||||
// dir > 0 (CCW in Math/Pos): Left <- Up. Up moves to Left.
|
||||
// So Pos Z moves Up to Left.
|
||||
// F (CW) needs Up to Right.
|
||||
// So F (CW) is Neg Z. `direction = -1`.
|
||||
|
||||
// B (Back): z=-1. Back face CW.
|
||||
// B moves Up -> Left.
|
||||
// Z (Pos) moves Up -> Left.
|
||||
// So B (CW) = Z (Pos). `direction = 1`.
|
||||
|
||||
const layerOps = [];
|
||||
|
||||
switch (move) {
|
||||
case 'U': layerOps.push({ axis: 'y', index: 1, dir: -1 }); break;
|
||||
case 'D': layerOps.push({ axis: 'y', index: -1, dir: 1 }); break;
|
||||
case 'L': layerOps.push({ axis: 'x', index: -1, dir: 1 }); break;
|
||||
case 'R': layerOps.push({ axis: 'x', index: 1, dir: -1 }); break;
|
||||
case 'F': layerOps.push({ axis: 'z', index: 1, dir: -1 }); break;
|
||||
case 'B': layerOps.push({ axis: 'z', index: -1, dir: 1 }); break;
|
||||
|
||||
// Slices
|
||||
case 'M': // Middle (between L and R), follows L direction
|
||||
layerOps.push({ axis: 'x', index: 0, dir: 1 }); break;
|
||||
case 'E': // Equator (between U and D), follows D direction
|
||||
layerOps.push({ axis: 'y', index: 0, dir: 1 }); break;
|
||||
case 'S': // Standing (between F and B), follows F direction
|
||||
layerOps.push({ axis: 'z', index: 0, dir: -1 }); break;
|
||||
|
||||
// Whole Cube Rotations
|
||||
case 'x': // Follows R
|
||||
layerOps.push({ axis: 'x', index: -1, dir: -1 });
|
||||
layerOps.push({ axis: 'x', index: 0, dir: -1 });
|
||||
layerOps.push({ axis: 'x', index: 1, dir: -1 });
|
||||
break;
|
||||
case 'y': // Follows U
|
||||
layerOps.push({ axis: 'y', index: -1, dir: -1 });
|
||||
layerOps.push({ axis: 'y', index: 0, dir: -1 });
|
||||
layerOps.push({ axis: 'y', index: 1, dir: -1 });
|
||||
break;
|
||||
case 'z': // Follows F
|
||||
layerOps.push({ axis: 'z', index: -1, dir: -1 });
|
||||
layerOps.push({ axis: 'z', index: 0, dir: -1 });
|
||||
layerOps.push({ axis: 'z', index: 1, dir: -1 });
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply operations
|
||||
for (let i = 0; i < times; i++) {
|
||||
layerOps.forEach(op => {
|
||||
this.rotateLayer(op.axis, op.index, op.dir * direction);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate a layer
|
||||
// axis: 'x', 'y', 'z'
|
||||
// Helper: Rotate a 2D matrix
|
||||
// direction: 1 (CW), -1 (CCW)
|
||||
_rotateMatrix(matrix, direction) {
|
||||
const N = matrix.length;
|
||||
// Transpose
|
||||
for (let i = 0; i < N; i++) {
|
||||
for (let j = i; j < N; j++) {
|
||||
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse Rows (for CW) or Columns (for CCW)
|
||||
if (direction > 0) {
|
||||
// CW: Reverse each row
|
||||
matrix.forEach(row => row.reverse());
|
||||
} else {
|
||||
// CCW: Reverse columns (or Reverse rows before transpose? No.)
|
||||
// Transpose + Reverse Rows = CW.
|
||||
// Transpose + Reverse Cols = CCW?
|
||||
// Let's check:
|
||||
// [1 2] T [1 3] RevCol [3 1] -> CCW?
|
||||
// [3 4] [2 4] [4 2]
|
||||
// 1 (0,0) -> (0,1). (Top-Left -> Top-Right). This is CW.
|
||||
// Wait.
|
||||
// CW: (x,y) -> (y, -x).
|
||||
// (0,0) -> (0, 0).
|
||||
// (1,0) -> (0, -1).
|
||||
|
||||
// Let's stick to standard:
|
||||
// CW: Transpose -> Reverse Rows.
|
||||
// CCW: Reverse Rows -> Transpose.
|
||||
|
||||
// Since I already transposed:
|
||||
// To get CCW from Transpose:
|
||||
// [1 2] T [1 3]
|
||||
// [3 4] [2 4]
|
||||
// Target CCW:
|
||||
// [2 4]
|
||||
// [1 3]
|
||||
// This is reversing columns of Transpose.
|
||||
// Or reversing rows of original, then transpose.
|
||||
|
||||
// Since I modify in place and already transposed:
|
||||
// I need to reverse columns.
|
||||
// Alternatively, re-implement:
|
||||
|
||||
// Undo transpose for CCW case and do correct order?
|
||||
// No, let's just reverse columns.
|
||||
for (let i = 0; i < N; i++) {
|
||||
for (let j = 0; j < N / 2; j++) {
|
||||
[matrix[j][i], matrix[N - 1 - j][i]] = [matrix[N - 1 - j][i], matrix[j][i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// index: -1, 0, 1
|
||||
// direction: 1 (Positive Axis), -1 (Negative Axis)
|
||||
rotateLayer(axis, index, direction) {
|
||||
// 1. Select cubies in the layer
|
||||
const layerCubies = this.cubies.filter(c => c[axis] === index);
|
||||
|
||||
// 2. Map cubies to 3x3 Matrix based on Axis View
|
||||
// We need a consistent mapping from (u, v) -> Matrix[row][col]
|
||||
// such that RotateMatrix(CW) corresponds to Physical CW Rotation.
|
||||
|
||||
// Grid coordinates:
|
||||
// Row: 0..2, Col: 0..2
|
||||
|
||||
// Mapping function: returns {row, col} for a cubie
|
||||
// Inverse function: updates cubie coordinates from {row, col}
|
||||
|
||||
let mapToGrid, updateFromGrid;
|
||||
|
||||
if (axis === 'z') {
|
||||
// Front (z=1): X=Right, Y=Up.
|
||||
// Matrix: Row 0 is Top (y=1). Col 0 is Left (x=-1).
|
||||
mapToGrid = (c) => ({ row: 1 - c.y, col: c.x + 1 });
|
||||
updateFromGrid = (c, row, col) => { c.y = 1 - row; c.x = col - 1; };
|
||||
} else if (axis === 'x') {
|
||||
// Right (x=1): Y=Up, Z=Back?
|
||||
// CW Rotation around X (Right face):
|
||||
// Up -> Front -> Down -> Back.
|
||||
// Matrix: Row 0 is Top (y=1).
|
||||
// Col 0 is Front (z=1)?
|
||||
// If Col 0 is Front, Col 2 is Back (z=-1).
|
||||
// Let's check CW:
|
||||
// Top (y=1) -> Front (z=1).
|
||||
// Matrix (0, ?) -> (?, 0).
|
||||
// (0, 1) [Top-Center] -> (1, 0) [Front-Center].
|
||||
// Row 0 -> Col 0. (Transpose).
|
||||
// Then Reverse Rows?
|
||||
// (0, 1) -> (1, 0).
|
||||
// (0, 0) [Top-Front] -> (0, 0) [Front-Top]? No.
|
||||
// Top-Front (y=1, z=1).
|
||||
// Rot X CW: (y, z) -> (-z, y).
|
||||
// (1, 1) -> (-1, 1). (Back-Top).
|
||||
// Wait.
|
||||
// Rot X CW:
|
||||
// Y->Z->-Y->-Z.
|
||||
// Up(y=1) -> Front(z=1)? No.
|
||||
// Standard Axis Rotation (Right Hand Rule):
|
||||
// Thumb +X. Fingers Y -> Z.
|
||||
// So Y axis moves towards Z axis.
|
||||
// (0, 1, 0) -> (0, 0, 1).
|
||||
// Up -> Front.
|
||||
// So Top (y=1) moves to Front (z=1).
|
||||
|
||||
// Let's map:
|
||||
// Row 0 (Top, y=1). Row 2 (Bottom, y=-1).
|
||||
// Col 0 (Front, z=1). Col 2 (Back, z=-1).
|
||||
mapToGrid = (c) => ({ row: 1 - c.y, col: 1 - c.z });
|
||||
updateFromGrid = (c, row, col) => { c.y = 1 - row; c.z = 1 - col; };
|
||||
} else if (axis === 'y') {
|
||||
// Up (y=1): Z=Back, X=Right.
|
||||
// Rot Y CW:
|
||||
// Z -> X.
|
||||
// Back (z=-1) -> Right (x=1).
|
||||
// Matrix: Row 0 (Back, z=-1). Row 2 (Front, z=1).
|
||||
// Col 0 (Left, x=-1). Col 2 (Right, x=1).
|
||||
mapToGrid = (c) => ({ row: c.z + 1, col: c.x + 1 });
|
||||
updateFromGrid = (c, row, col) => { c.z = row - 1; c.x = col - 1; };
|
||||
}
|
||||
|
||||
// 3. Create Matrix
|
||||
const matrix = Array(3).fill(null).map(() => Array(3).fill(null));
|
||||
layerCubies.forEach(c => {
|
||||
const { row, col } = mapToGrid(c);
|
||||
matrix[row][col] = c;
|
||||
});
|
||||
|
||||
// 4. Rotate Matrix
|
||||
// Note: Direction 1 is Physical CW (CCW in Math).
|
||||
// Mapping analysis shows that for all axes (X, Y, Z),
|
||||
// Physical CW corresponds to Matrix CW.
|
||||
// However, rotateLayer receives direction -1 for CW (from move() notation).
|
||||
// _rotateMatrix expects direction 1 for CW.
|
||||
// So we must invert the direction for all axes.
|
||||
|
||||
const matrixDirection = -direction;
|
||||
this._rotateMatrix(matrix, matrixDirection);
|
||||
|
||||
// 5. Update Cubie Coordinates
|
||||
for (let r = 0; r < 3; r++) {
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const cubie = matrix[r][c];
|
||||
if (cubie) {
|
||||
updateFromGrid(cubie, r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Rotate Faces of each cubie
|
||||
layerCubies.forEach(cubie => {
|
||||
this._rotateCubieFaces(cubie, axis, direction);
|
||||
});
|
||||
}
|
||||
|
||||
_rotateCubieFaces(cubie, axis, direction) {
|
||||
const f = { ...cubie.faces };
|
||||
|
||||
// Helper to swap faces
|
||||
// We map: newFace <- oldFace
|
||||
|
||||
// Axis X Rotation (Right/Left)
|
||||
// CW (dir > 0): Up -> Front -> Down -> Back -> Up
|
||||
if (axis === 'x') {
|
||||
if (direction > 0) {
|
||||
// Corrected cycle for +X rotation:
|
||||
// Up face moves to Front face
|
||||
// Front face moves to Down face
|
||||
// Down face moves to Back face
|
||||
// Back face moves to Up face
|
||||
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];
|
||||
} else {
|
||||
// Reverse cycle for -X
|
||||
cubie.faces[FACES.UP] = f[FACES.FRONT];
|
||||
cubie.faces[FACES.FRONT] = f[FACES.DOWN];
|
||||
cubie.faces[FACES.DOWN] = f[FACES.BACK];
|
||||
cubie.faces[FACES.BACK] = f[FACES.UP];
|
||||
}
|
||||
}
|
||||
|
||||
// Axis Y Rotation (Up/Down)
|
||||
// CW (dir > 0): Front -> Right -> Back -> Left -> Front
|
||||
// Front -> Right, Right -> Back, Back -> Left, Left -> Front
|
||||
if (axis === 'y') {
|
||||
if (direction > 0) {
|
||||
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 {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
// Axis Z Rotation (Front/Back)
|
||||
// CW (dir > 0) in Math is CCW visually: Top -> Left -> Bottom -> Right -> Top
|
||||
if (axis === 'z') {
|
||||
if (direction > 0) {
|
||||
// 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];
|
||||
} else {
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current state as standard 6-face matrices (for display/export)
|
||||
getState() {
|
||||
const state = {
|
||||
[FACES.UP]: [[],[],[]],
|
||||
[FACES.DOWN]: [[],[],[]],
|
||||
[FACES.LEFT]: [[],[],[]],
|
||||
[FACES.RIGHT]: [[],[],[]],
|
||||
[FACES.FRONT]: [[],[],[]],
|
||||
[FACES.BACK]: [[],[],[]]
|
||||
};
|
||||
|
||||
this.cubies.forEach(c => {
|
||||
// Map x,y,z to matrix indices
|
||||
|
||||
// UP: y=1. row = z (-1->0, 0->1, 1->2)?
|
||||
// In `CubeCSS` I reversed this logic to match `Cube.js`.
|
||||
// Let's stick to standard visual mapping.
|
||||
// UP Face (Top View):
|
||||
// Row 0 is Back (z=-1). Row 2 is Front (z=1).
|
||||
// Col 0 is Left (x=-1). Col 2 is Right (x=1).
|
||||
if (c.y === 1) {
|
||||
const row = c.z + 1;
|
||||
const col = c.x + 1;
|
||||
state[FACES.UP][row][col] = c.faces[FACES.UP];
|
||||
}
|
||||
|
||||
// DOWN Face (Bottom View):
|
||||
// Usually "unfolded". Top of Down face is Front (z=1).
|
||||
// Row 0 is Front (z=1). Row 2 is Back (z=-1).
|
||||
// Col 0 is Left (x=-1). Col 2 is Right (x=1).
|
||||
if (c.y === -1) {
|
||||
const row = 1 - c.z;
|
||||
const col = c.x + 1;
|
||||
state[FACES.DOWN][row][col] = c.faces[FACES.DOWN];
|
||||
}
|
||||
|
||||
// FRONT Face (z=1):
|
||||
// Row 0 is Top (y=1). Row 2 is Bottom (y=-1).
|
||||
// Col 0 is Left (x=-1). Col 2 is Right (x=1).
|
||||
if (c.z === 1) {
|
||||
const row = 1 - c.y;
|
||||
const col = c.x + 1;
|
||||
state[FACES.FRONT][row][col] = c.faces[FACES.FRONT];
|
||||
}
|
||||
|
||||
// BACK Face (z=-1):
|
||||
// Viewed from Back.
|
||||
// Row 0 is Top (y=1).
|
||||
// Col 0 is Right (x=1) (Viewer's Left). Col 2 is Left (x=-1).
|
||||
if (c.z === -1) {
|
||||
const row = 1 - c.y;
|
||||
const col = 1 - c.x;
|
||||
state[FACES.BACK][row][col] = c.faces[FACES.BACK];
|
||||
}
|
||||
|
||||
// LEFT Face (x=-1):
|
||||
// Viewed from Left.
|
||||
// Row 0 is Top (y=1).
|
||||
// Col 0 is Back (z=-1). Col 2 is Front (z=1).
|
||||
if (c.x === -1) {
|
||||
const row = 1 - c.y;
|
||||
const col = c.z + 1;
|
||||
state[FACES.LEFT][row][col] = c.faces[FACES.LEFT];
|
||||
}
|
||||
|
||||
// RIGHT Face (x=1):
|
||||
// Viewed from Right.
|
||||
// Row 0 is Top (y=1).
|
||||
// Col 0 is Front (z=1). Col 2 is Back (z=-1).
|
||||
if (c.x === 1) {
|
||||
const row = 1 - c.y;
|
||||
const col = 1 - c.z;
|
||||
state[FACES.RIGHT][row][col] = c.faces[FACES.RIGHT];
|
||||
}
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user