-
+
-
-
-
-
{
:style="{ backgroundColor: color }">
-
@@ -546,7 +397,7 @@ const getCubieStyle = (cubie) => {
width: 300px;
height: 300px;
perspective: 900px;
- pointer-events: auto;
+ pointer-events: auto;
}
.cube-group {
@@ -576,8 +427,6 @@ const getCubieStyle = (cubie) => {
justify-content: center;
align-items: center;
backface-visibility: hidden; /* Optimization? Or we want to see inside? */
- /* We want solid cubies. So we need backfaces or 6 faces. */
- /* We are rendering 6 faces. */
}
.sticker-border {
@@ -586,9 +435,6 @@ const getCubieStyle = (cubie) => {
border: 2px solid rgba(0,0,0,0.5);
border-radius: 8px; /* Rounded sticker */
background: inherit; /* Sticker color */
- /* The face bg is the plastic color (black usually). */
- /* Here we set face bg to color directly. */
- /* Let's adjust: face bg = black. sticker-border bg = color. */
}
/* Face transforms relative to Cubie Center */
@@ -600,4 +446,3 @@ const getCubieStyle = (cubie) => {
.sticker-face.down { transform: rotateX(-90deg) translateZ(50px); }
-
diff --git a/src/composables/useCube.js b/src/composables/useCube.js
index cd639d9..dd10e96 100644
--- a/src/composables/useCube.js
+++ b/src/composables/useCube.js
@@ -1,70 +1,43 @@
import { ref, computed } from 'vue';
import { Cube, COLORS, FACES } from '../utils/Cube';
-// Map natural numbers (0-5) to CSS colors
-const COLOR_MAP = {
- [COLORS.WHITE]: 'white',
- [COLORS.YELLOW]: 'yellow',
- [COLORS.ORANGE]: 'orange',
- [COLORS.RED]: 'red',
- [COLORS.GREEN]: 'green',
- [COLORS.BLUE]: 'blue'
-};
-
export function useCube() {
const cube = ref(new Cube());
- // Expose state for rendering (flattened for Vue template simplicity or kept as matrix)
- // Let's expose matrix but maybe helper to flatten if needed?
- // The Cube class uses 3x3 matrices.
- // The Vue template currently iterates `cubeState.top` (array of 9).
- // We should adapt `cubeState` to match what the template expects OR update template.
- // The user asked to "Dostosować src/components/Main.vue do renderowania nowego modelu danych".
- // So we can expose the 3x3 matrices directly.
+ // Make cubies reactive so Vue tracks changes
+ // We can just expose the cube instance, but better to expose reactive properties
+ // Since `cube` is a ref, `cube.value.cubies` is not deeply reactive by default unless `cube.value` is reactive.
+ // But `ref` wraps the object. If we mutate properties of the object, it might not trigger.
+ // Let's rely on triggering updates manually or creating a new instance on reset.
+ // For rotation, we will force update.
- const cubeState = computed(() => cube.value.state);
+ const cubies = computed(() => cube.value.cubies);
+
+ // Compute the 6-face state matrix for display/debug
+ const cubeState = computed(() => cube.value.getState());
const initCube = () => {
cube.value.reset();
+ triggerUpdate();
};
- const rotateLayer = (layer, direction) => {
- // layer is string 'top', 'front' etc.
- // Map string to FACES constant if needed, but FACES values are 'up', 'down', etc.
- // The previous implementation used 'top', 'bottom', 'left', 'right', 'front', 'back'.
- // Cube.js uses 'up', 'down', 'left', 'right', 'front', 'back'.
-
- // Map legacy layer names to new Face names
- const layerMap = {
- 'top': FACES.UP,
- 'bottom': FACES.DOWN,
- 'left': FACES.LEFT,
- 'right': FACES.RIGHT,
- 'front': FACES.FRONT,
- 'back': FACES.BACK
- };
-
- const face = layerMap[layer];
- if (face) {
- cube.value.rotate(face, direction);
- // Trigger reactivity since Cube is a class and state is internal object
- // We made `cube` a ref, but mutating its internal state might not trigger update
- // unless we replace the state or use reactive().
- // `cube.value.rotate` mutates `this.state`.
- // We should probably make `cube.value.state` reactive or trigger update.
- cube.value = Object.assign(Object.create(Object.getPrototypeOf(cube.value)), cube.value);
- }
+ const triggerUpdate = () => {
+ // Force Vue to notice change
+ cube.value = Object.assign(Object.create(Object.getPrototypeOf(cube.value)), cube.value);
+ };
+
+ const rotateLayer = (axis, index, direction) => {
+ cube.value.rotateLayer(axis, index, direction);
+ triggerUpdate();
};
- // Helper to get flattened array for a face (if template wants it)
- // But better to update template to use 2D loop or flatten here.
- // Let's provide a helper or just let template handle it.
-
return {
+ cube,
+ cubies,
cubeState,
initCube,
rotateLayer,
- COLOR_MAP,
+ COLORS,
FACES
};
}
diff --git a/src/composables/useDebug.js b/src/composables/useDebug.js
new file mode 100644
index 0000000..b08236c
--- /dev/null
+++ b/src/composables/useDebug.js
@@ -0,0 +1,50 @@
+
+import { reactive, watch } from 'vue'
+
+const settings = reactive({
+ viewRotation: {
+ invertX: false, // Inverts Up/Down view rotation
+ invertY: false, // Inverts Left/Right view rotation (Drag Right -> Increase Angle -> Rotate Right)
+ speed: 0.5
+ },
+ dragMapping: {
+ // Multipliers for drag direction on faces
+ front: { x: 1, y: -1 }, // Changed x to 1
+ back: { x: 1, y: 1 },
+ right: { x: -1, y: 1 },
+ left: { x: -1, y: -1 },
+ up: { x: 1, y: 1 },
+ down: { x: -1, y: -1 }
+ },
+ physics: {
+ enabled: true,
+ tension: 200,
+ friction: 10 // Not currently used but good for future
+ }
+})
+
+// Persist to localStorage for convenience during reload
+const STORAGE_KEY = 'rubik-debug-settings-v2' // Changed key to force reset settings
+
+try {
+ const saved = localStorage.getItem(STORAGE_KEY)
+ if (saved) {
+ const parsed = JSON.parse(saved)
+ // Merge deeply? For now just top level sections
+ Object.assign(settings.viewRotation, parsed.viewRotation)
+ Object.assign(settings.dragMapping, parsed.dragMapping)
+ Object.assign(settings.physics, parsed.physics)
+ }
+} catch (e) {
+ console.warn('Failed to load debug settings', e)
+}
+
+watch(settings, (newSettings) => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newSettings))
+}, { deep: true })
+
+export function useDebug() {
+ return {
+ settings
+ }
+}
diff --git a/src/composables/useInteractionLogger.js b/src/composables/useInteractionLogger.js
new file mode 100644
index 0000000..221b86c
--- /dev/null
+++ b/src/composables/useInteractionLogger.js
@@ -0,0 +1,50 @@
+
+import { ref, reactive } from 'vue'
+
+// Global state for logs so it persists across component re-mounts
+const logs = ref([])
+const isRecording = ref(true)
+const maxLogs = 500 // Limit history size
+
+export function useInteractionLogger() {
+
+ const addLog = (type, data) => {
+ if (!isRecording.value) return
+
+ const timestamp = Date.now()
+ const logEntry = {
+ id: timestamp + Math.random().toString(36).substr(2, 9),
+ timestamp,
+ type,
+ data: JSON.parse(JSON.stringify(data)) // Deep copy to snapshot state
+ }
+
+ logs.value.push(logEntry)
+ if (logs.value.length > maxLogs) {
+ logs.value.shift()
+ }
+ }
+
+ const clearLogs = () => {
+ logs.value = []
+ }
+
+ const exportLogs = () => {
+ return JSON.stringify(logs.value, null, 2)
+ }
+
+ // Helper to format logs for LLM analysis
+ const getRecentLogsForAnalysis = (count = 50) => {
+ const recent = logs.value.slice(-count)
+ return JSON.stringify(recent, null, 2)
+ }
+
+ return {
+ logs,
+ isRecording,
+ addLog,
+ clearLogs,
+ exportLogs,
+ getRecentLogsForAnalysis
+ }
+}
diff --git a/src/utils/Cube.js b/src/utils/Cube.js
index e5464bf..7ac16a7 100644
--- a/src/utils/Cube.js
+++ b/src/utils/Cube.js
@@ -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;
+ }
}
diff --git a/test/cube_logic.test.js b/test/cube_logic.test.js
new file mode 100644
index 0000000..97134a9
--- /dev/null
+++ b/test/cube_logic.test.js
@@ -0,0 +1,96 @@
+
+import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
+import assert from 'assert';
+
+console.log('Running Cube Logic Tests...');
+
+const cube = new Cube();
+
+// Helper to check a specific face color at a position
+const checkFace = (x, y, z, face, expectedColor, message) => {
+ const cubie = cube.cubies.find(c => c.x === x && c.y === y && c.z === z);
+ if (!cubie) {
+ console.error(`Cubie not found at ${x}, ${y}, ${z}`);
+ return false;
+ }
+ const color = cubie.faces[face];
+ if (color !== expectedColor) {
+ console.error(`FAIL: ${message}. Expected ${expectedColor} at ${face} of (${x},${y},${z}), got ${color}`);
+ return false;
+ }
+ return true;
+};
+
+// Test 1: Initial State
+console.log('Test 1: Initial State');
+// Top-Front-Right corner (1, 1, 1) should have Up=White, Front=Green, Right=Red
+checkFace(1, 1, 1, FACES.UP, COLORS.WHITE, 'Initial Top-Right-Front UP');
+checkFace(1, 1, 1, FACES.FRONT, COLORS.GREEN, 'Initial Top-Right-Front FRONT');
+checkFace(1, 1, 1, FACES.RIGHT, COLORS.RED, 'Initial Top-Right-Front RIGHT');
+
+// Test 2: Rotate Right Face (R) -> Axis X, index 1, direction -1 (based on previous mapping)
+// Wait, let's test `rotateLayer` directly first with axis 'x'.
+// Axis X Positive Rotation (direction 1).
+// Up (y=1) -> Front (z=1).
+// The cubie at (1, 1, 1) (Top-Front-Right)
+// Should move to (1, 0, 1)? No.
+// (x, y, z) -> (x, -z, y).
+// (1, 1, 1) -> (1, -1, 1). (Bottom-Front-Right).
+// Let's trace the color.
+// The White color was on UP.
+// The cubie moves to Bottom-Front.
+// The UP face of the cubie now points FRONT.
+// So the cubie at (1, -1, 1) should have FRONT = WHITE.
+
+console.log('Test 2: Rotate X Axis +90 (Right Layer)');
+cube.rotateLayer('x', 1, 1);
+
+// Cubie originally at (1, 1, 1) [White Up] moves to (1, -1, 1).
+// Check (1, -1, 1).
+// Its Front face should be White.
+const result1 = checkFace(1, -1, 1, FACES.FRONT, COLORS.WHITE, 'After X+90: Old Up(White) should be on Front');
+
+// Cubie originally at (1, 1, -1) [Blue Back, White Up] (Top-Back-Right)
+// (1, 1, -1) -> (1, 1, 1). (Top-Front-Right).
+// Wait. ny = -z = -(-1) = 1. nz = y = 1.
+// So Top-Back moves to Top-Front.
+// Its UP face (White) moves to FRONT?
+// No. The rotation is around X.
+// Top-Back (y=1, z=-1).
+// Rot +90 X: y->z, z->-y ? No.
+// ny = -z = 1. nz = y = 1.
+// New pos: (1, 1, 1).
+// The cubie moves from Top-Back to Top-Front.
+// Its Up face (White) stays Up?
+// No, the cubie rotates.
+// Up face rotates to Front?
+// Rotation around X axis.
+// Top (Y+) rotates to Front (Z+)?
+// Yes.
+// So the cubie at (1, 1, 1) (new position) should have FRONT = WHITE.
+const result2 = checkFace(1, 1, 1, FACES.FRONT, COLORS.WHITE, 'After X+90: Old Top-Back Up(White) should be on Front');
+
+if (result1 && result2) {
+ console.log('PASS: X Axis Rotation Logic seems correct (if fixed)');
+} else {
+ console.log('FAIL: X Axis Rotation Logic is broken');
+}
+
+// Reset for Y test
+cube.reset();
+console.log('Test 3: Rotate Y Axis +90 (Top Layer)');
+// Top Layer (y=1).
+// Rotate Y+ (direction 1).
+// Front (z=1) -> Right (x=1).
+// Cubie at (0, 1, 1) (Front-Top-Center) [Green Front, White Up].
+// Moves to (1, 1, 0) (Right-Top-Center).
+// Its Front Face (Green) should move to Right Face.
+cube.rotateLayer('y', 1, 1);
+const resultY = checkFace(1, 1, 0, FACES.RIGHT, COLORS.GREEN, 'After Y+90: Old Front(Green) should be on Right');
+
+if (resultY) {
+ console.log('PASS: Y Axis Rotation Logic seems correct');
+} else {
+ console.log('FAIL: Y Axis Rotation Logic is broken');
+}
+
diff --git a/test/cube_matrix.test.js b/test/cube_matrix.test.js
new file mode 100644
index 0000000..2ed1c36
--- /dev/null
+++ b/test/cube_matrix.test.js
@@ -0,0 +1,109 @@
+
+import { Cube, FACES, COLORS } from '../src/utils/Cube.js';
+import assert from 'assert';
+
+console.log('Running Cube Matrix Rotation Tests...');
+
+const cube = new Cube();
+
+// Helper to check position and face
+const checkCubie = (origX, origY, origZ, newX, newY, newZ, faceCheck) => {
+ const cubie = cube.cubies.find(c => c.x === newX && c.y === newY && c.z === newZ);
+ if (!cubie) {
+ console.error(`FAIL: Cubie not found at ${newX}, ${newY}, ${newZ}`);
+ return false;
+ }
+
+ // Verify it's the correct original cubie (tracking ID would be better, but position logic is enough if unique)
+ // Let's assume we track a specific cubie.
+ return true;
+};
+
+// Test 1: Z-Axis Rotation (Front Face)
+// Front Face is z=1.
+// Top-Left (x=-1, y=1) -> Top-Right (x=1, y=1)?
+// Physical CW (Z-Axis): Up -> Right.
+// Top-Middle (0, 1) -> Right-Middle (1, 0).
+console.log('Test 1: Z-Axis CW (Front)');
+cube.reset();
+// Find Top-Middle of Front Face: (0, 1, 1). White Up, Green Front.
+const topMid = cube.cubies.find(c => c.x === 0 && c.y === 1 && c.z === 1);
+assert.strictEqual(topMid.faces[FACES.UP], COLORS.WHITE);
+assert.strictEqual(topMid.faces[FACES.FRONT], COLORS.GREEN);
+
+cube.rotateLayer('z', 1, -1); // CW (direction -1 in move(), but rotateLayer takes direction. Standard move F is direction -1?)
+// move('F') calls rotateLayer('z', 1, -1).
+// So let's test rotateLayer('z', 1, -1).
+
+// Expect: (0, 1, 1) -> (1, 0, 1). (Right-Middle of Front).
+// Faces: Old Up (White) becomes Right?
+// Z-Axis CW: Up -> Right.
+// So new pos should have Right=White.
+// Old Front (Green) stays Front.
+const newPos = cube.cubies.find(c => c.id === topMid.id);
+console.log(`Moved to: (${newPos.x}, ${newPos.y}, ${newPos.z})`);
+assert.strictEqual(newPos.x, 1);
+assert.strictEqual(newPos.y, 0);
+assert.strictEqual(newPos.z, 1);
+assert.strictEqual(newPos.faces[FACES.RIGHT], COLORS.WHITE);
+assert.strictEqual(newPos.faces[FACES.FRONT], COLORS.GREEN);
+console.log('PASS Z-Axis CW');
+
+
+// Test 2: X-Axis Rotation (Right Face)
+// Right Face is x=1.
+// Top-Front (1, 1, 1) -> Top-Back (1, 1, -1)?
+// Physical CW (X-Axis): Up -> Front.
+// Top-Middle (1, 1, 0) -> Front-Middle (1, 0, 1).
+console.log('Test 2: X-Axis CW (Right)');
+cube.reset();
+// Find Top-Middle of Right Face: (1, 1, 0). White Up, Red Right.
+const rightTop = cube.cubies.find(c => c.x === 1 && c.y === 1 && c.z === 0);
+
+cube.rotateLayer('x', 1, -1); // CW (direction -1 for R in move()?)
+// move('R') calls rotateLayer('x', 1, -1).
+// So let's test -1.
+
+// Expect: (1, 1, 0) -> (1, 0, -1).
+// Faces: Old Up (White) becomes Back?
+// X-Axis CW (Right Face): Up -> Back.
+// So new pos should have Back=White.
+// Old Right (Red) stays Right.
+const newRightPos = cube.cubies.find(c => c.id === rightTop.id);
+console.log(`Moved to: (${newRightPos.x}, ${newRightPos.y}, ${newRightPos.z})`);
+assert.strictEqual(newRightPos.x, 1);
+assert.strictEqual(newRightPos.y, 0);
+assert.strictEqual(newRightPos.z, -1);
+assert.strictEqual(newRightPos.faces[FACES.BACK], COLORS.WHITE);
+assert.strictEqual(newRightPos.faces[FACES.RIGHT], COLORS.RED);
+console.log('PASS X-Axis CW');
+
+
+// Test 3: Y-Axis Rotation (Up Face)
+// Up Face is y=1.
+// Front-Middle (0, 1, 1) -> Left-Middle (-1, 1, 0).
+// Physical CW (Y-Axis): Front -> Left.
+// Wait. move('U') calls rotateLayer('y', 1, -1).
+// Standard U is CW. Y-Axis direction?
+// move('U'): dir = -1.
+console.log('Test 3: Y-Axis CW (Up)');
+cube.reset();
+// Find Front-Middle of Up Face: (0, 1, 1). Green Front, White Up.
+const upFront = cube.cubies.find(c => c.x === 0 && c.y === 1 && c.z === 1);
+
+cube.rotateLayer('y', 1, -1); // CW (direction -1).
+
+// Expect: (0, 1, 1) -> (-1, 1, 0). (Left-Middle).
+// Faces: Old Front (Green) becomes Left?
+// Y-Axis CW (U): Front -> Left.
+// So new pos should have Left=Green.
+// Old Up (White) stays Up.
+const newUpPos = cube.cubies.find(c => c.id === upFront.id);
+console.log(`Moved to: (${newUpPos.x}, ${newUpPos.y}, ${newUpPos.z})`);
+assert.strictEqual(newUpPos.x, -1);
+assert.strictEqual(newUpPos.y, 1);
+assert.strictEqual(newUpPos.z, 0);
+assert.strictEqual(newUpPos.faces[FACES.LEFT], COLORS.GREEN);
+assert.strictEqual(newUpPos.faces[FACES.UP], COLORS.WHITE);
+console.log('PASS Y-Axis CW');
+