Refactor: Move Cube logic to src/utils/Cube.js with 3x3 integer matrix model
All checks were successful
Deploy to Production / deploy (push) Successful in 6s
All checks were successful
Deploy to Production / deploy (push) Successful in 6s
This commit is contained in:
@@ -1,18 +1,60 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useCube } from '../composables/useCube'
|
||||
|
||||
const rx = ref(25)
|
||||
const { cubeState, initCube, rotateLayer, COLOR_MAP } = useCube()
|
||||
|
||||
const getFaceColors = (faceName) => {
|
||||
if (!cubeState.value || !cubeState.value[faceName]) return Array(9).fill('gray');
|
||||
// cubeState.value[faceName] is a 3x3 matrix.
|
||||
// We need to flatten it.
|
||||
return cubeState.value[faceName].flat().map(c => COLOR_MAP[c]);
|
||||
};
|
||||
|
||||
// ...ref(25)
|
||||
const ry = ref(25)
|
||||
const rz = ref(0)
|
||||
|
||||
const isDragging = ref(false)
|
||||
const dragMode = ref('view') // 'view' or 'layer'
|
||||
const startMouseX = ref(0)
|
||||
const startMouseY = ref(0)
|
||||
const lastMouseX = ref(0)
|
||||
const lastMouseY = ref(0)
|
||||
const selectedFace = ref(null)
|
||||
const selectedStickerIndex = ref(null)
|
||||
|
||||
const onMouseDown = (event) => {
|
||||
isDragging.value = true
|
||||
startMouseX.value = event.clientX
|
||||
startMouseY.value = event.clientY
|
||||
lastMouseX.value = event.clientX
|
||||
lastMouseY.value = event.clientY
|
||||
|
||||
// Check target
|
||||
const target = event.target
|
||||
// Find closest face
|
||||
const faceEl = target.closest('.face')
|
||||
const stickerEl = target.closest('.sticker')
|
||||
|
||||
if (faceEl && stickerEl) {
|
||||
const face = Array.from(faceEl.classList).find(c => ['top','bottom','left','right','front','back'].includes(c))
|
||||
const index = parseInt(stickerEl.dataset.index)
|
||||
|
||||
selectedFace.value = face
|
||||
selectedStickerIndex.value = index
|
||||
|
||||
// Center piece logic (index 4 is center)
|
||||
if (index === 4) {
|
||||
dragMode.value = 'view'
|
||||
} else {
|
||||
dragMode.value = 'layer'
|
||||
}
|
||||
} else {
|
||||
dragMode.value = 'view'
|
||||
selectedFace.value = null
|
||||
selectedStickerIndex.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseMove = (event) => {
|
||||
@@ -21,18 +63,95 @@ const onMouseMove = (event) => {
|
||||
const deltaX = event.clientX - lastMouseX.value
|
||||
const deltaY = event.clientY - lastMouseY.value
|
||||
|
||||
if (dragMode.value === 'view') {
|
||||
ry.value += deltaX * 0.5
|
||||
rx.value -= deltaY * 0.5
|
||||
} else if (dragMode.value === 'layer' && selectedFace.value) {
|
||||
const threshold = 10
|
||||
const absX = Math.abs(event.clientX - startMouseX.value)
|
||||
const absY = Math.abs(event.clientY - startMouseY.value)
|
||||
|
||||
if (absX > threshold || absY > threshold) {
|
||||
handleLayerDrag(event.clientX - startMouseX.value, event.clientY - startMouseY.value)
|
||||
isDragging.value = false
|
||||
}
|
||||
}
|
||||
|
||||
lastMouseX.value = event.clientX
|
||||
lastMouseY.value = event.clientY
|
||||
}
|
||||
|
||||
const handleLayerDrag = (dx, dy) => {
|
||||
if (!selectedFace.value || selectedStickerIndex.value === null) return
|
||||
|
||||
const face = selectedFace.value
|
||||
const idx = selectedStickerIndex.value
|
||||
|
||||
// Determine row/col from index
|
||||
const row = Math.floor(idx / 3)
|
||||
const col = idx % 3
|
||||
|
||||
// Determine drag axis (Horizontal or Vertical)
|
||||
const isHorizontal = Math.abs(dx) > Math.abs(dy)
|
||||
const direction = isHorizontal ? (dx > 0 ? 1 : -1) : (dy > 0 ? 1 : -1)
|
||||
|
||||
// Direction logic:
|
||||
// For standard faces (Front, Right, Top, etc.)
|
||||
// Horizontal drag moves along Row -> Rotates the layer corresponding to that Row.
|
||||
// Vertical drag moves along Col -> Rotates the layer corresponding to that Col.
|
||||
|
||||
let targetLayer = null
|
||||
let rotDir = 1
|
||||
|
||||
// Map rows/cols to layers based on Face
|
||||
// Note: Directions need careful mapping (CW/CCW).
|
||||
// Assuming standard view for Front.
|
||||
|
||||
if (isHorizontal) {
|
||||
// Dragging along a Row
|
||||
if (row === 0) targetLayer = 'top'
|
||||
if (row === 2) targetLayer = 'bottom'
|
||||
// Middle row (1) - ignore or equator
|
||||
|
||||
// Direction mapping
|
||||
// Drag Right (dx > 0) on Top Row of Front Face -> Top Layer rotates Left (CCW) or Right (CW)?
|
||||
// Dragging Top layer Right -> moves face stickers Right. This is Y axis CW (viewed from top).
|
||||
// Let's assume dx > 0 -> CW (1), dx < 0 -> CCW (-1).
|
||||
// Adjust for specific faces (Back might be inverted).
|
||||
if (face === 'back' || face === 'bottom') rotDir = -direction // Test this
|
||||
else rotDir = direction
|
||||
|
||||
} else {
|
||||
// Dragging along a Col
|
||||
if (col === 0) targetLayer = 'left'
|
||||
if (col === 2) targetLayer = 'right'
|
||||
|
||||
// Direction mapping
|
||||
// Drag Down (dy > 0) on Right Col of Front Face -> Right Layer rotates Down.
|
||||
// Right layer Down is X axis CW? No, X axis is Right. R layer moves "towards you" or "away".
|
||||
// Wait, Right Face rotation (R) is CW.
|
||||
// R (CW) moves top stickers to back. (Up).
|
||||
// So R moves stickers Up.
|
||||
// If I drag Down, I want R' (CCW).
|
||||
// So dy > 0 -> -1.
|
||||
rotDir = -direction
|
||||
|
||||
if (face === 'left' || face === 'back') rotDir = -rotDir // Invert again?
|
||||
}
|
||||
|
||||
if (targetLayer) {
|
||||
rotateLayer(targetLayer, rotDir)
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
isDragging.value = false
|
||||
selectedFace.value = null
|
||||
selectedStickerIndex.value = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCube()
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mouseup', onMouseUp)
|
||||
})
|
||||
@@ -53,32 +172,32 @@ const cubeStyle = computed(() => ({
|
||||
<div class="cube" :style="cubeStyle">
|
||||
<div class="face top">
|
||||
<div class="stickers">
|
||||
<div class="sticker" v-for="i in 9" :key="'t'+i"></div>
|
||||
<div class="sticker" v-for="(color, i) in getFaceColors('up')" :key="'t'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="face bottom">
|
||||
<div class="stickers">
|
||||
<div class="sticker" v-for="i in 9" :key="'b'+i"></div>
|
||||
<div class="sticker" v-for="(color, i) in getFaceColors('down')" :key="'b'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="face left">
|
||||
<div class="stickers">
|
||||
<div class="sticker" v-for="i in 9" :key="'l'+i"></div>
|
||||
<div class="sticker" v-for="(color, i) in getFaceColors('left')" :key="'l'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="face right">
|
||||
<div class="stickers">
|
||||
<div class="sticker" v-for="i in 9" :key="'r'+i"></div>
|
||||
<div class="sticker" v-for="(color, i) in getFaceColors('right')" :key="'r'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="face front">
|
||||
<div class="stickers">
|
||||
<div class="sticker" v-for="i in 9" :key="'f'+i"></div>
|
||||
<div class="sticker" v-for="(color, i) in getFaceColors('front')" :key="'f'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="face back">
|
||||
<div class="stickers">
|
||||
<div class="sticker" v-for="i in 9" :key="'k'+i"></div>
|
||||
<div class="sticker" v-for="(color, i) in getFaceColors('back')" :key="'k'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,84 +211,59 @@ const cubeStyle = computed(() => ({
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
perspective: 900px;
|
||||
margin: 90px auto;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container:active {
|
||||
cursor: grabbing;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.cube {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.face {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: var(--cube-edge-color, #000);
|
||||
padding: 9px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
transition: background-color 0.3s ease;
|
||||
border: 2px solid #000;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
opacity: 0.95;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.face.front { transform: rotateY(0deg) translateZ(150px); }
|
||||
.face.right { transform: rotateY(90deg) translateZ(150px); }
|
||||
.face.back { transform: rotateY(180deg) translateZ(150px); }
|
||||
.face.left { transform: rotateY(-90deg) translateZ(150px); }
|
||||
.face.top { transform: rotateX(90deg) translateZ(150px); }
|
||||
.face.bottom { transform: rotateX(-90deg) translateZ(150px); }
|
||||
|
||||
.stickers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 9px;
|
||||
gap: 4px; /* Gap between stickers */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sticker {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--sticker-color);
|
||||
border-radius: 9px;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.top { --sticker-color: #ffffff; }
|
||||
.bottom { --sticker-color: #ffd500; }
|
||||
.front { --sticker-color: #0051ba; }
|
||||
.back { --sticker-color: #009e60; }
|
||||
.left { --sticker-color: #c41e3a; }
|
||||
.right { --sticker-color: #ff5800; }
|
||||
|
||||
.front {
|
||||
transform: translateZ(150px);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: translateZ(-150px) rotateY(180deg);
|
||||
}
|
||||
|
||||
.left {
|
||||
transform: translateX(-150px) rotateY(-90deg);
|
||||
}
|
||||
|
||||
.right {
|
||||
transform: translateX(150px) rotateY(90deg);
|
||||
}
|
||||
|
||||
.top {
|
||||
transform: translateY(-150px) rotateX(90deg);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
transform: translateY(150px) rotateX(-90deg);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
70
src/composables/useCube.js
Normal file
70
src/composables/useCube.js
Normal file
@@ -0,0 +1,70 @@
|
||||
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.
|
||||
|
||||
const cubeState = computed(() => cube.value.state);
|
||||
|
||||
const initCube = () => {
|
||||
cube.value.reset();
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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 {
|
||||
cubeState,
|
||||
initCube,
|
||||
rotateLayer,
|
||||
COLOR_MAP,
|
||||
FACES
|
||||
};
|
||||
}
|
||||
709
src/utils/Cube.js
Normal file
709
src/utils/Cube.js
Normal file
@@ -0,0 +1,709 @@
|
||||
// Enum for colors/faces
|
||||
export const COLORS = {
|
||||
WHITE: 0,
|
||||
YELLOW: 1,
|
||||
ORANGE: 2,
|
||||
RED: 3,
|
||||
GREEN: 4,
|
||||
BLUE: 5,
|
||||
};
|
||||
|
||||
// Faces mapping
|
||||
export const FACES = {
|
||||
UP: 'up',
|
||||
DOWN: 'down',
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right',
|
||||
FRONT: 'front',
|
||||
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)),
|
||||
};
|
||||
|
||||
export class Cube {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
// Deep copy initial state
|
||||
this.state = JSON.parse(JSON.stringify(INITIAL_STATE));
|
||||
}
|
||||
|
||||
// Rotate a 3x3 matrix 90 degrees clockwise
|
||||
_rotateMatrixCW(matrix) {
|
||||
const N = matrix.length;
|
||||
const result = Array(N).fill().map(() => Array(N).fill(null));
|
||||
for (let i = 0; i < N; i++) {
|
||||
for (let j = 0; j < N; j++) {
|
||||
result[j][N - 1 - i] = matrix[i][j];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Rotate a 3x3 matrix 90 degrees counter-clockwise
|
||||
_rotateMatrixCCW(matrix) {
|
||||
const N = matrix.length;
|
||||
const result = Array(N).fill().map(() => Array(N).fill(null));
|
||||
for (let i = 0; i < N; i++) {
|
||||
for (let j = 0; j < N; j++) {
|
||||
result[N - 1 - j][i] = matrix[i][j];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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
|
||||
// We need to define the cycle of adjacent faces and their specific rows/cols.
|
||||
// Order is critical. For CW rotation:
|
||||
// Front: Up(row2) -> Right(col0) -> Down(row0) -> Left(col2) -> Up...
|
||||
// Note: Orientation matters.
|
||||
// Up: row2 (bottom row) usually corresponds to Front top row.
|
||||
// Right: col0 (left col) corresponds to Front right col.
|
||||
// Down: row0 (top row) corresponds to Front bottom row.
|
||||
// Left: col2 (right col) corresponds to Front left col.
|
||||
|
||||
// Let's implement generic cycle logic.
|
||||
// A cycle is an array of 4 segments: { face, type: 'row'|'col', index, reverse: bool }
|
||||
|
||||
let cycle = [];
|
||||
|
||||
switch (face) {
|
||||
case FACES.FRONT:
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'row', index: 2 }, // Bottom row of Up
|
||||
{ face: FACES.RIGHT, type: 'col', index: 0 }, // Left col of Right
|
||||
{ face: FACES.DOWN, type: 'row', index: 0 }, // Top row of Down (reversed relative to Up in standard net? No, standard is consistent)
|
||||
// Wait. Standard net: Up row 2 is adjacent to Front row 0.
|
||||
// Right col 0 is adjacent to Front col 2.
|
||||
// Down row 0 is adjacent to Front row 2.
|
||||
// Left col 2 is adjacent to Front col 0.
|
||||
|
||||
// Let's trace a sticker moving CW on Front face (e.g. top-right corner).
|
||||
// It moves to bottom-right.
|
||||
// An edge piece at Top (Up[2][1]) moves to Right (Right[1][0]).
|
||||
// Right[1][0] moves to Bottom (Down[0][1]).
|
||||
// Down[0][1] moves to Left (Left[1][2]).
|
||||
// Left[1][2] moves to Top (Up[2][1]).
|
||||
|
||||
// So the cycle is Up -> Right -> Down -> Left.
|
||||
// Up[2] (row) -> Right[col 0] -> Down[0] (row) -> Left[col 2].
|
||||
// Direction:
|
||||
// Up[2] (left-to-right) maps to Right[col 0] (top-to-bottom)?
|
||||
// Up[2][0] (L) -> Right[0][0] (T).
|
||||
// Up[2][2] (R) -> Right[2][0] (B).
|
||||
// So Up row matches Right col.
|
||||
// Right col (top-to-bottom) matches Down row (right-to-left? or left-to-right?)
|
||||
// Right[2][0] (B) -> Down[0][2] (R).
|
||||
// Right[0][0] (T) -> Down[0][0] (L).
|
||||
// So Right col matches Down row REVERSED.
|
||||
|
||||
// Let's verify standard notation.
|
||||
// F moves U -> R -> D -> L.
|
||||
// U[2][0,1,2] -> R[0,1,2][0]
|
||||
// R[0,1,2][0] -> D[0][2,1,0] (Reversed)
|
||||
// D[0][2,1,0] -> L[2,1,0][2] (Reversed col)
|
||||
// L[2,1,0][2] -> U[2][0,1,2]
|
||||
|
||||
// To simplify, let's just extract values, shift them, and put them back.
|
||||
|
||||
{ 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)
|
||||
]; // Wait, Left col 2 is Right side of Left face.
|
||||
// Standard Left face: col 2 is adjacent to Front col 0.
|
||||
// So yes.
|
||||
break;
|
||||
|
||||
case FACES.BACK:
|
||||
// Inverse of Front logic essentially, but on the other side.
|
||||
// Back is adjacent to Up, Left, Down, Right.
|
||||
// Cycle: Up -> Left -> Down -> Right.
|
||||
// Up[0] (top row) -> Left[col 0] (left col)
|
||||
// Left[col 0] -> Down[2] (bottom row)
|
||||
// Down[2] -> Right[col 2] (right col)
|
||||
// Right[col 2] -> Up[0]
|
||||
|
||||
// Orientation:
|
||||
// Up[0][2] (R) -> Left[0][0] (T).
|
||||
// Up[0][0] (L) -> Left[2][0] (B).
|
||||
// So Up row (reversed) -> Left col (normal).
|
||||
// Or Up row (normal) -> Left col (reversed).
|
||||
// Let's look at net.
|
||||
// Back is "behind".
|
||||
// Up[0] is adjacent to Back[0].
|
||||
// If we rotate Back CW (looking from back):
|
||||
// Up[0] moves to Left[col 0].
|
||||
// Up[0][0] (Top-Left of Up) is Back-Left corner.
|
||||
// It moves to Left[2][0] (Bottom-Left of Left, which is Back-Bottom corner).
|
||||
// So Up[0][0] -> Left[2][0].
|
||||
// Up[0][2] -> Left[0][0].
|
||||
// So Up row (normal) -> Left col (reversed).
|
||||
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'row', index: 0, reverse: false }, // we handle reverse logic in extraction
|
||||
{ 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 }
|
||||
];
|
||||
// Let's re-verify orientations.
|
||||
// B CW:
|
||||
// U[0][0..2] -> L[2..0][0] (Left col, bottom-to-top)
|
||||
// L[2..0][0] -> D[2][0..2] (Bottom row, left-to-right)
|
||||
// D[2][0..2] -> R[0..2][2] (Right col, top-to-bottom)
|
||||
// R[0..2][2] -> U[0][0..2] (Top row, left-to-right? No, R[0][2] is Top-Right of Right, which is Back-Top corner. U[0][2] is Top-Right of Up, which is Back-Right corner.)
|
||||
// Matches.
|
||||
|
||||
// So:
|
||||
// U[0] -> L[col 0] (Reverse)
|
||||
// L[col 0] (Reverse) -> D[2] (Normal)
|
||||
// D[2] (Normal) -> R[col 2] (Normal)
|
||||
// R[col 2] (Normal) -> U[0] (Normal?? No, R[0][2] -> U[0][0]??)
|
||||
// R[0][2] (Top-Right of Right) -> U[0][0] (Top-Left of Up).
|
||||
// Yes.
|
||||
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'row', index: 0, reverse: true }, // Logic to be applied
|
||||
{ face: FACES.LEFT, type: 'col', index: 0, reverse: false }, // Wait, if I reverse input, output is reversed?
|
||||
{ face: FACES.DOWN, type: 'row', index: 2, reverse: false },
|
||||
{ face: FACES.RIGHT, type: 'col', index: 2, reverse: false }
|
||||
];
|
||||
// Wait, cycle definitions are tricky.
|
||||
// Let's implement a robust `getSegment` and `setSegment` that handles reversal.
|
||||
// And we just define: Segment A moves to Segment B.
|
||||
// If Segment A is [1,2,3], Segment B becomes [1,2,3].
|
||||
// If the geometric mapping requires [3,2,1], we mark it.
|
||||
|
||||
// Back CW:
|
||||
// U[0] (0,1,2) -> L[col 0] (2,1,0) (Top-to-bottom? No. U[0][0] is Top-Left. L[col 0][0] is Top-Left.
|
||||
// U[0][0] is Back-Left corner.
|
||||
// L[col 0][0] is Back-Top corner.
|
||||
// L[col 0][2] is Back-Bottom corner.
|
||||
// Back CW: Top-Left corner moves to Top-Right? No.
|
||||
// Back CW (looking from back) is CCW looking from front.
|
||||
// Top-Left (U[0][2] in Front view?) No.
|
||||
// U[0][0] is "Back Left".
|
||||
// Rotating Back CW: Back-Left moves to Back-Top? No, Back-Left moves to Back-Right (if 180).
|
||||
// Back-Left (U[0][0]) moves to Back-Bottom (L[col 0][2]).
|
||||
// So U[0][0] -> L[2][0].
|
||||
// U[0][1] -> L[1][0].
|
||||
// U[0][2] -> L[0][0].
|
||||
// So U[0] -> L[col 0] (Reverse).
|
||||
|
||||
// L[2][0] -> D[2][2].
|
||||
// L[1][0] -> D[2][1].
|
||||
// L[0][0] -> D[2][0].
|
||||
// So L[col 0] (Reverse) -> D[2] (Reverse).
|
||||
// Or simply L[col 0] (Normal 0->2) maps to D[2] (Normal 0->2)?
|
||||
// L[0][0] -> D[2][0]. L[2][0] -> D[2][2].
|
||||
// Yes, Normal maps to Normal.
|
||||
|
||||
// D[2][0] -> R[0][2].
|
||||
// D[2][2] -> R[2][2].
|
||||
// So D[2] -> R[col 2] (Normal).
|
||||
|
||||
// R[0][2] -> U[0][0].
|
||||
// R[2][2] -> U[0][2].
|
||||
// So R[col 2] -> U[0] (Normal).
|
||||
|
||||
// So cycle:
|
||||
// U[0] (Reverse) -> L[col 0].
|
||||
// L[col 0] -> D[2].
|
||||
// D[2] -> R[col 2].
|
||||
// R[col 2] -> U[0] (Reverse).
|
||||
|
||||
// Let's refine.
|
||||
// "A moves to B" means B takes A's values.
|
||||
// So L[col 0] takes U[0] (Reversed).
|
||||
// D[2] takes L[col 0].
|
||||
// R[col 2] takes D[2].
|
||||
// U[0] takes R[col 2] (Reversed? No).
|
||||
|
||||
// Let's restart Back Cycle.
|
||||
// U[0] values: [a, b, c].
|
||||
// L[col 0] becomes [c, b, a].
|
||||
// D[2] becomes [c, b, a] (from L's new values? No, from L's old values).
|
||||
// Let's trace values.
|
||||
// U_old -> L_new (Reversed).
|
||||
// L_old -> D_new.
|
||||
// D_old -> R_new.
|
||||
// R_old -> U_new (Reversed).
|
||||
|
||||
// So cycle array:
|
||||
// { face: UP, ... }, { face: LEFT, ... }, { face: DOWN, ... }, { face: RIGHT, ... }
|
||||
// U -> L (Rev), L -> D, D -> R, R -> U (Rev).
|
||||
// Wait, if R -> U is Rev, then U_new = R_old (Rev).
|
||||
// R_old[0] -> U_new[2]. R_old[2] -> U_new[0].
|
||||
// R[0][2] (Back-Top-Right) -> U[0][0] (Back-Top-Left). Yes.
|
||||
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'row', index: 0 },
|
||||
{ face: FACES.LEFT, type: 'col', index: 0 },
|
||||
{ face: FACES.DOWN, type: 'row', index: 2 },
|
||||
{ face: FACES.RIGHT, type: 'col', index: 2 }
|
||||
];
|
||||
// Modifiers for mapping:
|
||||
// U -> L: Reverse.
|
||||
// L -> D: Normal.
|
||||
// D -> R: Normal.
|
||||
// R -> U: Reverse.
|
||||
break;
|
||||
|
||||
case FACES.UP:
|
||||
// Up CW:
|
||||
// Back[0] -> Right[0] -> Front[0] -> Left[0] -> Back[0].
|
||||
// B[0] (L-to-R) -> R[0] (L-to-R).
|
||||
// B[0][0] (Back-Top-Right looking from front? No. Back[0][0] is Top-Right of Back Face (looking from back).
|
||||
// In standard net, Back[0][0] is adjacent to Left[0][0]?
|
||||
// Standard net:
|
||||
// U
|
||||
// L F R B
|
||||
// D
|
||||
// U is adjacent to L, F, R, B.
|
||||
// U Top edge (row 0) -> B Top edge (row 0).
|
||||
// U Bottom edge (row 2) -> F Top edge (row 0).
|
||||
// U Left edge (col 0) -> L Top edge (row 0).
|
||||
// U Right edge (col 2) -> R Top edge (row 0).
|
||||
|
||||
// Rotation U CW:
|
||||
// F[0] -> L[0].
|
||||
// L[0] -> B[0].
|
||||
// B[0] -> R[0].
|
||||
// R[0] -> F[0].
|
||||
|
||||
// Orientation:
|
||||
// F[0] (L-to-R) -> L[0] (L-to-R).
|
||||
// F[0][0] (Front-Top-Left) -> L[0][0] (Left-Top-Left). Yes.
|
||||
// So all are normal.
|
||||
|
||||
cycle = [
|
||||
{ face: FACES.FRONT, type: 'row', index: 0 },
|
||||
{ face: FACES.LEFT, type: 'row', index: 0 },
|
||||
{ face: FACES.BACK, type: 'row', index: 0 },
|
||||
{ face: FACES.RIGHT, type: 'row', index: 0 }
|
||||
];
|
||||
// All Normal.
|
||||
break;
|
||||
|
||||
case FACES.DOWN:
|
||||
// Down CW:
|
||||
// F[2] -> R[2] -> B[2] -> L[2] -> F[2].
|
||||
// F[2][0] (Front-Bottom-Left) -> R[2][0] (Right-Bottom-Left). Yes.
|
||||
|
||||
cycle = [
|
||||
{ face: FACES.FRONT, type: 'row', index: 2 },
|
||||
{ face: FACES.RIGHT, type: 'row', index: 2 },
|
||||
{ face: FACES.BACK, type: 'row', index: 2 },
|
||||
{ face: FACES.LEFT, type: 'row', index: 2 }
|
||||
];
|
||||
// All Normal.
|
||||
break;
|
||||
|
||||
case FACES.LEFT:
|
||||
// Left CW:
|
||||
// U[col 0] -> F[col 0] -> D[col 0] -> B[col 2] (Reverse) -> U...
|
||||
// U[0][0] (Back-Left) -> F[0][0] (Front-Left). Yes.
|
||||
// F[0][0] -> D[0][0] (Front-Left). Yes.
|
||||
// D[0][0] -> B[2][2] (Back-Left). (D is bottom, B is back).
|
||||
// D[2][0] (Back-Left) -> B[0][2] (Back-Left).
|
||||
// So D[col 0] (Normal) -> B[col 2] (Reverse).
|
||||
// B[col 2] (Reverse) -> U[col 0] (Normal).
|
||||
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'col', index: 0 },
|
||||
{ face: FACES.FRONT, type: 'col', index: 0 },
|
||||
{ face: FACES.DOWN, type: 'col', index: 0 },
|
||||
{ face: FACES.BACK, type: 'col', index: 2 } // Reverse mapping logic needed
|
||||
];
|
||||
// U -> F: Normal.
|
||||
// F -> D: Normal.
|
||||
// D -> B: Reverse. (D[0][0] -> B[2][2], D[2][0] -> B[0][2])
|
||||
// B -> U: Reverse. (B[2][2] -> U[0][0], B[0][2] -> U[2][0])
|
||||
break;
|
||||
|
||||
case FACES.RIGHT:
|
||||
// Right CW:
|
||||
// U[col 2] -> B[col 0] (Reverse) -> D[col 2] -> F[col 2] -> U...
|
||||
// U[2][2] (Front-Right) -> B[0][0] (Front-Right? No. B[0][0] is Back-Top-Right).
|
||||
// U[2][2] is Top-Right corner of Up. That's Front-Right corner.
|
||||
// B[0][0] is Top-Right corner of Back (looking from back). Which is Top-Left looking from front.
|
||||
// Wait. Right CW moves Front to Up? No.
|
||||
// Right CW moves Front-Right to Top-Right? No.
|
||||
// Right CW moves Front-Right to Up-Right? No.
|
||||
// Right CW moves Up-Right to Back-Right? No.
|
||||
// Right CW moves Up-Right to Back-Left (on the net).
|
||||
// U[col 2] -> B[col 0] (Reversed).
|
||||
// B[col 0] (Reversed) -> D[col 2].
|
||||
// D[col 2] -> F[col 2].
|
||||
// F[col 2] -> U[col 2].
|
||||
|
||||
cycle = [
|
||||
{ face: FACES.UP, type: 'col', index: 2 },
|
||||
{ face: FACES.BACK, type: 'col', index: 0 },
|
||||
{ face: FACES.DOWN, type: 'col', index: 2 },
|
||||
{ face: FACES.FRONT, type: 'col', index: 2 }
|
||||
];
|
||||
// U -> B: Reverse.
|
||||
// B -> D: Reverse.
|
||||
// D -> F: Normal.
|
||||
// F -> U: Normal.
|
||||
break;
|
||||
}
|
||||
|
||||
if (direction === -1) {
|
||||
// Reverse cycle order
|
||||
// A -> B -> C -> D ==> D -> C -> B -> A
|
||||
// But we also need to invert the mapping logic?
|
||||
// A -> B (Normal) becomes B -> A (Normal).
|
||||
// A -> B (Reverse) becomes B -> A (Reverse).
|
||||
// Yes, reversibility is symmetric.
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_applyCycle(cycle, direction, faceName) {
|
||||
// Cycle is an array of segment definitions.
|
||||
// Values move: cycle[0] -> cycle[1] -> cycle[2] -> cycle[3] -> cycle[0].
|
||||
|
||||
// We need to handle the specific "Reverse" mappings derived earlier.
|
||||
// Since logic is complex, I will implement specific mappings for each face in the switch above?
|
||||
// Or use a generalized "transform" property in cycle.
|
||||
|
||||
// Let's refine the cycle definition to include `transform` which says how Current maps to Next.
|
||||
// transform: 'normal' | 'reverse'.
|
||||
|
||||
// Re-deriving transforms for CW:
|
||||
// FRONT: U->R (N), R->D (R), D->L (N), L->U (N)?
|
||||
// U[2] (L-R) -> R[col 0] (T-B). N.
|
||||
// R[col 0] (T-B) -> D[0] (L-R). R[0][0]->D[0][0]? R[0][0] is Top-Left of Right (Front-Top). D[0][0] is Top-Left of Down (Front-Left).
|
||||
// R[0][0] should move to D[0][2]?
|
||||
// R[0][0] (Front-Top-Right corner) moves to D[0][2] (Front-Bottom-Right corner)?
|
||||
// Front CW: Top-Right (U[2][2]) -> Bottom-Right (R[2][0]?)
|
||||
// U[2][2] -> R[0][0]? No.
|
||||
// U[2][2] is Front-Right corner of Top face.
|
||||
// R[0][0] is Top-Left corner of Right face (adjacent to Front-Top-Right).
|
||||
// Yes, U[2][2] moves to R[0][0].
|
||||
// So U[2] (L-R) -> R[col 0] (T-B).
|
||||
// U[2][0] -> R[0][0]? No. U[2][0] is Front-Left. R[0][0] is Front-Right.
|
||||
// U[2][0] moves to R[2][0]? No.
|
||||
// U[2][0] moves to... Front CW. Front-Top-Left moves to Front-Top-Right.
|
||||
// U[2][0] is adjacent to Front-Top-Left.
|
||||
// It moves to position adjacent to Front-Top-Right.
|
||||
// Which is R[0][0].
|
||||
// So U[2][0] -> R[0][0].
|
||||
// U[2][2] -> R[2][0].
|
||||
// So U[2] (0..2) maps to R[col 0] (0..2). Normal.
|
||||
|
||||
// R[col 0] -> D[row 0].
|
||||
// R[0][0] (Front-Top-Right) moves to D[0][2] (Front-Bottom-Right).
|
||||
// R[2][0] (Front-Bottom-Right) moves to D[0][0] (Front-Bottom-Left).
|
||||
// So R[col 0] (0..2) maps to D[row 0] (2..0). REVERSE.
|
||||
|
||||
// D[row 0] -> L[col 2].
|
||||
// D[0][2] -> L[2][2]?
|
||||
// D[0][2] (Front-Bottom-Right) -> L[2][2] (Front-Bottom-Left).
|
||||
// D[0][0] (Front-Bottom-Left) -> L[0][2] (Front-Top-Left).
|
||||
// So D[row 0] (2..0) maps to L[col 2] (2..0). Normal (relative to reversed input).
|
||||
// Or: D[0] (0..2) maps to L[col 2] (0..2)?
|
||||
// D[0][0] -> L[0][2].
|
||||
// D[0][2] -> L[2][2].
|
||||
// So D[row 0] (0..2) maps to L[col 2] (0..2). Normal.
|
||||
|
||||
// L[col 2] -> U[row 2].
|
||||
// L[0][2] (Front-Top-Left) -> U[2][0] (Front-Top-Left).
|
||||
// L[2][2] -> U[2][2].
|
||||
// Normal.
|
||||
|
||||
// Summary Front CW:
|
||||
// U -> R (N)
|
||||
// R -> D (R)
|
||||
// D -> L (N)
|
||||
// L -> U (N)
|
||||
// Wait, D -> L logic check:
|
||||
// D[0][0] -> L[0][2].
|
||||
// D[0][2] -> L[2][2].
|
||||
// So D(0..2) -> L(0..2). Normal.
|
||||
// But D receives R reversed.
|
||||
// So R(0..2) -> D(2..0).
|
||||
// D(2..0) is [D[0][2], D[0][1], D[0][0]].
|
||||
// These move to L.
|
||||
// D[0][2] -> L[2][2].
|
||||
// D[0][0] -> L[0][2].
|
||||
// So values at D[2..0] move to L[2..0].
|
||||
// Matches.
|
||||
|
||||
// To implement this generically:
|
||||
// We need to define `reverse` for each step.
|
||||
|
||||
// Let's hardcode the logic per face to avoid bugs in generic solver.
|
||||
|
||||
const values = cycle.map(c => this._getSegment(c.face, c.type, c.index));
|
||||
const newValues = [];
|
||||
|
||||
if (faceName === FACES.FRONT) {
|
||||
if (direction === 1) {
|
||||
// U -> R (N), R -> D (R), D -> L (N), L -> U (N)
|
||||
// Order in cycle: U, R, D, L.
|
||||
// U gets L (N).
|
||||
// R gets U (N).
|
||||
// D gets R (R).
|
||||
// L gets D (N)? No.
|
||||
// Check L -> U.
|
||||
// L[0][2] -> U[2][0]. L[2][2] -> U[2][2].
|
||||
// L(0..2) -> U(0..2). Normal.
|
||||
|
||||
// Wait, cycle is A->B->C->D.
|
||||
// B gets A.
|
||||
// R gets U. (N)
|
||||
// D gets R. (R)
|
||||
// L gets D. (N)?
|
||||
// D[0][0] -> L[0][2]. D[0][2] -> L[2][2].
|
||||
// D(0..2) -> L(0..2). Normal.
|
||||
// But D holds Reversed R.
|
||||
// So R(0..2) -> D(2..0).
|
||||
// D(2..0) -> L(2..0).
|
||||
// L(2..0) -> U(2..0)?
|
||||
// L[2][2] -> U[2][2]. L[0][2] -> U[2][0].
|
||||
// Yes.
|
||||
|
||||
// So transforms:
|
||||
// U -> R: N
|
||||
// R -> D: R
|
||||
// D -> L: N
|
||||
// L -> U: N
|
||||
|
||||
newValues[0] = values[3]; // U gets L
|
||||
newValues[1] = values[0]; // R gets U
|
||||
newValues[2] = values[1].reverse(); // D gets R (Reversed)
|
||||
newValues[3] = values[2]; // L gets D
|
||||
} else {
|
||||
// CCW
|
||||
// U gets R.
|
||||
// R gets D (R).
|
||||
// D gets L.
|
||||
// L gets U.
|
||||
// Check U <- R.
|
||||
// U[2][0] <- R[0][0]? No.
|
||||
// U[2][0] (Front-Top-Left) gets R[0][0] (Front-Top-Right)? No.
|
||||
// U[2][0] gets L[0][2].
|
||||
// U[2][2] gets R[0][0]?
|
||||
// U[2][2] moves to L[0][2].
|
||||
// So U gets R?
|
||||
// U[2][2] gets R[0][0]? No, R[0][0] moves to D[0][2].
|
||||
// U[2][2] gets L[2][2]? No.
|
||||
|
||||
// CCW is reverse of CW.
|
||||
// U -> L -> D -> R -> U.
|
||||
// U gets R (from CW logic: L->U. So U->L).
|
||||
// Wait.
|
||||
// CW: U->R.
|
||||
// CCW: R->U.
|
||||
// So U gets R.
|
||||
// U[2][0] gets R[0][0]?
|
||||
// U[2][0] (Left) gets R[0][0] (Right)? No.
|
||||
// U[2][0] gets R[?].
|
||||
// Front CCW.
|
||||
// Top-Left (U[2][0]) moves to Left-Bottom (L[2][2]).
|
||||
// Top-Right (U[2][2]) moves to Left-Top (L[0][2]).
|
||||
|
||||
// Let's rely on standard: CCW is just inverse of CW.
|
||||
// If CW: A->B. CCW: B->A.
|
||||
// A_new = B_old.
|
||||
// U_new = R_old.
|
||||
// Check transform: U -> R was Normal. So R -> U should be Normal?
|
||||
// U[2][0] -> R[0][0].
|
||||
// So R[0][0] -> U[2][0].
|
||||
// R[0][0] (Top) -> U[2][0] (Left).
|
||||
// R[2][0] (Bottom) -> U[2][2] (Right).
|
||||
// So R(0..2) -> U(0..2). Normal.
|
||||
|
||||
// R_new = D_old.
|
||||
// R -> D was Reverse. So D -> R should be Reverse.
|
||||
// D(0..2) -> R(2..0).
|
||||
|
||||
// D_new = L_old.
|
||||
// D -> L was Normal.
|
||||
|
||||
// L_new = U_old.
|
||||
// L -> U was Normal.
|
||||
|
||||
newValues[0] = values[1]; // U gets R
|
||||
newValues[1] = values[2].reverse(); // R gets D (Reversed)
|
||||
newValues[2] = values[3]; // D gets L
|
||||
newValues[3] = values[0]; // L gets U
|
||||
}
|
||||
} else if (faceName === FACES.BACK) {
|
||||
if (direction === 1) {
|
||||
// CW: U(0) -> L(0) -> D(2) -> R(2) -> U(0)
|
||||
// U->L: R
|
||||
// L->D: N
|
||||
// D->R: N
|
||||
// R->U: R
|
||||
|
||||
// U gets R (R).
|
||||
// L gets U (R).
|
||||
// D gets L (N).
|
||||
// R gets D (N).
|
||||
|
||||
newValues[0] = values[3].reverse(); // U gets R (Rev)
|
||||
newValues[1] = values[0].reverse(); // L gets U (Rev)
|
||||
newValues[2] = values[1]; // D gets L
|
||||
newValues[3] = values[2]; // R gets D
|
||||
} else {
|
||||
// CCW: U gets L (R). L gets D (N). D gets R (N). R gets U (R).
|
||||
// Wait, inverse.
|
||||
// U -> L was R. So L -> U is R.
|
||||
// So U gets L (R).
|
||||
// L -> D was N. So D -> L is N.
|
||||
// L gets D (N).
|
||||
// D -> R was N.
|
||||
// D gets R (N).
|
||||
// R -> U was R.
|
||||
// R gets U (R).
|
||||
|
||||
newValues[0] = values[1].reverse(); // U gets L (Rev)
|
||||
newValues[1] = values[2]; // L gets D
|
||||
newValues[2] = values[3]; // D gets R
|
||||
newValues[3] = values[0].reverse(); // R gets U (Rev)
|
||||
}
|
||||
} else if (faceName === FACES.UP) {
|
||||
if (direction === 1) {
|
||||
// F -> L -> B -> R -> F. (All Normal)
|
||||
// F gets R.
|
||||
// L gets F.
|
||||
// B gets L.
|
||||
// R gets B.
|
||||
newValues[0] = values[3];
|
||||
newValues[1] = values[0];
|
||||
newValues[2] = values[1];
|
||||
newValues[3] = values[2];
|
||||
} else {
|
||||
// F gets L.
|
||||
newValues[0] = values[1];
|
||||
newValues[1] = values[2];
|
||||
newValues[2] = values[3];
|
||||
newValues[3] = values[0];
|
||||
}
|
||||
} else if (faceName === FACES.DOWN) {
|
||||
if (direction === 1) {
|
||||
// F -> R -> B -> L -> F. (All Normal)
|
||||
// F gets L.
|
||||
newValues[0] = values[3];
|
||||
newValues[1] = values[0];
|
||||
newValues[2] = values[1];
|
||||
newValues[3] = values[2];
|
||||
} else {
|
||||
// F gets R.
|
||||
newValues[0] = values[1];
|
||||
newValues[1] = values[2];
|
||||
newValues[2] = values[3];
|
||||
newValues[3] = values[0];
|
||||
}
|
||||
} else if (faceName === FACES.LEFT) {
|
||||
if (direction === 1) {
|
||||
// U -> F -> D -> B -> U
|
||||
// U->F: N
|
||||
// F->D: N
|
||||
// D->B: R
|
||||
// B->U: R
|
||||
|
||||
// U gets B (R)
|
||||
// F gets U (N)
|
||||
// D gets F (N)
|
||||
// B gets D (R)
|
||||
|
||||
newValues[0] = values[3].reverse();
|
||||
newValues[1] = values[0];
|
||||
newValues[2] = values[1];
|
||||
newValues[3] = values[2].reverse();
|
||||
} else {
|
||||
// U gets F (N)
|
||||
// F gets D (N)
|
||||
// D gets B (R)
|
||||
// B gets U (R)
|
||||
|
||||
newValues[0] = values[1];
|
||||
newValues[1] = values[2];
|
||||
newValues[2] = values[3].reverse();
|
||||
newValues[3] = values[0].reverse();
|
||||
}
|
||||
} else if (faceName === FACES.RIGHT) {
|
||||
if (direction === 1) {
|
||||
// U -> B -> D -> F -> U
|
||||
// U->B: R
|
||||
// B->D: R
|
||||
// D->F: N
|
||||
// F->U: N
|
||||
|
||||
// U gets F (N)
|
||||
// B gets U (R)
|
||||
// D gets B (R)
|
||||
// F gets D (N)
|
||||
|
||||
newValues[0] = values[3];
|
||||
newValues[1] = values[0].reverse();
|
||||
newValues[2] = values[1].reverse();
|
||||
newValues[3] = values[2];
|
||||
} else {
|
||||
// U gets B (R)
|
||||
// B gets D (R)
|
||||
// D gets F (N)
|
||||
// F gets U (N)
|
||||
|
||||
newValues[0] = values[1].reverse();
|
||||
newValues[1] = values[2].reverse();
|
||||
newValues[2] = values[3];
|
||||
newValues[3] = values[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply new values
|
||||
cycle.forEach((c, i) => {
|
||||
this._setSegment(c.face, c.type, c.index, newValues[i]);
|
||||
});
|
||||
}
|
||||
}
|
||||
112
src/utils/Matrix4.js
Normal file
112
src/utils/Matrix4.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export class Matrix4 {
|
||||
constructor() {
|
||||
this.elements = [
|
||||
1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
];
|
||||
}
|
||||
|
||||
static identity() {
|
||||
return new Matrix4();
|
||||
}
|
||||
|
||||
multiply(other) {
|
||||
const a = this.elements;
|
||||
const b = other.elements;
|
||||
const result = new Matrix4();
|
||||
const r = result.elements;
|
||||
|
||||
// a is row-major? No, CSS matrix3d is column-major.
|
||||
// Let's stick to column-major as per WebGL/CSS standard.
|
||||
// r[0] = a[0]*b[0] + a[4]*b[1] + a[8]*b[2] + a[12]*b[3] ...
|
||||
|
||||
for (let i = 0; i < 4; i++) { // Column of B
|
||||
for (let j = 0; j < 4; j++) { // Row of A
|
||||
let sum = 0;
|
||||
for (let k = 0; k < 4; k++) {
|
||||
sum += a[j + k * 4] * b[i * 4 + k]; // Correct for column-major storage
|
||||
}
|
||||
r[j + i * 4] = sum;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Multiply this * other
|
||||
multiplySelf(other) {
|
||||
this.elements = this.multiply(other).elements;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Multiply other * this (pre-multiply)
|
||||
premultiply(other) {
|
||||
this.elements = other.multiply(this).elements;
|
||||
return this;
|
||||
}
|
||||
|
||||
static translation(x, y, z) {
|
||||
const m = new Matrix4();
|
||||
m.elements[12] = x;
|
||||
m.elements[13] = y;
|
||||
m.elements[14] = z;
|
||||
return m;
|
||||
}
|
||||
|
||||
static rotationX(angleRad) {
|
||||
const m = new Matrix4();
|
||||
const c = Math.cos(angleRad);
|
||||
const s = Math.sin(angleRad);
|
||||
m.elements[5] = c;
|
||||
m.elements[6] = s;
|
||||
m.elements[9] = -s;
|
||||
m.elements[10] = c;
|
||||
return m;
|
||||
}
|
||||
|
||||
static rotationY(angleRad) {
|
||||
const m = new Matrix4();
|
||||
const c = Math.cos(angleRad);
|
||||
const s = Math.sin(angleRad);
|
||||
m.elements[0] = c;
|
||||
m.elements[2] = -s;
|
||||
m.elements[8] = s;
|
||||
m.elements[10] = c;
|
||||
return m;
|
||||
}
|
||||
|
||||
static rotationZ(angleRad) {
|
||||
const m = new Matrix4();
|
||||
const c = Math.cos(angleRad);
|
||||
const s = Math.sin(angleRad);
|
||||
m.elements[0] = c;
|
||||
m.elements[1] = s;
|
||||
m.elements[4] = -s;
|
||||
m.elements[5] = c;
|
||||
return m;
|
||||
}
|
||||
|
||||
translate(x, y, z) {
|
||||
return this.multiplySelf(Matrix4.translation(x, y, z));
|
||||
}
|
||||
|
||||
rotateX(deg) {
|
||||
return this.multiplySelf(Matrix4.rotationX(deg * Math.PI / 180));
|
||||
}
|
||||
|
||||
rotateY(deg) {
|
||||
return this.multiplySelf(Matrix4.rotationY(deg * Math.PI / 180));
|
||||
}
|
||||
|
||||
rotateZ(deg) {
|
||||
return this.multiplySelf(Matrix4.rotationZ(deg * Math.PI / 180));
|
||||
}
|
||||
|
||||
toCSS() {
|
||||
// CSS matrix3d takes comma-separated values
|
||||
// Round to avoid scientific notation like 1e-15 which CSS hates
|
||||
const rounded = this.elements.map(v => Math.abs(v) < 1e-10 ? 0 : v);
|
||||
return `matrix3d(${rounded.join(',')})`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user