feat: implement split rendering for smooth layer rotation, bump version to 0.0.3
All checks were successful
Deploy to Production / deploy (push) Successful in 12s
All checks were successful
Deploy to Production / deploy (push) Successful in 12s
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gkucmierz/utils": "^1.28.3",
|
"@gkucmierz/utils": "^1.28.3",
|
||||||
"lucide-vue-next": "^0.564.0",
|
"lucide-vue-next": "^0.564.0",
|
||||||
|
"matrix-js": "^1.8.0",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -985,6 +986,11 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/matrix-js": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/matrix-js/-/matrix-js-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-2PHn6veiSf7aS/VBhdgrUVYCjVBkaAwFtIuXUnrHduKbSNpFHYzkdPYPgKI95idqFMKKEieYoMglimo2YGIapQ=="
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "rubic-cube",
|
"name": "rubic-cube",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gkucmierz/utils": "^1.28.3",
|
"@gkucmierz/utils": "^1.28.3",
|
||||||
"lucide-vue-next": "^0.564.0",
|
"lucide-vue-next": "^0.564.0",
|
||||||
|
"matrix-js": "^1.8.0",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,215 +1,29 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useCube } from '../composables/useCube'
|
import { useRenderer } from '../composables/useRenderer'
|
||||||
|
import CubeCSS from './renderers/CubeCSS.vue'
|
||||||
|
import CubeSVG from './renderers/CubeSVG.vue'
|
||||||
|
import CubeCanvas from './renderers/CubeCanvas.vue'
|
||||||
|
|
||||||
const { cubeState, initCube, rotateLayer, COLOR_MAP } = useCube()
|
const { activeRenderer, RENDERERS } = useRenderer()
|
||||||
|
|
||||||
const getFaceColors = (faceName) => {
|
const currentRendererComponent = computed(() => {
|
||||||
if (!cubeState.value || !cubeState.value[faceName]) {
|
switch (activeRenderer.value) {
|
||||||
// Return placeholder colors if state is not ready
|
case RENDERERS.CSS:
|
||||||
return Array(9).fill('#333');
|
return CubeCSS
|
||||||
|
case RENDERERS.SVG:
|
||||||
|
return CubeSVG
|
||||||
|
case RENDERERS.CANVAS:
|
||||||
|
return CubeCanvas
|
||||||
|
default:
|
||||||
|
return CubeCSS
|
||||||
}
|
}
|
||||||
// cubeState.value[faceName] is a 3x3 matrix.
|
|
||||||
// We need to flatten it.
|
|
||||||
try {
|
|
||||||
return cubeState.value[faceName].flat().map(c => COLOR_MAP[c] || 'gray');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error mapping face colors', e);
|
|
||||||
return Array(9).fill('red'); // Error state
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ...ref(25)
|
|
||||||
const ry = ref(25)
|
|
||||||
const rz = ref(0)
|
|
||||||
|
|
||||||
const isDragging = ref(false)
|
|
||||||
const dragMode = ref('view') // 'view' or 'layer'
|
|
||||||
const startMouseX = ref(0)
|
|
||||||
const startMouseY = ref(0)
|
|
||||||
const lastMouseX = ref(0)
|
|
||||||
const lastMouseY = ref(0)
|
|
||||||
const selectedFace = ref(null)
|
|
||||||
const selectedStickerIndex = ref(null)
|
|
||||||
|
|
||||||
const onMouseDown = (event) => {
|
|
||||||
isDragging.value = true
|
|
||||||
startMouseX.value = event.clientX
|
|
||||||
startMouseY.value = event.clientY
|
|
||||||
lastMouseX.value = event.clientX
|
|
||||||
lastMouseY.value = event.clientY
|
|
||||||
|
|
||||||
// Check target
|
|
||||||
const target = event.target
|
|
||||||
// Find closest face
|
|
||||||
const faceEl = target.closest('.face')
|
|
||||||
const stickerEl = target.closest('.sticker')
|
|
||||||
|
|
||||||
if (faceEl && stickerEl) {
|
|
||||||
const face = Array.from(faceEl.classList).find(c => ['top','bottom','left','right','front','back'].includes(c))
|
|
||||||
const index = parseInt(stickerEl.dataset.index)
|
|
||||||
|
|
||||||
selectedFace.value = face
|
|
||||||
selectedStickerIndex.value = index
|
|
||||||
|
|
||||||
// Center piece logic (index 4 is center)
|
|
||||||
if (index === 4) {
|
|
||||||
dragMode.value = 'view'
|
|
||||||
} else {
|
|
||||||
dragMode.value = 'layer'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dragMode.value = 'view'
|
|
||||||
selectedFace.value = null
|
|
||||||
selectedStickerIndex.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseMove = (event) => {
|
|
||||||
if (!isDragging.value) return
|
|
||||||
|
|
||||||
const deltaX = event.clientX - lastMouseX.value
|
|
||||||
const deltaY = event.clientY - lastMouseY.value
|
|
||||||
|
|
||||||
if (dragMode.value === 'view') {
|
|
||||||
ry.value += deltaX * 0.5
|
|
||||||
rx.value -= deltaY * 0.5
|
|
||||||
} else if (dragMode.value === 'layer' && selectedFace.value) {
|
|
||||||
const threshold = 10
|
|
||||||
const absX = Math.abs(event.clientX - startMouseX.value)
|
|
||||||
const absY = Math.abs(event.clientY - startMouseY.value)
|
|
||||||
|
|
||||||
if (absX > threshold || absY > threshold) {
|
|
||||||
handleLayerDrag(event.clientX - startMouseX.value, event.clientY - startMouseY.value)
|
|
||||||
isDragging.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMouseX.value = event.clientX
|
|
||||||
lastMouseY.value = event.clientY
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLayerDrag = (dx, dy) => {
|
|
||||||
if (!selectedFace.value || selectedStickerIndex.value === null) return
|
|
||||||
|
|
||||||
const face = selectedFace.value
|
|
||||||
const idx = selectedStickerIndex.value
|
|
||||||
|
|
||||||
// Determine row/col from index
|
|
||||||
const row = Math.floor(idx / 3)
|
|
||||||
const col = idx % 3
|
|
||||||
|
|
||||||
// Determine drag axis (Horizontal or Vertical)
|
|
||||||
const isHorizontal = Math.abs(dx) > Math.abs(dy)
|
|
||||||
const direction = isHorizontal ? (dx > 0 ? 1 : -1) : (dy > 0 ? 1 : -1)
|
|
||||||
|
|
||||||
// Direction logic:
|
|
||||||
// For standard faces (Front, Right, Top, etc.)
|
|
||||||
// Horizontal drag moves along Row -> Rotates the layer corresponding to that Row.
|
|
||||||
// Vertical drag moves along Col -> Rotates the layer corresponding to that Col.
|
|
||||||
|
|
||||||
let targetLayer = null
|
|
||||||
let rotDir = 1
|
|
||||||
|
|
||||||
// Map rows/cols to layers based on Face
|
|
||||||
// Note: Directions need careful mapping (CW/CCW).
|
|
||||||
// Assuming standard view for Front.
|
|
||||||
|
|
||||||
if (isHorizontal) {
|
|
||||||
// Dragging along a Row
|
|
||||||
if (row === 0) targetLayer = 'top'
|
|
||||||
if (row === 2) targetLayer = 'bottom'
|
|
||||||
// Middle row (1) - ignore or equator
|
|
||||||
|
|
||||||
// Direction mapping
|
|
||||||
// Drag Right (dx > 0) on Top Row of Front Face -> Top Layer rotates Left (CCW) or Right (CW)?
|
|
||||||
// Dragging Top layer Right -> moves face stickers Right. This is Y axis CW (viewed from top).
|
|
||||||
// Let's assume dx > 0 -> CW (1), dx < 0 -> CCW (-1).
|
|
||||||
// Adjust for specific faces (Back might be inverted).
|
|
||||||
if (face === 'back' || face === 'bottom') rotDir = -direction // Test this
|
|
||||||
else rotDir = direction
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Dragging along a Col
|
|
||||||
if (col === 0) targetLayer = 'left'
|
|
||||||
if (col === 2) targetLayer = 'right'
|
|
||||||
|
|
||||||
// Direction mapping
|
|
||||||
// Drag Down (dy > 0) on Right Col of Front Face -> Right Layer rotates Down.
|
|
||||||
// Right layer Down is X axis CW? No, X axis is Right. R layer moves "towards you" or "away".
|
|
||||||
// Wait, Right Face rotation (R) is CW.
|
|
||||||
// R (CW) moves top stickers to back. (Up).
|
|
||||||
// So R moves stickers Up.
|
|
||||||
// If I drag Down, I want R' (CCW).
|
|
||||||
// So dy > 0 -> -1.
|
|
||||||
rotDir = -direction
|
|
||||||
|
|
||||||
if (face === 'left' || face === 'back') rotDir = -rotDir // Invert again?
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetLayer) {
|
|
||||||
rotateLayer(targetLayer, rotDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
|
||||||
isDragging.value = false
|
|
||||||
selectedFace.value = null
|
|
||||||
selectedStickerIndex.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initCube()
|
|
||||||
window.addEventListener('mousemove', onMouseMove)
|
|
||||||
window.addEventListener('mouseup', onMouseUp)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('mousemove', onMouseMove)
|
|
||||||
window.removeEventListener('mouseup', onMouseUp)
|
|
||||||
})
|
|
||||||
|
|
||||||
const cubeStyle = computed(() => ({
|
|
||||||
transform: `rotateX(${rx.value}deg) rotateY(${ry.value}deg) rotateZ(${rz.value}deg)`
|
|
||||||
}))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="container" @mousedown="onMouseDown">
|
<component :is="currentRendererComponent" />
|
||||||
<div class="cube" :style="cubeStyle">
|
|
||||||
<div class="face top">
|
|
||||||
<div class="stickers">
|
|
||||||
<div class="sticker" v-for="(color, i) in getFaceColors('up')" :key="'t'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="face bottom">
|
|
||||||
<div class="stickers">
|
|
||||||
<div class="sticker" v-for="(color, i) in getFaceColors('down')" :key="'b'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="face left">
|
|
||||||
<div class="stickers">
|
|
||||||
<div class="sticker" v-for="(color, i) in getFaceColors('left')" :key="'l'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="face right">
|
|
||||||
<div class="stickers">
|
|
||||||
<div class="sticker" v-for="(color, i) in getFaceColors('right')" :key="'r'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="face front">
|
|
||||||
<div class="stickers">
|
|
||||||
<div class="sticker" v-for="(color, i) in getFaceColors('front')" :key="'f'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="face back">
|
|
||||||
<div class="stickers">
|
|
||||||
<div class="sticker" v-for="(color, i) in getFaceColors('back')" :key="'k'+i" :data-index="i" :style="{ backgroundColor: color }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -223,55 +37,4 @@ const cubeStyle = computed(() => ({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
perspective: 900px;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cube {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
transition: transform 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.face {
|
|
||||||
position: absolute;
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
border: 2px solid #000;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
opacity: 0.95;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.face.front { transform: rotateY(0deg) translateZ(150px); }
|
|
||||||
.face.right { transform: rotateY(90deg) translateZ(150px); }
|
|
||||||
.face.back { transform: rotateY(180deg) translateZ(150px); }
|
|
||||||
.face.left { transform: rotateY(-90deg) translateZ(150px); }
|
|
||||||
.face.top { transform: rotateX(90deg) translateZ(150px); }
|
|
||||||
.face.bottom { transform: rotateX(-90deg) translateZ(150px); }
|
|
||||||
|
|
||||||
.stickers {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
grid-template-rows: repeat(3, 1fr);
|
|
||||||
gap: 4px; /* Gap between stickers */
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticker {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Sun, Moon } from 'lucide-vue-next';
|
import { Sun, Moon } from 'lucide-vue-next';
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRenderer } from '../composables/useRenderer';
|
||||||
|
|
||||||
|
const { activeRenderer, setRenderer, RENDERERS } = useRenderer();
|
||||||
|
|
||||||
const isDark = ref(true);
|
const isDark = ref(true);
|
||||||
|
|
||||||
@@ -50,6 +53,18 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-container">
|
<div class="nav-container">
|
||||||
|
<div class="renderer-selector">
|
||||||
|
<button
|
||||||
|
v-for="renderer in RENDERERS"
|
||||||
|
:key="renderer"
|
||||||
|
@click="setRenderer(renderer)"
|
||||||
|
class="renderer-btn"
|
||||||
|
:class="{ active: activeRenderer === renderer }"
|
||||||
|
>
|
||||||
|
{{ renderer }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Theme Toggle -->
|
<!-- Theme Toggle -->
|
||||||
<button class="btn-neon nav-btn icon-only" @click="toggleTheme" :title="isDark ? 'Przełącz na jasny' : 'Przełącz na ciemny'">
|
<button class="btn-neon nav-btn icon-only" @click="toggleTheme" :title="isDark ? 'Przełącz na jasny' : 'Przełącz na ciemny'">
|
||||||
<Sun v-if="isDark" :size="20" />
|
<Sun v-if="isDark" :size="20" />
|
||||||
@@ -97,6 +112,36 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.renderer-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderer-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderer-btn:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderer-btn.active {
|
||||||
|
background: var(--glass-border);
|
||||||
|
color: var(--text-color);
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.nav-btn {
|
.nav-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
420
src/components/renderers/CubeCSS.vue
Normal file
420
src/components/renderers/CubeCSS.vue
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
import { useCube } from '../../composables/useCube'
|
||||||
|
|
||||||
|
const { cubeState, initCube, rotateLayer, COLOR_MAP } = useCube()
|
||||||
|
|
||||||
|
const getFaceColors = (faceName) => {
|
||||||
|
if (!cubeState.value || !cubeState.value[faceName]) {
|
||||||
|
return Array(9).fill('#333');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return cubeState.value[faceName].flat().map(c => COLOR_MAP[c] || 'gray');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error mapping face colors', e);
|
||||||
|
return Array(9).fill('red');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rx = ref(25)
|
||||||
|
const ry = ref(25)
|
||||||
|
const rz = ref(0)
|
||||||
|
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const dragMode = ref('view') // 'view' or 'layer'
|
||||||
|
const startMouseX = ref(0)
|
||||||
|
const startMouseY = ref(0)
|
||||||
|
const lastMouseX = ref(0)
|
||||||
|
const lastMouseY = ref(0)
|
||||||
|
const selectedFace = ref(null)
|
||||||
|
const selectedStickerIndex = ref(null)
|
||||||
|
|
||||||
|
// Animation state
|
||||||
|
const activeLayer = ref(null)
|
||||||
|
const layerRotation = ref(0)
|
||||||
|
const isSnapping = ref(false)
|
||||||
|
|
||||||
|
// Layer definitions (which stickers belong to which layer)
|
||||||
|
const LAYER_MAP = {
|
||||||
|
top: {
|
||||||
|
up: [0,1,2,3,4,5,6,7,8],
|
||||||
|
front: [0,1,2],
|
||||||
|
right: [0,1,2],
|
||||||
|
back: [0,1,2],
|
||||||
|
left: [0,1,2]
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
down: [0,1,2,3,4,5,6,7,8],
|
||||||
|
front: [6,7,8],
|
||||||
|
right: [6,7,8],
|
||||||
|
back: [6,7,8],
|
||||||
|
left: [6,7,8]
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
left: [0,1,2,3,4,5,6,7,8],
|
||||||
|
front: [0,3,6],
|
||||||
|
up: [0,3,6],
|
||||||
|
back: [2,5,8], // Opposite column on back face
|
||||||
|
down: [0,3,6]
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
right: [0,1,2,3,4,5,6,7,8],
|
||||||
|
front: [2,5,8],
|
||||||
|
up: [2,5,8],
|
||||||
|
back: [0,3,6], // Opposite column on back face
|
||||||
|
down: [2,5,8]
|
||||||
|
},
|
||||||
|
front: {
|
||||||
|
front: [0,1,2,3,4,5,6,7,8],
|
||||||
|
up: [6,7,8],
|
||||||
|
right: [0,3,6],
|
||||||
|
down: [0,1,2],
|
||||||
|
left: [2,5,8]
|
||||||
|
},
|
||||||
|
back: {
|
||||||
|
back: [0,1,2,3,4,5,6,7,8],
|
||||||
|
up: [0,1,2],
|
||||||
|
right: [2,5,8],
|
||||||
|
down: [6,7,8],
|
||||||
|
left: [0,3,6]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStickerInLayer = (layer, face, index) => {
|
||||||
|
if (!layer || !LAYER_MAP[layer]) return false
|
||||||
|
const indices = LAYER_MAP[layer][face]
|
||||||
|
return indices && indices.includes(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseDown = (event) => {
|
||||||
|
if (isSnapping.value) return // Prevent interaction during snap
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
startMouseX.value = event.clientX
|
||||||
|
startMouseY.value = event.clientY
|
||||||
|
lastMouseX.value = event.clientX
|
||||||
|
lastMouseY.value = event.clientY
|
||||||
|
|
||||||
|
const target = event.target
|
||||||
|
const faceEl = target.closest('.face')
|
||||||
|
const stickerEl = target.closest('.sticker')
|
||||||
|
|
||||||
|
if (faceEl && stickerEl) {
|
||||||
|
const face = Array.from(faceEl.classList).find(c => ['top','bottom','left','right','front','back'].includes(c))
|
||||||
|
const index = parseInt(stickerEl.dataset.index)
|
||||||
|
|
||||||
|
selectedFace.value = face
|
||||||
|
selectedStickerIndex.value = index
|
||||||
|
|
||||||
|
if (index === 4) {
|
||||||
|
dragMode.value = 'view'
|
||||||
|
} else {
|
||||||
|
dragMode.value = 'layer'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dragMode.value = 'view'
|
||||||
|
selectedFace.value = null
|
||||||
|
selectedStickerIndex.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseMove = (event) => {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
|
||||||
|
const deltaX = event.clientX - lastMouseX.value
|
||||||
|
const deltaY = event.clientY - lastMouseY.value
|
||||||
|
|
||||||
|
if (dragMode.value === 'view') {
|
||||||
|
ry.value += deltaX * 0.5
|
||||||
|
rx.value -= deltaY * 0.5
|
||||||
|
} else if (dragMode.value === 'layer' && selectedFace.value) {
|
||||||
|
const totalDeltaX = event.clientX - startMouseX.value
|
||||||
|
const totalDeltaY = event.clientY - startMouseY.value
|
||||||
|
|
||||||
|
updateLayerDrag(totalDeltaX, totalDeltaY)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMouseX.value = event.clientX
|
||||||
|
lastMouseY.value = event.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateLayerDrag = (dx, dy) => {
|
||||||
|
if (!selectedFace.value || selectedStickerIndex.value === null) return
|
||||||
|
|
||||||
|
// Determine direction if not yet determined (or update continuously)
|
||||||
|
// Logic from previous handleLayerDrag, but adapted for continuous angle
|
||||||
|
|
||||||
|
const face = selectedFace.value
|
||||||
|
const idx = selectedStickerIndex.value
|
||||||
|
|
||||||
|
const row = Math.floor(idx / 3)
|
||||||
|
const col = idx % 3
|
||||||
|
|
||||||
|
// Determine dominant axis for the drag
|
||||||
|
const isHorizontal = Math.abs(dx) > Math.abs(dy)
|
||||||
|
|
||||||
|
// Determine target layer based on drag start position and direction
|
||||||
|
let targetLayer = null
|
||||||
|
let rotSign = 1
|
||||||
|
|
||||||
|
if (isHorizontal) {
|
||||||
|
// Horizontal drag rotates Top or Bottom layers
|
||||||
|
if (row === 0) targetLayer = 'top'
|
||||||
|
if (row === 2) targetLayer = 'bottom'
|
||||||
|
// Middle row horizontal drag -> rotates Middle layer (skip for now or map to 'middle')
|
||||||
|
|
||||||
|
// Determine rotation sign
|
||||||
|
// If dragging right (dx > 0):
|
||||||
|
// On Front face: Top moves Left (CW? No).
|
||||||
|
// Let's use standard:
|
||||||
|
// Front Face, Top Row, Drag Right -> Face moves Right.
|
||||||
|
// That is Anti-Clockwise Top Rotation (Top Face turns Left).
|
||||||
|
// So dx > 0 -> angle < 0.
|
||||||
|
|
||||||
|
if (face === 'back' || face === 'bottom') rotSign = 1 // Inverted logic for back/bottom
|
||||||
|
else rotSign = -1
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Vertical drag rotates Left or Right layers
|
||||||
|
if (col === 0) targetLayer = 'left'
|
||||||
|
if (col === 2) targetLayer = 'right'
|
||||||
|
|
||||||
|
// Sign logic
|
||||||
|
// Front Face, Right Col, Drag Down (dy > 0).
|
||||||
|
// Face moves Down.
|
||||||
|
// Right Face rotates ... towards user?
|
||||||
|
// Right Face CW: Front moves Up.
|
||||||
|
// So Drag Down = Anti-Clockwise (-1).
|
||||||
|
|
||||||
|
rotSign = -1
|
||||||
|
if (face === 'left' || face === 'back') rotSign = 1 // Inverted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetLayer) {
|
||||||
|
activeLayer.value = targetLayer
|
||||||
|
// Sensitivity: 1px = 0.5 degree
|
||||||
|
const rawDelta = isHorizontal ? dx : dy
|
||||||
|
layerRotation.value = rawDelta * rotSign * 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = async () => {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
isDragging.value = false
|
||||||
|
|
||||||
|
if (dragMode.value === 'layer' && activeLayer.value) {
|
||||||
|
isSnapping.value = true
|
||||||
|
|
||||||
|
// Snap to nearest 90 degrees
|
||||||
|
const currentRot = layerRotation.value
|
||||||
|
const steps = Math.round(currentRot / 90)
|
||||||
|
const targetRot = steps * 90
|
||||||
|
|
||||||
|
// Animate to target
|
||||||
|
layerRotation.value = targetRot
|
||||||
|
|
||||||
|
// Wait for transition
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200)) // Match CSS transition time
|
||||||
|
|
||||||
|
// Commit changes to model
|
||||||
|
if (steps !== 0) {
|
||||||
|
// Rotate layer N times
|
||||||
|
const direction = steps > 0 ? 1 : -1
|
||||||
|
const count = Math.abs(steps)
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
rotateLayer(activeLayer.value, direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset animation state silently
|
||||||
|
// We need to disable transition temporarily to prevent "rewind" animation
|
||||||
|
const movingCube = document.querySelector('.cube.moving')
|
||||||
|
if (movingCube) movingCube.style.transition = 'none'
|
||||||
|
|
||||||
|
activeLayer.value = null
|
||||||
|
layerRotation.value = 0
|
||||||
|
|
||||||
|
// Re-enable transition next tick
|
||||||
|
nextTick(() => {
|
||||||
|
if (movingCube) movingCube.style.transition = ''
|
||||||
|
isSnapping.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
selectedFace.value = null
|
||||||
|
selectedStickerIndex.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCube()
|
||||||
|
window.addEventListener('mousemove', onMouseMove)
|
||||||
|
window.addEventListener('mouseup', onMouseUp)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('mousemove', onMouseMove)
|
||||||
|
window.removeEventListener('mouseup', onMouseUp)
|
||||||
|
})
|
||||||
|
|
||||||
|
const cubeStyle = computed(() => ({
|
||||||
|
transform: `rotateX(${rx.value}deg) rotateY(${ry.value}deg) rotateZ(${rz.value}deg)`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const movingGroupStyle = computed(() => {
|
||||||
|
if (!activeLayer.value) return {}
|
||||||
|
|
||||||
|
// Determine axis of rotation
|
||||||
|
let axis = 'Y'
|
||||||
|
if (['top', 'bottom'].includes(activeLayer.value)) axis = 'Y'
|
||||||
|
if (['left', 'right'].includes(activeLayer.value)) axis = 'X'
|
||||||
|
if (['front', 'back'].includes(activeLayer.value)) axis = 'Z'
|
||||||
|
|
||||||
|
// Correction for axis orientation to match standard rotation
|
||||||
|
// Up/Down rotate around Y axis? No.
|
||||||
|
// Standard CSS 3D:
|
||||||
|
// rotateY -> horizontal rotation (Left/Right)
|
||||||
|
// rotateX -> vertical rotation (Up/Down)
|
||||||
|
// rotateZ -> in-plane rotation (Front/Back)
|
||||||
|
|
||||||
|
// Map layer to axis:
|
||||||
|
// Top/Bottom -> rotate around Y axis (vertical axis of the cube)
|
||||||
|
// Left/Right -> rotate around X axis (horizontal axis)
|
||||||
|
// Front/Back -> rotate around Z axis (depth axis)
|
||||||
|
|
||||||
|
if (['top', 'bottom'].includes(activeLayer.value)) axis = 'Y'
|
||||||
|
else if (['left', 'right'].includes(activeLayer.value)) axis = 'X'
|
||||||
|
else axis = 'Z'
|
||||||
|
|
||||||
|
// We must concatenate the transforms because Vue style array merging overrides properties with same name.
|
||||||
|
// cubeStyle has 'transform' (view rotation).
|
||||||
|
// We want to append the layer rotation to it.
|
||||||
|
const viewTransform = cubeStyle.value.transform
|
||||||
|
const layerTransform = `rotate${axis}(${layerRotation.value}deg)`
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: `${viewTransform} ${layerTransform}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="scene" @mousedown="onMouseDown">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- Static Group (Non-moving parts) -->
|
||||||
|
<div class="cube static" :style="cubeStyle">
|
||||||
|
<div v-for="faceName in ['top', 'bottom', 'left', 'right', 'front', 'back']"
|
||||||
|
:key="'static-'+faceName"
|
||||||
|
class="face"
|
||||||
|
:class="faceName">
|
||||||
|
<div class="stickers">
|
||||||
|
<div v-for="(color, i) in getFaceColors(faceName === 'top' ? 'up' : faceName === 'bottom' ? 'down' : faceName)"
|
||||||
|
:key="'s-'+i"
|
||||||
|
class="sticker-wrapper"
|
||||||
|
:data-index="i"
|
||||||
|
:style="{ visibility: activeLayer && isStickerInLayer(activeLayer, faceName === 'top' ? 'up' : faceName === 'bottom' ? 'down' : faceName, i) ? 'hidden' : 'visible' }">
|
||||||
|
<div class="sticker" :style="{ backgroundColor: color }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Moving Group (Active layer) -->
|
||||||
|
<div v-if="activeLayer" class="cube moving" :style="movingGroupStyle">
|
||||||
|
<div v-for="faceName in ['top', 'bottom', 'left', 'right', 'front', 'back']"
|
||||||
|
:key="'moving-'+faceName"
|
||||||
|
class="face"
|
||||||
|
:class="faceName">
|
||||||
|
<div class="stickers">
|
||||||
|
<div v-for="(color, i) in getFaceColors(faceName === 'top' ? 'up' : faceName === 'bottom' ? 'down' : faceName)"
|
||||||
|
:key="'m-'+i"
|
||||||
|
class="sticker-wrapper"
|
||||||
|
:data-index="i"
|
||||||
|
:style="{ visibility: isStickerInLayer(activeLayer, faceName === 'top' ? 'up' : faceName === 'bottom' ? 'down' : faceName, i) ? 'visible' : 'hidden' }">
|
||||||
|
<div class="sticker" :style="{ backgroundColor: color }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scene {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
perspective: 900px;
|
||||||
|
pointer-events: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cube {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute; /* Stack them */
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: transform 0.1s; /* View rotation transition */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cube.moving {
|
||||||
|
transition: transform 0.2s ease-out; /* Layer snap transition */
|
||||||
|
}
|
||||||
|
|
||||||
|
.face {
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
border: 2px solid rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
/* Transparent background to allow seeing through split parts */
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face.front { transform: rotateY(0deg) translateZ(150px); }
|
||||||
|
.face.right { transform: rotateY(90deg) translateZ(150px); }
|
||||||
|
.face.back { transform: rotateY(180deg) translateZ(150px); }
|
||||||
|
.face.left { transform: rotateY(-90deg) translateZ(150px); }
|
||||||
|
.face.top { transform: rotateX(90deg) translateZ(150px); }
|
||||||
|
.face.bottom { transform: rotateX(-90deg) translateZ(150px); }
|
||||||
|
|
||||||
|
.stickers {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
gap: 4px; /* Gap between stickers */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticker-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: black; /* The black plastic look */
|
||||||
|
padding: 2px; /* Inner padding for sticker */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticker {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
34
src/components/renderers/CubeCanvas.vue
Normal file
34
src/components/renderers/CubeCanvas.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('Canvas Renderer mounted (placeholder)')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="canvas-container">
|
||||||
|
<canvas width="300" height="300"></canvas>
|
||||||
|
<div class="overlay">
|
||||||
|
Canvas Renderer (Coming Soon)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.canvas-container {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
28
src/components/renderers/CubeSVG.vue
Normal file
28
src/components/renderers/CubeSVG.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('SVG Renderer mounted (placeholder)')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="svg-container">
|
||||||
|
<svg width="300" height="300" viewBox="0 0 300 300">
|
||||||
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="white">
|
||||||
|
SVG Renderer (Coming Soon)
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.svg-container {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
src/composables/useRenderer.js
Normal file
23
src/composables/useRenderer.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const RENDERERS = {
|
||||||
|
CSS: 'CSS',
|
||||||
|
SVG: 'SVG',
|
||||||
|
CANVAS: 'Canvas'
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeRenderer = ref(RENDERERS.CSS);
|
||||||
|
|
||||||
|
export function useRenderer() {
|
||||||
|
const setRenderer = (renderer) => {
|
||||||
|
if (Object.values(RENDERERS).includes(renderer)) {
|
||||||
|
activeRenderer.value = renderer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeRenderer,
|
||||||
|
setRenderer,
|
||||||
|
RENDERERS
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import MatrixLib from 'matrix-js';
|
||||||
|
|
||||||
|
const Matrix = MatrixLib && MatrixLib.default ? MatrixLib.default : MatrixLib;
|
||||||
|
|
||||||
const mod = (n, m) => ((n % m) + m) % m;
|
const mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
|
||||||
// Enum for colors/faces
|
// Enum for colors/faces
|
||||||
@@ -42,26 +46,81 @@ export class Cube {
|
|||||||
|
|
||||||
// Rotate a 3x3 matrix 90 degrees clockwise
|
// Rotate a 3x3 matrix 90 degrees clockwise
|
||||||
_rotateMatrixCW(matrix) {
|
_rotateMatrixCW(matrix) {
|
||||||
const N = matrix.length;
|
// CW Rotation: Transpose -> Reverse Rows (or Reverse Cols -> Transpose?)
|
||||||
const result = Array(N).fill().map(() => Array(N).fill(null));
|
// CW: (x,y) -> (y, -x).
|
||||||
for (let i = 0; i < N; i++) {
|
// Transpose: (x,y) -> (y,x).
|
||||||
for (let j = 0; j < N; j++) {
|
// Reverse rows?
|
||||||
result[j][N - 1 - i] = matrix[i][j];
|
// 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());
|
||||||
}
|
}
|
||||||
return result;
|
// 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
|
// Rotate a 3x3 matrix 90 degrees counter-clockwise
|
||||||
_rotateMatrixCCW(matrix) {
|
_rotateMatrixCCW(matrix) {
|
||||||
const N = matrix.length;
|
// CCW Rotation: Transpose -> Reverse Cols?
|
||||||
const result = Array(N).fill().map(() => Array(N).fill(null));
|
// Or Reverse Rows -> Transpose?
|
||||||
for (let i = 0; i < N; i++) {
|
// 1 2 3 3 2 1 3 6 9
|
||||||
for (let j = 0; j < N; j++) {
|
// 4 5 6 -> 6 5 4 -> 2 5 8
|
||||||
result[N - 1 - j][i] = matrix[i][j];
|
// 7 8 9 9 8 7 1 4 7
|
||||||
}
|
// Reverse rows then transpose.
|
||||||
}
|
|
||||||
return result;
|
// Reverse rows first (manual)
|
||||||
|
const reversed = matrix.map(row => [...row].reverse());
|
||||||
|
// Then transpose using matrix-js
|
||||||
|
return Matrix(reversed).trans();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rotate a face (layer)
|
// Rotate a face (layer)
|
||||||
|
|||||||
Reference in New Issue
Block a user