From c773ff8876fa0cbe36ee3554745f01a3e0d35df6 Mon Sep 17 00:00:00 2001 From: Grzegorz Kucmierz Date: Mon, 16 Feb 2026 03:19:02 +0100 Subject: [PATCH] fix: invert rotation logic and add debug tools --- src/App.vue | 20 +- src/components/DebugPanel.vue | 137 +++++ src/components/InteractionReplay.vue | 188 +++++++ src/components/Main.vue | 75 ++- src/components/renderers/CubeCSS.vue | 557 +++++++------------ src/composables/useCube.js | 71 +-- src/composables/useDebug.js | 50 ++ src/composables/useInteractionLogger.js | 50 ++ src/utils/Cube.js | 696 ++++++++++++++++-------- test/cube_logic.test.js | 96 ++++ test/cube_matrix.test.js | 109 ++++ 11 files changed, 1407 insertions(+), 642 deletions(-) create mode 100644 src/components/DebugPanel.vue create mode 100644 src/components/InteractionReplay.vue create mode 100644 src/composables/useDebug.js create mode 100644 src/composables/useInteractionLogger.js create mode 100644 test/cube_logic.test.js create mode 100644 test/cube_matrix.test.js diff --git a/src/App.vue b/src/App.vue index ed7a4e3..662abf3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,17 +1,15 @@ diff --git a/src/components/DebugPanel.vue b/src/components/DebugPanel.vue new file mode 100644 index 0000000..ee9f40d --- /dev/null +++ b/src/components/DebugPanel.vue @@ -0,0 +1,137 @@ + + + + + + diff --git a/src/components/InteractionReplay.vue b/src/components/InteractionReplay.vue new file mode 100644 index 0000000..00611f4 --- /dev/null +++ b/src/components/InteractionReplay.vue @@ -0,0 +1,188 @@ + + + + + + diff --git a/src/components/Main.vue b/src/components/Main.vue index efbbcb2..5536848 100644 --- a/src/components/Main.vue +++ b/src/components/Main.vue @@ -1,11 +1,13 @@ diff --git a/src/components/renderers/CubeCSS.vue b/src/components/renderers/CubeCSS.vue index d410879..a05ce63 100644 --- a/src/components/renderers/CubeCSS.vue +++ b/src/components/renderers/CubeCSS.vue @@ -1,8 +1,12 @@ @@ -512,12 +367,9 @@ const getCubieStyle = (cubie) => {
- +
- - - -
{ :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'); +