fix: invert rotation logic and add debug tools
This commit is contained in:
20
src/App.vue
20
src/App.vue
@@ -1,17 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Main from './components/Main.vue'
|
import CubeCSS from './components/renderers/CubeCSS.vue'
|
||||||
import NavBar from './components/NavBar.vue'
|
import DebugPanel from './components/DebugPanel.vue'
|
||||||
import Footer from './components/Footer.vue'
|
import InteractionReplay from './components/InteractionReplay.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NavBar />
|
<div class="app-content">
|
||||||
|
<DebugPanel />
|
||||||
<main class="app-content">
|
<InteractionReplay />
|
||||||
<Main />
|
<CubeCSS />
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -24,5 +22,7 @@ import Footer from './components/Footer.vue'
|
|||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
137
src/components/DebugPanel.vue
Normal file
137
src/components/DebugPanel.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useDebug } from '../composables/useDebug'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const { settings } = useDebug()
|
||||||
|
const isOpen = ref(true)
|
||||||
|
|
||||||
|
const toggle = () => isOpen.value = !isOpen.value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="debug-panel" :class="{ open: isOpen }">
|
||||||
|
<div class="header" @click="toggle">
|
||||||
|
<span>🛠️ Debug Config</span>
|
||||||
|
<span>{{ isOpen ? '▼' : '▲' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isOpen" class="content">
|
||||||
|
<div class="section">
|
||||||
|
<h3>View Rotation</h3>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="settings.viewRotation.invertX"> Invert X (Up/Down)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="settings.viewRotation.invertY"> Invert Y (Left/Right)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Speed: <input type="number" step="0.1" v-model="settings.viewRotation.speed">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Drag Mappings (Sign)</h3>
|
||||||
|
<p class="hint">Adjust signs (-1 or 1) to correct drag direction</p>
|
||||||
|
|
||||||
|
<div class="face-group" v-for="(val, face) in settings.dragMapping" :key="face">
|
||||||
|
<strong>{{ face.toUpperCase() }}</strong>
|
||||||
|
<div class="controls">
|
||||||
|
<label>X: <input type="number" :step="2" :min="-1" :max="1" v-model="settings.dragMapping[face].x"></label>
|
||||||
|
<label>Y: <input type="number" :step="2" :min="-1" :max="1" v-model="settings.dragMapping[face].y"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Physics</h3>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="settings.physics.enabled"> Inertia & Snap
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.debug-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 250px;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 10px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
width: 40px;
|
||||||
|
background: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face-group {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: #222;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
188
src/components/InteractionReplay.vue
Normal file
188
src/components/InteractionReplay.vue
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useInteractionLogger } from '../composables/useInteractionLogger'
|
||||||
|
|
||||||
|
const { logs, isRecording, clearLogs, getRecentLogsForAnalysis } = useInteractionLogger()
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const copied = ref(false)
|
||||||
|
|
||||||
|
const toggle = () => isOpen.value = !isOpen.value
|
||||||
|
|
||||||
|
const copyReport = async () => {
|
||||||
|
const report = getRecentLogsForAnalysis(50)
|
||||||
|
const context = `
|
||||||
|
### User Interaction Report
|
||||||
|
Please analyze the following interaction logs to identify the issue.
|
||||||
|
Focus on: Drag direction, Active Layer, Rotation Mapping, and State changes.
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
${report}
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(context)
|
||||||
|
copied.value = true
|
||||||
|
setTimeout(() => copied.value = false, 2000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy logs', err)
|
||||||
|
alert('Failed to copy to clipboard. Check console.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="interaction-replay">
|
||||||
|
<div class="header" @click="toggle" :class="{ recording: isRecording }">
|
||||||
|
<span class="indicator">●</span>
|
||||||
|
<span>Logger ({{ logs.length }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isOpen" class="panel">
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="copyReport" :class="{ success: copied }">
|
||||||
|
{{ copied ? 'Copied!' : '📋 Copy Report for AI' }}
|
||||||
|
</button>
|
||||||
|
<button @click="clearLogs" class="secondary">Clear</button>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="isRecording"> Rec
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-list">
|
||||||
|
<div v-for="log in logs.slice().reverse()" :key="log.id" class="log-item">
|
||||||
|
<span class="time">{{ new Date(log.timestamp).toISOString().substr(14, 9) }}</span>
|
||||||
|
<span class="type" :class="log.type">{{ log.type }}</span>
|
||||||
|
<pre class="data">{{ log.data }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.interaction-replay {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 10000;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header.recording .indicator {
|
||||||
|
color: #ff4444;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 40px;
|
||||||
|
right: 0;
|
||||||
|
width: 350px;
|
||||||
|
height: 400px;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px;
|
||||||
|
background: #007acc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.success {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
background: #444;
|
||||||
|
flex: 0 0 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type.drag-start { color: #4fc3f7; }
|
||||||
|
.type.drag-update { color: #ffd54f; }
|
||||||
|
.type.drag-end { color: #81c784; }
|
||||||
|
.type.rotation { color: #ba68c8; }
|
||||||
|
|
||||||
|
.data {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 10px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRenderer } from '../composables/useRenderer'
|
import { useRenderer } from '../composables/useRenderer'
|
||||||
|
import { useCube } from '../composables/useCube'
|
||||||
import CubeCSS from './renderers/CubeCSS.vue'
|
import CubeCSS from './renderers/CubeCSS.vue'
|
||||||
import CubeSVG from './renderers/CubeSVG.vue'
|
import CubeSVG from './renderers/CubeSVG.vue'
|
||||||
import CubeCanvas from './renderers/CubeCanvas.vue'
|
import CubeCanvas from './renderers/CubeCanvas.vue'
|
||||||
|
|
||||||
const { activeRenderer, RENDERERS } = useRenderer()
|
const { activeRenderer, RENDERERS } = useRenderer()
|
||||||
|
const { cubeState } = useCube()
|
||||||
|
|
||||||
const currentRendererComponent = computed(() => {
|
const currentRendererComponent = computed(() => {
|
||||||
switch (activeRenderer.value) {
|
switch (activeRenderer.value) {
|
||||||
@@ -19,22 +21,89 @@ const currentRendererComponent = computed(() => {
|
|||||||
return CubeCSS
|
return CubeCSS
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const formattedState = computed(() => {
|
||||||
|
if (!cubeState.value) return '{}'
|
||||||
|
|
||||||
|
// Custom formatter to keep faces compact
|
||||||
|
const s = cubeState.value
|
||||||
|
const faces = Object.keys(s)
|
||||||
|
|
||||||
|
// Helper to shorten colors
|
||||||
|
const shortColor = (c) => c && typeof c === 'string' ? c[0].toUpperCase() : '-'
|
||||||
|
|
||||||
|
let out = '{\n'
|
||||||
|
faces.forEach((face, i) => {
|
||||||
|
const matrix = s[face]
|
||||||
|
// Format as ["WWW", "WWW", "WWW"]
|
||||||
|
const rows = matrix.map(row => `"${row.map(shortColor).join('')}"`).join(', ')
|
||||||
|
out += ` "${face}": [ ${rows} ]`
|
||||||
|
if (i < faces.length - 1) out += ','
|
||||||
|
out += '\n'
|
||||||
|
})
|
||||||
|
out += '}'
|
||||||
|
return out
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<component :is="currentRendererComponent" />
|
<div class="renderer-container">
|
||||||
|
<component :is="currentRendererComponent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="state-panel">
|
||||||
|
<h3>Cube State</h3>
|
||||||
|
<pre>{{ formattedState }}</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding: 2rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderer-container {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-panel {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useCube } from '../../composables/useCube'
|
import { useCube } from '../../composables/useCube'
|
||||||
|
import { useDebug } from '../../composables/useDebug'
|
||||||
|
import { useInteractionLogger } from '../../composables/useInteractionLogger'
|
||||||
|
|
||||||
const { cubeState, initCube, rotateLayer, COLOR_MAP, FACES } = useCube()
|
const { cubies, initCube, rotateLayer, FACES } = useCube()
|
||||||
|
const { settings: debugSettings } = useDebug()
|
||||||
|
const { addLog } = useInteractionLogger()
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const rx = ref(25)
|
const rx = ref(25)
|
||||||
@@ -19,173 +23,17 @@ const selectedCubieId = ref(null) // ID of the cubie where drag started
|
|||||||
const selectedFaceNormal = ref(null) // Normal vector of the face clicked
|
const selectedFaceNormal = ref(null) // Normal vector of the face clicked
|
||||||
|
|
||||||
// Animation state
|
// Animation state
|
||||||
const activeLayer = ref(null) // { axis: 'x'|'y'|'z', index: -1|0|1 }
|
const activeLayer = ref(null) // { axis: 'x'|'y'|'z', index: -1|0|1 }
|
||||||
const layerRotation = ref(0)
|
const layerRotation = ref(0)
|
||||||
const isSnapping = ref(false)
|
const isSnapping = ref(false)
|
||||||
const velocity = ref(0)
|
const velocity = ref(0)
|
||||||
const lastTime = ref(0)
|
const lastTime = ref(0)
|
||||||
const rafId = ref(null)
|
const rafId = ref(null)
|
||||||
|
|
||||||
// Cubies Model
|
|
||||||
// We represent the cube as 27 independent cubies.
|
|
||||||
// Each cubie has a current position (x, y, z) in grid coordinates [-1, 0, 1].
|
|
||||||
// And a rotation matrix (or simplified orientation).
|
|
||||||
// Actually, for CSS rendering, we can just keep track of their current (x,y,z) and applying transforms.
|
|
||||||
// But to match `useCube` state (which is color based), we need to map colors to cubies.
|
|
||||||
//
|
|
||||||
// Alternative:
|
|
||||||
// `useCube` maintains the logical state of colors on faces.
|
|
||||||
// To render 27 cubies that MOVE, we need to know which color belongs to which face of which cubie.
|
|
||||||
//
|
|
||||||
// Mapping:
|
|
||||||
// 27 Cubies. ID: 0..26.
|
|
||||||
// Position: x,y,z in {-1, 0, 1}.
|
|
||||||
//
|
|
||||||
// Colors:
|
|
||||||
// A cubie at (x,y,z) exposes faces if x/y/z is +/- 1.
|
|
||||||
// e.g. (1, 1, 1) is Right-Top-Front corner.
|
|
||||||
// It has 3 colored faces: Right, Top, Front.
|
|
||||||
// We need to fetch the color from `cubeState` at the correct indices.
|
|
||||||
//
|
|
||||||
// `cubeState` is organized by Faces.
|
|
||||||
// Front Face is a 3x3 matrix.
|
|
||||||
// (0,0) is Top-Left of Front Face.
|
|
||||||
// Front Face covers z=1 plane.
|
|
||||||
// x goes -1 (Left) to 1 (Right).
|
|
||||||
// y goes 1 (Top) to -1 (Bottom).
|
|
||||||
//
|
|
||||||
// Let's define the 27 cubies.
|
|
||||||
const cubies = ref([])
|
|
||||||
|
|
||||||
const initCubies = () => {
|
|
||||||
const newCubies = []
|
|
||||||
let id = 0
|
|
||||||
for (let x = -1; x <= 1; x++) {
|
|
||||||
for (let y = -1; y <= 1; y++) {
|
|
||||||
for (let z = -1; z <= 1; z++) {
|
|
||||||
newCubies.push({
|
|
||||||
id: id++,
|
|
||||||
x, y, z, // Current grid position
|
|
||||||
// Store initial rotation or accumulate transform?
|
|
||||||
// Simplest is to accumulate rotation transforms for the cubie div.
|
|
||||||
// But for logic, we update x,y,z after snap.
|
|
||||||
transform: ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cubies.value = newCubies
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map logical face colors to cubie faces
|
|
||||||
// We need a function that given a cubie (x,y,z) returns the colors of its 6 faces.
|
|
||||||
// If a face is internal, color is black (or null).
|
|
||||||
const getCubieFaces = (cubie) => {
|
|
||||||
const { x, y, z } = cubie
|
|
||||||
const faces = {}
|
|
||||||
|
|
||||||
// Helper to map grid (x,y) to Matrix indices (row, col)
|
|
||||||
// Grid: x (-1..1), y (-1..1).
|
|
||||||
// Matrix: row (0..2), col (0..2).
|
|
||||||
//
|
|
||||||
// Face UP (y=1). z: Back(-1)..Front(1). x: Left(-1)..Right(1).
|
|
||||||
// Up Matrix: row 0 is Back, row 2 is Front. col 0 is Left, col 2 is Right.
|
|
||||||
// So: row = z + 1? No.
|
|
||||||
// z=-1 -> row 0. z=0 -> row 1. z=1 -> row 2. Yes.
|
|
||||||
// x=-1 -> col 0. x=0 -> col 1. x=1 -> col 2. Yes.
|
|
||||||
if (y === 1) {
|
|
||||||
const row = z + 1
|
|
||||||
const col = x + 1
|
|
||||||
faces.up = getColor(FACES.UP, row, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Face DOWN (y=-1). z: Back(-1)..Front(1). x: Left(-1)..Right(1).
|
|
||||||
// Down Matrix: row 0 is Front, row 2 is Back. col 0 is Left, col 2 is Right.
|
|
||||||
// Wait, check standard mapping in `Cube.js` or standard rubik.
|
|
||||||
// Usually unfolding:
|
|
||||||
// Up: Back row is top.
|
|
||||||
// Down: Front row is top?
|
|
||||||
// Let's assume standard intuitive mapping:
|
|
||||||
// Down Face viewed from bottom.
|
|
||||||
// Row 0 is Front (top of view).
|
|
||||||
// z=1 -> row 0. z=-1 -> row 2.
|
|
||||||
// So row = 1 - z.
|
|
||||||
// x=-1 -> col 0. x=1 -> col 2.
|
|
||||||
if (y === -1) {
|
|
||||||
const row = 1 - z
|
|
||||||
const col = x + 1
|
|
||||||
faces.down = getColor(FACES.DOWN, row, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Face FRONT (z=1). y: Top(1)..Bottom(-1). x: Left(-1)..Right(1).
|
|
||||||
// Matrix: row 0 is Top.
|
|
||||||
// y=1 -> row 0. y=-1 -> row 2.
|
|
||||||
// row = 1 - y.
|
|
||||||
// x=-1 -> col 0.
|
|
||||||
if (z === 1) {
|
|
||||||
const row = 1 - y
|
|
||||||
const col = x + 1
|
|
||||||
faces.front = getColor(FACES.FRONT, row, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Face BACK (z=-1). y: Top(1)..Bottom(-1). x: Right(1)..Left(-1)?
|
|
||||||
// Back Face viewed from Back.
|
|
||||||
// Left side of view is Cube Right (x=1).
|
|
||||||
// Right side of view is Cube Left (x=-1).
|
|
||||||
// Matrix: row 0 is Top.
|
|
||||||
// y=1 -> row 0.
|
|
||||||
// col 0 (Left of view) -> x=1.
|
|
||||||
// col 2 (Right of view) -> x=-1.
|
|
||||||
// col = 1 - x.
|
|
||||||
if (z === -1) {
|
|
||||||
const row = 1 - y
|
|
||||||
const col = 1 - x
|
|
||||||
faces.back = getColor(FACES.BACK, row, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Face RIGHT (x=1). y: Top(1)..Bottom(-1). z: Front(1)..Back(-1).
|
|
||||||
// Right Face viewed from Right.
|
|
||||||
// Left side of view is Front (z=1).
|
|
||||||
// Right side of view is Back (z=-1).
|
|
||||||
// Matrix: row 0 is Top.
|
|
||||||
// y=1 -> row 0.
|
|
||||||
// col 0 -> z=1.
|
|
||||||
// col 2 -> z=-1.
|
|
||||||
// col = 1 - z.
|
|
||||||
if (x === 1) {
|
|
||||||
const row = 1 - y
|
|
||||||
const col = 1 - z
|
|
||||||
faces.right = getColor(FACES.RIGHT, row, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Face LEFT (x=-1). y: Top(1)..Bottom(-1). z: Back(-1)..Front(1).
|
|
||||||
// Left Face viewed from Left.
|
|
||||||
// Left side of view is Back (z=-1).
|
|
||||||
// Right side of view is Front (z=1).
|
|
||||||
// Matrix: row 0 is Top.
|
|
||||||
// y=1 -> row 0.
|
|
||||||
// col 0 -> z=-1.
|
|
||||||
// col 2 -> z=1.
|
|
||||||
// col = z + 1.
|
|
||||||
if (x === -1) {
|
|
||||||
const row = 1 - y
|
|
||||||
const col = z + 1
|
|
||||||
faces.left = getColor(FACES.LEFT, row, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
return faces
|
|
||||||
}
|
|
||||||
|
|
||||||
const getColor = (face, row, col) => {
|
|
||||||
if (!cubeState.value || !cubeState.value[face]) return 'black'
|
|
||||||
const colorIndex = cubeState.value[face][row][col]
|
|
||||||
return COLOR_MAP[colorIndex] || 'black'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mouse Interaction
|
// Mouse Interaction
|
||||||
const onMouseDown = (event) => {
|
const onMouseDown = (event) => {
|
||||||
if (isSnapping.value) return
|
if (isSnapping.value) return
|
||||||
|
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
startMouseX.value = event.clientX
|
startMouseX.value = event.clientX
|
||||||
startMouseY.value = event.clientY
|
startMouseY.value = event.clientY
|
||||||
@@ -193,128 +41,222 @@ const onMouseDown = (event) => {
|
|||||||
lastMouseY.value = event.clientY
|
lastMouseY.value = event.clientY
|
||||||
lastTime.value = performance.now()
|
lastTime.value = performance.now()
|
||||||
velocity.value = 0 // Reset velocity
|
velocity.value = 0 // Reset velocity
|
||||||
|
|
||||||
const target = event.target
|
const target = event.target
|
||||||
const stickerEl = target.closest('.sticker-face')
|
const stickerEl = target.closest('.sticker-face')
|
||||||
|
|
||||||
if (stickerEl) {
|
if (stickerEl) {
|
||||||
// Clicked on a cubie face
|
// Clicked on a cubie face
|
||||||
const cubieId = parseInt(stickerEl.dataset.cubieId)
|
const cubieId = parseInt(stickerEl.dataset.cubieId)
|
||||||
const faceName = stickerEl.dataset.face
|
const faceName = stickerEl.dataset.face
|
||||||
|
|
||||||
selectedCubieId.value = cubieId
|
selectedCubieId.value = cubieId
|
||||||
selectedFaceNormal.value = faceName // 'up', 'down', etc.
|
selectedFaceNormal.value = faceName // 'up', 'down', etc.
|
||||||
|
|
||||||
// Check if it's a center face (implies View Drag)?
|
|
||||||
// User wants drag to rotate layers if grabbing edge/corner.
|
|
||||||
// Center face of the whole cube? No, center face of a side.
|
|
||||||
// If I grab the center sticker of Front Face, I might want to rotate View OR Front Face?
|
|
||||||
// User said: "jedynie centralny element kostki dragowany, bedzie ja po prostu obracal"
|
|
||||||
// So Center Sticker -> View Drag.
|
|
||||||
// Center Sticker is when x,y,z has two zeros? No.
|
|
||||||
// Center of Front Face: (0,0,1).
|
|
||||||
// Edge: (1,0,1). Corner: (1,1,1).
|
|
||||||
|
|
||||||
const cubie = cubies.value.find(c => c.id === cubieId)
|
const cubie = cubies.value.find(c => c.id === cubieId)
|
||||||
const isCenter = (Math.abs(cubie.x) + Math.abs(cubie.y) + Math.abs(cubie.z)) === 1
|
const isCenter = (Math.abs(cubie.x) + Math.abs(cubie.y) + Math.abs(cubie.z)) === 1
|
||||||
|
|
||||||
if (isCenter) {
|
if (isCenter) {
|
||||||
dragMode.value = 'view'
|
dragMode.value = 'view'
|
||||||
document.body.style.cursor = 'move'
|
document.body.style.cursor = 'move'
|
||||||
|
addLog('drag-start', { mode: 'view', cubieId, face: faceName })
|
||||||
} else {
|
} else {
|
||||||
dragMode.value = 'layer'
|
dragMode.value = 'layer'
|
||||||
document.body.style.cursor = 'grab'
|
document.body.style.cursor = 'grab'
|
||||||
|
addLog('drag-start', { mode: 'layer', cubieId, face: faceName })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dragMode.value = 'view'
|
dragMode.value = 'view'
|
||||||
selectedCubieId.value = null
|
selectedCubieId.value = null
|
||||||
document.body.style.cursor = 'move'
|
document.body.style.cursor = 'move'
|
||||||
|
addLog('drag-start', { mode: 'view', target: 'background' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseMove = (event) => {
|
const onMouseMove = (event) => {
|
||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
|
|
||||||
if (dragMode.value === 'layer') {
|
if (dragMode.value === 'layer') {
|
||||||
document.body.style.cursor = 'grabbing'
|
document.body.style.cursor = 'grabbing'
|
||||||
}
|
}
|
||||||
|
|
||||||
const deltaX = event.clientX - lastMouseX.value
|
const deltaX = event.clientX - lastMouseX.value
|
||||||
const deltaY = event.clientY - lastMouseY.value
|
const deltaY = event.clientY - lastMouseY.value
|
||||||
|
|
||||||
if (dragMode.value === 'view') {
|
if (dragMode.value === 'view') {
|
||||||
ry.value += deltaX * 0.5
|
const s = debugSettings.viewRotation
|
||||||
rx.value -= deltaY * 0.5
|
const speed = s.speed || 0.5
|
||||||
velocity.value = 0 // Reset velocity for view drag (or track it separately if needed)
|
|
||||||
|
// Use debug settings for direction
|
||||||
|
ry.value += deltaX * speed * (s.invertY ? -1 : 1)
|
||||||
|
rx.value += deltaY * speed * (s.invertX ? -1 : 1)
|
||||||
|
|
||||||
|
velocity.value = 0
|
||||||
} else if (dragMode.value === 'layer' && selectedCubieId.value !== null) {
|
} else if (dragMode.value === 'layer' && selectedCubieId.value !== null) {
|
||||||
const totalDeltaX = event.clientX - startMouseX.value
|
const totalDeltaX = event.clientX - startMouseX.value
|
||||||
const totalDeltaY = event.clientY - startMouseY.value
|
const totalDeltaY = event.clientY - startMouseY.value
|
||||||
|
|
||||||
// Calculate velocity
|
|
||||||
const now = performance.now()
|
const now = performance.now()
|
||||||
const dt = now - lastTime.value
|
const dt = now - lastTime.value
|
||||||
lastTime.value = now
|
lastTime.value = now
|
||||||
|
|
||||||
// We only care about velocity of rotation, so we calculate it inside updateLayerDrag?
|
|
||||||
// Or we track mouse velocity here.
|
|
||||||
// Let's track rotation velocity in updateLayerDrag to be accurate with axis mapping.
|
|
||||||
updateLayerDrag(totalDeltaX, totalDeltaY, dt)
|
updateLayerDrag(totalDeltaX, totalDeltaY, dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMouseX.value = event.clientX
|
lastMouseX.value = event.clientX
|
||||||
lastMouseY.value = event.clientY
|
lastMouseY.value = event.clientY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRotationMapping = (face) => {
|
||||||
|
const m = debugSettings.dragMapping[face]
|
||||||
|
// Default structure but with signs from debug settings
|
||||||
|
const defaults = {
|
||||||
|
[FACES.FRONT]: [
|
||||||
|
{ axis: 'x', rotAxis: 'y', sign: m ? m.x : -1 },
|
||||||
|
{ axis: 'y', rotAxis: 'x', sign: m ? m.y : -1 }
|
||||||
|
],
|
||||||
|
[FACES.BACK]: [
|
||||||
|
{ axis: 'x', rotAxis: 'y', sign: m ? m.x : 1 },
|
||||||
|
{ axis: 'y', rotAxis: 'x', sign: m ? m.y : 1 }
|
||||||
|
],
|
||||||
|
[FACES.RIGHT]: [
|
||||||
|
{ axis: 'z', rotAxis: 'y', sign: m ? m.x : -1 },
|
||||||
|
{ axis: 'y', rotAxis: 'z', sign: m ? m.y : 1 }
|
||||||
|
],
|
||||||
|
[FACES.LEFT]: [
|
||||||
|
{ axis: 'z', rotAxis: 'y', sign: m ? m.x : -1 },
|
||||||
|
{ axis: 'y', rotAxis: 'z', sign: m ? m.y : -1 }
|
||||||
|
],
|
||||||
|
[FACES.UP]: [
|
||||||
|
{ axis: 'x', rotAxis: 'z', sign: m ? m.x : 1 },
|
||||||
|
{ axis: 'z', rotAxis: 'x', sign: m ? m.y : 1 }
|
||||||
|
],
|
||||||
|
[FACES.DOWN]: [
|
||||||
|
{ axis: 'x', rotAxis: 'z', sign: m ? m.x : -1 },
|
||||||
|
{ axis: 'z', rotAxis: 'x', sign: m ? m.y : -1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return defaults[face]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROTATION_MAPPING = {
|
||||||
|
// Kept for reference or initial state if needed, but we use getRotationMapping now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to project 3D vector to 2D screen space based on current view rotation
|
||||||
|
const projectVector = (vector) => {
|
||||||
|
const radX = rx.value * Math.PI / 180
|
||||||
|
const radY = ry.value * Math.PI / 180
|
||||||
|
const radZ = rz.value * Math.PI / 180
|
||||||
|
|
||||||
|
const { x, y, z } = vector
|
||||||
|
|
||||||
|
// v1 = Rz * v
|
||||||
|
let x1 = x * Math.cos(radZ) - y * Math.sin(radZ)
|
||||||
|
let y1 = x * Math.sin(radZ) + y * Math.cos(radZ)
|
||||||
|
let z1 = z
|
||||||
|
|
||||||
|
// v2 = Ry * v1
|
||||||
|
let x2 = x1 * Math.cos(radY) + z1 * Math.sin(radY)
|
||||||
|
let y2 = y1
|
||||||
|
let z2 = -x1 * Math.sin(radY) + z1 * Math.cos(radY)
|
||||||
|
|
||||||
|
// v3 = Rx * v2
|
||||||
|
let x3 = x2
|
||||||
|
let y3 = y2 * Math.cos(radX) - z2 * Math.sin(radX)
|
||||||
|
let z3 = y2 * Math.sin(radX) + z2 * Math.cos(radX)
|
||||||
|
|
||||||
|
return { x: x3, y: y3 }
|
||||||
|
}
|
||||||
|
|
||||||
const updateLayerDrag = (dx, dy, dt) => {
|
const updateLayerDrag = (dx, dy, dt) => {
|
||||||
// Determine rotation axis and direction based on drag vector and clicked face
|
|
||||||
const cubie = cubies.value.find(c => c.id === selectedCubieId.value)
|
const cubie = cubies.value.find(c => c.id === selectedCubieId.value)
|
||||||
if (!cubie) return
|
if (!cubie) return
|
||||||
|
|
||||||
// Need to map 2D drag to 3D axis.
|
|
||||||
// Face Normals:
|
|
||||||
// Front: Z. Right: X. Up: Y.
|
|
||||||
|
|
||||||
const face = selectedFaceNormal.value
|
|
||||||
let axis = null
|
let axis = null
|
||||||
let sign = 1
|
|
||||||
|
|
||||||
const absDx = Math.abs(dx)
|
|
||||||
const absDy = Math.abs(dy)
|
|
||||||
const isHorizontal = absDx > absDy
|
|
||||||
|
|
||||||
// Logic:
|
|
||||||
if (face === 'front' || face === 'back') {
|
|
||||||
if (isHorizontal) axis = 'y'; else axis = 'x';
|
|
||||||
} else if (face === 'right' || face === 'left') {
|
|
||||||
if (isHorizontal) axis = 'y'; else axis = 'z';
|
|
||||||
} else if (face === 'up' || face === 'down') {
|
|
||||||
if (isHorizontal) axis = 'y';
|
|
||||||
else axis = 'x';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!axis) return
|
|
||||||
|
|
||||||
// Determine layer index
|
|
||||||
let index = 0
|
let index = 0
|
||||||
if (axis === 'x') index = cubie.x
|
let dragVector = null
|
||||||
if (axis === 'y') index = cubie.y
|
|
||||||
if (axis === 'z') index = cubie.z
|
if (activeLayer.value) {
|
||||||
|
axis = activeLayer.value.axis
|
||||||
activeLayer.value = { axis, index }
|
index = activeLayer.value.index
|
||||||
|
dragVector = activeLayer.value.dragVector
|
||||||
// Determine Sign (Visual mapping)
|
} else {
|
||||||
const delta = isHorizontal ? dx : dy
|
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return
|
||||||
const baseSign = isHorizontal ? 1 : -1
|
|
||||||
|
const face = selectedFaceNormal.value
|
||||||
const newRotation = delta * baseSign * 0.5
|
// Use dynamic mapping from debug settings if available, else fallback to constant
|
||||||
|
// But better to make ROTATION_MAPPING computed or access directly
|
||||||
// Calculate velocity (deg/ms)
|
|
||||||
|
const mapping = getRotationMapping(face)
|
||||||
|
|
||||||
|
if (!mapping) return
|
||||||
|
|
||||||
|
// Create basis vectors for the two possible tangent axes
|
||||||
|
const vectors = mapping.map(m => {
|
||||||
|
const v = { x: 0, y: 0, z: 0 }
|
||||||
|
v[m.axis] = 1
|
||||||
|
return { ...m, vector: v }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Project them to screen space
|
||||||
|
const projected = vectors.map(v => {
|
||||||
|
const p = projectVector(v.vector)
|
||||||
|
const len = Math.sqrt(p.x * p.x + p.y * p.y)
|
||||||
|
return { ...v, px: p.x, py: p.y, len }
|
||||||
|
})
|
||||||
|
|
||||||
|
const mouseLen = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
if (mouseLen === 0) return
|
||||||
|
|
||||||
|
const ndx = dx / mouseLen
|
||||||
|
const ndy = dy / mouseLen
|
||||||
|
|
||||||
|
let bestMatch = null
|
||||||
|
let maxDot = -1
|
||||||
|
|
||||||
|
projected.forEach(p => {
|
||||||
|
if (p.len < 0.1) return
|
||||||
|
|
||||||
|
const npx = p.px / p.len
|
||||||
|
const npy = p.py / p.len
|
||||||
|
|
||||||
|
const dot = Math.abs(ndx * npx + ndy * npy)
|
||||||
|
if (dot > maxDot) {
|
||||||
|
maxDot = dot
|
||||||
|
bestMatch = p
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!bestMatch) return
|
||||||
|
|
||||||
|
axis = bestMatch.rotAxis
|
||||||
|
|
||||||
|
if (axis === 'x') index = cubie.x
|
||||||
|
if (axis === 'y') index = cubie.y
|
||||||
|
if (axis === 'z') index = cubie.z
|
||||||
|
|
||||||
|
dragVector = { x: bestMatch.px, y: bestMatch.py, sign: bestMatch.sign }
|
||||||
|
|
||||||
|
activeLayer.value = { axis, index, dragVector }
|
||||||
|
addLog('layer-select', { axis, index, vector: dragVector, face: selectedFaceNormal.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x: vx, y: vy, sign } = activeLayer.value.dragVector
|
||||||
|
const vLen = Math.sqrt(vx * vx + vy * vy)
|
||||||
|
if (vLen === 0) return
|
||||||
|
|
||||||
|
const nvx = vx / vLen
|
||||||
|
const nvy = vy / vLen
|
||||||
|
|
||||||
|
const moveAmount = dx * nvx + dy * nvy
|
||||||
|
const newRotation = moveAmount * sign * 0.5
|
||||||
|
|
||||||
if (dt > 0) {
|
if (dt > 0) {
|
||||||
const dRot = newRotation - layerRotation.value
|
const dRot = newRotation - layerRotation.value
|
||||||
// Simple low-pass filter for smoothing
|
|
||||||
velocity.value = 0.6 * velocity.value + 0.4 * (dRot / dt)
|
velocity.value = 0.6 * velocity.value + 0.4 * (dRot / dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
layerRotation.value = newRotation
|
layerRotation.value = newRotation
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,36 +264,29 @@ const onMouseUp = async () => {
|
|||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
document.body.style.cursor = ''
|
document.body.style.cursor = ''
|
||||||
|
|
||||||
if (dragMode.value === 'layer' && activeLayer.value) {
|
if (dragMode.value === 'layer' && activeLayer.value) {
|
||||||
isSnapping.value = true
|
isSnapping.value = true
|
||||||
|
|
||||||
// Inertia calculation
|
|
||||||
// Project final position based on velocity
|
|
||||||
// 200ms projection is reasonable for "throw" feel
|
|
||||||
const projection = velocity.value * 200
|
const projection = velocity.value * 200
|
||||||
const projectedRot = layerRotation.value + projection
|
const projectedRot = layerRotation.value + projection
|
||||||
|
|
||||||
// Snap to nearest 90 degrees
|
|
||||||
const steps = Math.round(projectedRot / 90)
|
const steps = Math.round(projectedRot / 90)
|
||||||
const targetRot = steps * 90
|
const targetRot = steps * 90
|
||||||
|
|
||||||
// Animation Loop
|
|
||||||
const startRot = layerRotation.value
|
const startRot = layerRotation.value
|
||||||
const startTime = performance.now()
|
const startTime = performance.now()
|
||||||
const duration = 300 // ms
|
const duration = 300
|
||||||
|
|
||||||
// Ease out cubic function
|
|
||||||
const easeOut = (t) => 1 - Math.pow(1 - t, 3)
|
const easeOut = (t) => 1 - Math.pow(1 - t, 3)
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const animate = (time) => {
|
const animate = (time) => {
|
||||||
const elapsed = time - startTime
|
const elapsed = time - startTime
|
||||||
const progress = Math.min(elapsed / duration, 1)
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
const ease = easeOut(progress)
|
const ease = easeOut(progress)
|
||||||
|
|
||||||
layerRotation.value = startRot + (targetRot - startRot) * ease
|
layerRotation.value = startRot + (targetRot - startRot) * ease
|
||||||
|
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
rafId.value = requestAnimationFrame(animate)
|
rafId.value = requestAnimationFrame(animate)
|
||||||
} else {
|
} else {
|
||||||
@@ -368,86 +303,28 @@ const onMouseUp = async () => {
|
|||||||
|
|
||||||
const finishRotation = (steps) => {
|
const finishRotation = (steps) => {
|
||||||
if (steps !== 0) {
|
if (steps !== 0) {
|
||||||
// Update logical state
|
|
||||||
const { axis, index } = activeLayer.value
|
const { axis, index } = activeLayer.value
|
||||||
let layerName = null
|
|
||||||
|
// Calculate logical direction
|
||||||
if (axis === 'x') {
|
// We found that Visual Rotation direction is inverted relative to Logical Rotation direction
|
||||||
if (index === -1) layerName = 'left'
|
// for all axes due to coordinate system differences (Y-down vs Y-up).
|
||||||
if (index === 1) layerName = 'right'
|
// Visual Positive -> Logical Negative.
|
||||||
} else if (axis === 'y') {
|
const direction = steps > 0 ? -1 : 1
|
||||||
if (index === 1) layerName = 'top'
|
const count = Math.abs(steps)
|
||||||
if (index === -1) layerName = 'bottom'
|
|
||||||
} else if (axis === 'z') {
|
for (let i = 0; i < count; i++) {
|
||||||
if (index === 1) layerName = 'front'
|
rotateLayer(axis, index, direction)
|
||||||
if (index === -1) layerName = 'back'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layerName) {
|
|
||||||
// Apply rotation to logical cube
|
|
||||||
let direction = steps > 0 ? 1 : -1
|
|
||||||
|
|
||||||
// Invert direction for specific layers where Visual and Logical rotations are opposite
|
|
||||||
if (layerName === 'top' || layerName === 'back' || layerName === 'right') {
|
|
||||||
direction = -direction
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = Math.abs(steps)
|
|
||||||
|
|
||||||
// 1. Update Logical State (Colors)
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
rotateLayer(layerName, direction)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Update Visual State (Cubies Position)
|
|
||||||
// We must rotate the (x,y,z) coordinates of the cubies that were in the active layer.
|
|
||||||
const visualSteps = steps // + means +90deg along axis
|
|
||||||
|
|
||||||
// Apply N times
|
|
||||||
const rotations = Math.abs(visualSteps)
|
|
||||||
const sign = Math.sign(visualSteps)
|
|
||||||
|
|
||||||
for (let r = 0; r < rotations; r++) {
|
|
||||||
cubies.value.forEach(cubie => {
|
|
||||||
// Check if cubie is in the rotating layer
|
|
||||||
let inLayer = false
|
|
||||||
if (axis === 'x' && cubie.x === index) inLayer = true
|
|
||||||
if (axis === 'y' && cubie.y === index) inLayer = true
|
|
||||||
if (axis === 'z' && cubie.z === index) inLayer = true
|
|
||||||
|
|
||||||
if (inLayer) {
|
|
||||||
const { x, y, z } = cubie
|
|
||||||
let nx = x, ny = y, nz = z
|
|
||||||
|
|
||||||
if (axis === 'x') {
|
|
||||||
if (sign > 0) { ny = -z; nz = y; } // (x, -z, y)
|
|
||||||
else { ny = z; nz = -y; } // (x, z, -y)
|
|
||||||
} else if (axis === 'y') {
|
|
||||||
if (sign > 0) { nx = z; nz = -x; } // (z, y, -x)
|
|
||||||
else { nx = -z; nz = x; } // (-z, y, x)
|
|
||||||
} else if (axis === 'z') {
|
|
||||||
if (sign > 0) { nx = -y; ny = x; } // (-y, x, z)
|
|
||||||
else { nx = y; ny = -x; } // (y, -x, z)
|
|
||||||
}
|
|
||||||
|
|
||||||
cubie.x = nx
|
|
||||||
cubie.y = ny
|
|
||||||
cubie.z = nz
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
addLog('rotation-finish', { axis, index, direction, steps, count })
|
||||||
}
|
}
|
||||||
|
|
||||||
activeLayer.value = null
|
activeLayer.value = null
|
||||||
layerRotation.value = 0
|
layerRotation.value = 0
|
||||||
isSnapping.value = false
|
isSnapping.value = false
|
||||||
velocity.value = 0
|
velocity.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initCubies()
|
|
||||||
initCube()
|
initCube()
|
||||||
window.addEventListener('mousemove', onMouseMove)
|
window.addEventListener('mousemove', onMouseMove)
|
||||||
window.addEventListener('mouseup', onMouseUp)
|
window.addEventListener('mouseup', onMouseUp)
|
||||||
@@ -459,51 +336,29 @@ onUnmounted(() => {
|
|||||||
if (rafId.value) cancelAnimationFrame(rafId.value)
|
if (rafId.value) cancelAnimationFrame(rafId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Styles
|
|
||||||
const cubeStyle = computed(() => ({
|
const cubeStyle = computed(() => ({
|
||||||
transform: `rotateX(${rx.value}deg) rotateY(${ry.value}deg) rotateZ(${rz.value}deg)`
|
transform: `rotateX(${rx.value}deg) rotateY(${ry.value}deg) rotateZ(${rz.value}deg)`
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const getCubieStyle = (cubie) => {
|
const getCubieStyle = (cubie) => {
|
||||||
// Base position
|
|
||||||
// scale 300px total. 100px per cubie.
|
|
||||||
// x,y,z in -1..1.
|
|
||||||
// translate(x*100, -y*100, z*100).
|
|
||||||
// Y is inverted in CSS (down is positive)?
|
|
||||||
// Usually in 3D CSS:
|
|
||||||
// X right, Y down, Z towards viewer.
|
|
||||||
// My Grid: Y=1 is Top.
|
|
||||||
// So Y=1 -> translateY(-100px).
|
|
||||||
|
|
||||||
const tx = cubie.x * 100
|
const tx = cubie.x * 100
|
||||||
const ty = cubie.y * -100
|
const ty = cubie.y * -100
|
||||||
const tz = cubie.z * 100
|
const tz = cubie.z * 100
|
||||||
|
|
||||||
let transform = `translate3d(${tx}px, ${ty}px, ${tz}px)`
|
let transform = `translate3d(${tx}px, ${ty}px, ${tz}px)`
|
||||||
|
|
||||||
// Apply rotation if active layer
|
|
||||||
if (activeLayer.value) {
|
if (activeLayer.value) {
|
||||||
const { axis, index } = activeLayer.value
|
const { axis, index } = activeLayer.value
|
||||||
let match = false
|
let match = false
|
||||||
if (axis === 'x' && cubie.x === index) match = true
|
if (axis === 'x' && cubie.x === index) match = true
|
||||||
if (axis === 'y' && cubie.y === index) match = true
|
if (axis === 'y' && cubie.y === index) match = true
|
||||||
if (axis === 'z' && cubie.z === index) match = true
|
if (axis === 'z' && cubie.z === index) match = true
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
// Rotation origin is center of cube (0,0,0).
|
|
||||||
// But we are translating the cubie.
|
|
||||||
// To rotate around global axis, we should rotate THEN translate?
|
|
||||||
// No, the Group rotates.
|
|
||||||
// But here we rotate individual cubies.
|
|
||||||
// A cubie at (100,0,0) rotating around Y axis:
|
|
||||||
// Needs to move in arc.
|
|
||||||
// `rotateY(angle) translate(...)` -> Rotates axis then moves.
|
|
||||||
// Yes. `rotateY` first puts it on the rotated axis.
|
|
||||||
// So `rotate` then `translate`.
|
|
||||||
transform = `rotate${axis.toUpperCase()}(${layerRotation.value}deg) ${transform}`
|
transform = `rotate${axis.toUpperCase()}(${layerRotation.value}deg) ${transform}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { transform }
|
return { transform }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -512,12 +367,9 @@ const getCubieStyle = (cubie) => {
|
|||||||
<div class="scene" @mousedown="onMouseDown">
|
<div class="scene" @mousedown="onMouseDown">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="cube-group" :style="cubeStyle">
|
<div class="cube-group" :style="cubeStyle">
|
||||||
|
|
||||||
<div v-for="cubie in cubies" :key="cubie.id" class="cubie" :style="getCubieStyle(cubie)">
|
<div v-for="cubie in cubies" :key="cubie.id" class="cubie" :style="getCubieStyle(cubie)">
|
||||||
<!-- Render 6 faces for each cubie -->
|
<div v-for="(color, face) in cubie.faces" :key="face"
|
||||||
<!-- Only render if color is not black? Optimization. -->
|
|
||||||
|
|
||||||
<div v-for="(color, face) in getCubieFaces(cubie)" :key="face"
|
|
||||||
class="sticker-face"
|
class="sticker-face"
|
||||||
:class="face"
|
:class="face"
|
||||||
:data-cubie-id="cubie.id"
|
:data-cubie-id="cubie.id"
|
||||||
@@ -525,7 +377,6 @@ const getCubieStyle = (cubie) => {
|
|||||||
:style="{ backgroundColor: color }">
|
:style="{ backgroundColor: color }">
|
||||||
<div class="sticker-border"></div>
|
<div class="sticker-border"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -546,7 +397,7 @@ const getCubieStyle = (cubie) => {
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
perspective: 900px;
|
perspective: 900px;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cube-group {
|
.cube-group {
|
||||||
@@ -576,8 +427,6 @@ const getCubieStyle = (cubie) => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
backface-visibility: hidden; /* Optimization? Or we want to see inside? */
|
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 {
|
.sticker-border {
|
||||||
@@ -586,9 +435,6 @@ const getCubieStyle = (cubie) => {
|
|||||||
border: 2px solid rgba(0,0,0,0.5);
|
border: 2px solid rgba(0,0,0,0.5);
|
||||||
border-radius: 8px; /* Rounded sticker */
|
border-radius: 8px; /* Rounded sticker */
|
||||||
background: inherit; /* Sticker color */
|
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 */
|
/* Face transforms relative to Cubie Center */
|
||||||
@@ -600,4 +446,3 @@ const getCubieStyle = (cubie) => {
|
|||||||
.sticker-face.down { transform: rotateX(-90deg) translateZ(50px); }
|
.sticker-face.down { transform: rotateX(-90deg) translateZ(50px); }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +1,43 @@
|
|||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { Cube, COLORS, FACES } from '../utils/Cube';
|
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() {
|
export function useCube() {
|
||||||
const cube = ref(new Cube());
|
const cube = ref(new Cube());
|
||||||
|
|
||||||
// Expose state for rendering (flattened for Vue template simplicity or kept as matrix)
|
// Make cubies reactive so Vue tracks changes
|
||||||
// Let's expose matrix but maybe helper to flatten if needed?
|
// We can just expose the cube instance, but better to expose reactive properties
|
||||||
// The Cube class uses 3x3 matrices.
|
// Since `cube` is a ref, `cube.value.cubies` is not deeply reactive by default unless `cube.value` is reactive.
|
||||||
// The Vue template currently iterates `cubeState.top` (array of 9).
|
// But `ref` wraps the object. If we mutate properties of the object, it might not trigger.
|
||||||
// We should adapt `cubeState` to match what the template expects OR update template.
|
// Let's rely on triggering updates manually or creating a new instance on reset.
|
||||||
// The user asked to "Dostosować src/components/Main.vue do renderowania nowego modelu danych".
|
// For rotation, we will force update.
|
||||||
// So we can expose the 3x3 matrices directly.
|
|
||||||
|
|
||||||
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 = () => {
|
const initCube = () => {
|
||||||
cube.value.reset();
|
cube.value.reset();
|
||||||
|
triggerUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateLayer = (layer, direction) => {
|
const triggerUpdate = () => {
|
||||||
// layer is string 'top', 'front' etc.
|
// Force Vue to notice change
|
||||||
// Map string to FACES constant if needed, but FACES values are 'up', 'down', etc.
|
cube.value = Object.assign(Object.create(Object.getPrototypeOf(cube.value)), cube.value);
|
||||||
// The previous implementation used 'top', 'bottom', 'left', 'right', 'front', 'back'.
|
};
|
||||||
// Cube.js uses 'up', 'down', 'left', 'right', 'front', 'back'.
|
|
||||||
|
const rotateLayer = (axis, index, direction) => {
|
||||||
// Map legacy layer names to new Face names
|
cube.value.rotateLayer(axis, index, direction);
|
||||||
const layerMap = {
|
triggerUpdate();
|
||||||
'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 {
|
return {
|
||||||
|
cube,
|
||||||
|
cubies,
|
||||||
cubeState,
|
cubeState,
|
||||||
initCube,
|
initCube,
|
||||||
rotateLayer,
|
rotateLayer,
|
||||||
COLOR_MAP,
|
COLORS,
|
||||||
FACES
|
FACES
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/composables/useDebug.js
Normal file
50
src/composables/useDebug.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/composables/useInteractionLogger.js
Normal file
50
src/composables/useInteractionLogger.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
import MatrixLib from 'matrix-js';
|
|
||||||
|
|
||||||
const Matrix = MatrixLib && MatrixLib.default ? MatrixLib.default : MatrixLib;
|
// Enum for colors
|
||||||
|
|
||||||
const mod = (n, m) => ((n % m) + m) % m;
|
|
||||||
|
|
||||||
// Enum for colors/faces
|
|
||||||
export const COLORS = {
|
export const COLORS = {
|
||||||
WHITE: 0,
|
WHITE: 'white',
|
||||||
YELLOW: 1,
|
YELLOW: 'yellow',
|
||||||
ORANGE: 2,
|
ORANGE: 'orange',
|
||||||
RED: 3,
|
RED: 'red',
|
||||||
GREEN: 4,
|
GREEN: 'green',
|
||||||
BLUE: 5,
|
BLUE: 'blue',
|
||||||
|
BLACK: 'black'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Faces mapping
|
// Faces enum
|
||||||
export const FACES = {
|
export const FACES = {
|
||||||
UP: 'up',
|
UP: 'up',
|
||||||
DOWN: 'down',
|
DOWN: 'down',
|
||||||
@@ -24,229 +20,481 @@ export const FACES = {
|
|||||||
BACK: 'back',
|
BACK: 'back',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial state: Solved cube
|
class Cubie {
|
||||||
const INITIAL_STATE = {
|
constructor(id, x, y, z) {
|
||||||
[FACES.UP]: Array(3).fill().map(() => Array(3).fill(COLORS.WHITE)),
|
this.id = id;
|
||||||
[FACES.DOWN]: Array(3).fill().map(() => Array(3).fill(COLORS.YELLOW)),
|
this.x = x;
|
||||||
[FACES.LEFT]: Array(3).fill().map(() => Array(3).fill(COLORS.ORANGE)),
|
this.y = y;
|
||||||
[FACES.RIGHT]: Array(3).fill().map(() => Array(3).fill(COLORS.RED)),
|
this.z = z;
|
||||||
[FACES.FRONT]: Array(3).fill().map(() => Array(3).fill(COLORS.GREEN)),
|
this.faces = {
|
||||||
[FACES.BACK]: Array(3).fill().map(() => Array(3).fill(COLORS.BLUE)),
|
[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 {
|
export class Cube {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.cubies = [];
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
// Deep copy initial state
|
this.cubies = [];
|
||||||
this.state = JSON.parse(JSON.stringify(INITIAL_STATE));
|
let id = 0;
|
||||||
}
|
for (let x = -1; x <= 1; x++) {
|
||||||
|
for (let y = -1; y <= 1; y++) {
|
||||||
// Rotate a 3x3 matrix 90 degrees clockwise
|
for (let z = -1; z <= 1; z++) {
|
||||||
_rotateMatrixCW(matrix) {
|
this.cubies.push(new Cubie(id++, x, y, z));
|
||||||
// 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];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyCycle(cycle, direction, faceName) {
|
// Perform a standard move (U, D, L, R, F, B, M, E, S, x, y, z)
|
||||||
const values = cycle.map(c => {
|
// Modifier: ' (prime) or 2 (double)
|
||||||
let val = this._getSegment(c.face, c.type, c.index);
|
move(moveStr) {
|
||||||
return c.reverse ? val.reverse() : val;
|
let move = moveStr[0];
|
||||||
});
|
let modifier = moveStr.length > 1 ? moveStr[1] : '';
|
||||||
|
|
||||||
const newValues = [];
|
let direction = 1; // CW
|
||||||
|
let times = 1;
|
||||||
// Shift values
|
|
||||||
// Last element moves to first position
|
if (modifier === "'") {
|
||||||
const last = values[values.length - 1];
|
direction = -1;
|
||||||
for (let i = 0; i < values.length; i++) {
|
} else if (modifier === '2') {
|
||||||
// Calculate previous index with modulo
|
times = 2;
|
||||||
// i=0 -> prev=3. i=1 -> prev=0.
|
|
||||||
const prevIdx = mod(i - 1, values.length);
|
|
||||||
newValues[i] = values[prevIdx];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply new values with reverse logic if needed
|
// Standard Notation Mapping to (axis, index, direction)
|
||||||
cycle.forEach((c, i) => {
|
// Note: Direction 1 in rotateLayer is "Positive Axis Rotation".
|
||||||
let val = newValues[i];
|
// We need to map Standard CW to Axis Direction.
|
||||||
if (c.reverse) val = val.reverse();
|
|
||||||
this._setSegment(c.face, c.type, c.index, val);
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
test/cube_logic.test.js
Normal file
96
test/cube_logic.test.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
109
test/cube_matrix.test.js
Normal file
109
test/cube_matrix.test.js
Normal file
@@ -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');
|
||||||
|
|
||||||
Reference in New Issue
Block a user