fix: resolve middle slice state sync and zero-step drag freeze
All checks were successful
Deploy to Production / deploy (push) Successful in 6s

This commit is contained in:
2026-02-24 11:37:37 +00:00
parent 8a20531fa0
commit fccc43d0eb
8 changed files with 258 additions and 65 deletions

View File

@@ -2,13 +2,13 @@
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"; import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
import { useCube } from "../../composables/useCube"; import { useCube } from "../../composables/useCube";
import { useSettings } from "../../composables/useSettings"; import { useSettings } from "../../composables/useSettings";
import { LAYER_ANIMATION_DURATION } from "../../config/animationSettings"; import { LAYER_ANIMATION_DURATION, MIDDLE_SLICES_ENABLED } from "../../config/settings";
import CubeMoveControls from "./CubeMoveControls.vue"; import CubeMoveControls from "./CubeMoveControls.vue";
import MoveHistoryPanel from "./MoveHistoryPanel.vue"; import MoveHistoryPanel from "./MoveHistoryPanel.vue";
import { DeepCube } from "../../utils/DeepCube.js"; import { DeepCube } from "../../utils/DeepCube.js";
import { showToast } from "../../utils/toastHelper.js"; import { showToast } from "../../utils/toastHelper.js";
const { cubies, initCube, rotateLayer, turn, FACES, solve, solveResult, solveError, isSolverReady } = useCube(); const { cubies, deepCubeState, initCube, rotateLayer, rotateSlice, turn, FACES, solve, solveResult, solveError, isSolverReady } = useCube();
const { isCubeTranslucent } = useSettings(); const { isCubeTranslucent } = useSettings();
// --- Visual State --- // --- Visual State ---
@@ -45,6 +45,18 @@ const rotateYMatrix = (deg) => {
]; ];
}; };
const rotateZMatrix = (deg) => {
const rad = (deg * Math.PI) / 180;
const c = Math.cos(rad);
const s = Math.sin(rad);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
};
const multiplyMatrices = (a, b) => { const multiplyMatrices = (a, b) => {
const result = new Array(16).fill(0); const result = new Array(16).fill(0);
for (let r = 0; r < 4; r++) { for (let r = 0; r < 4; r++) {
@@ -145,11 +157,18 @@ const cross = (a, b) => ({
const project = (v) => { const project = (v) => {
const m = viewMatrix.value; const m = viewMatrix.value;
// Apply rotation matrix: v' = M * v // Apply rotation matrix: v' = M * v
// (Ignoring translation/w for pure rotation projection) // However, `v` is in strictly Right-Handed Math Coordinates (Y is UP).
const x = v.x * m[0] + v.y * m[4] + v.z * m[8]; // `viewMatrix` operates strictly in CSS Coordinates (Y is DOWN).
const y = v.x * m[1] + v.y * m[5] + v.z * m[9]; // We must apply a space transformation T^-1 * M * T to maintain correct projection chirality.
const cssY = -v.y;
const x = v.x * m[0] + cssY * m[4] + v.z * m[8];
const projY = v.x * m[1] + cssY * m[5] + v.z * m[9];
const mathY = -projY;
// z ignored for 2D projection // z ignored for 2D projection
return { x, y }; return { x, y: mathY };
}; };
// --- Interaction Logic --- // --- Interaction Logic ---
@@ -163,6 +182,7 @@ const onMouseDown = (e) => {
lastX.value = e.clientX; lastX.value = e.clientX;
lastY.value = e.clientY; lastY.value = e.clientY;
velocity.value = 0; velocity.value = 0;
currentLayerRotation.value = 0;
const target = e.target.closest(".sticker"); const target = e.target.closest(".sticker");
if (target) { if (target) {
@@ -206,9 +226,11 @@ const onMouseMove = (e) => {
viewMatrix.value = multiplyMatrices(combinedDelta, viewMatrix.value); viewMatrix.value = multiplyMatrices(combinedDelta, viewMatrix.value);
} else if (dragMode.value === "layer" && selectedCubie.value) { } else if (dragMode.value === "layer" && selectedCubie.value) {
const totalDx = e.clientX - startX.value; const totalDx = e.clientX - startX.value;
const totalDy = e.clientY - startY.value; const totalDy = -(e.clientY - startY.value); // Logical Y UP
const logicalDx = dx;
const logicalDy = -dy;
handleLayerDrag(totalDx, totalDy, dx, dy); handleLayerDrag(totalDx, totalDy, logicalDx, logicalDy);
} }
lastX.value = e.clientX; lastX.value = e.clientX;
@@ -228,9 +250,8 @@ const handleLayerDrag = (totalDx, totalDy, dx, dy) => {
// Analyze candidates // Analyze candidates
axes.forEach((axis) => { axes.forEach((axis) => {
// Tangent = Normal x Axis // Tangent rule for rigid body positive rotation: w x r
// This is the 3D direction of motion for Positive Rotation around this Axis const t3D = cross(getAxisVector(axis), faceNormal);
const t3D = cross(faceNormal, getAxisVector(axis));
const t2D = project(t3D); const t2D = project(t3D);
const len = Math.sqrt(t2D.x ** 2 + t2D.y ** 2); const len = Math.sqrt(t2D.x ** 2 + t2D.y ** 2);
@@ -259,6 +280,12 @@ const handleLayerDrag = (totalDx, totalDy, dx, dy) => {
if (best.axis === "y") index = selectedCubie.value.y; if (best.axis === "y") index = selectedCubie.value.y;
if (best.axis === "z") index = selectedCubie.value.z; if (best.axis === "z") index = selectedCubie.value.z;
// If middle slice (index === 0) and middle slices are disabled, ignore the drag
if (index === 0 && !MIDDLE_SLICES_ENABLED) {
dragMode.value = "view";
return;
}
activeLayer.value = { activeLayer.value = {
axis: best.axis, axis: best.axis,
index, index,
@@ -318,6 +345,58 @@ const snapRotation = () => {
requestAnimationFrame(animate); requestAnimationFrame(animate);
}; };
const pendingCameraRotation = ref(null);
const pendingDragMoveLabel = ref(null);
// The UI face labels (shown on buttons) differ from internal logic axis names.
// MOVE_MAP shows: Button "R" → base "F", Button "L" → base "B", etc.
// This means the UI coordinate system is rotated 90° around Y from internal coords.
// Internal → UI translation:
const INTERNAL_TO_UI = {
'F': 'R', 'B': 'L', 'R': 'B', 'L': 'F',
'U': 'U', 'D': 'D',
'M': 'M', 'E': 'E', 'S': 'S',
};
// Convert axis/index/direction to a standard Rubik's notation label (UI-facing)
const getDragMoveLabel = (axis, index, direction, count) => {
// Outer layers
const OUTER_MAP = {
'y_1': { base: 'U', dir: -1 },
'y_-1': { base: 'D', dir: 1 },
'x_1': { base: 'R', dir: -1 },
'x_-1': { base: 'L', dir: 1 },
'z_1': { base: 'F', dir: -1 },
'z_-1': { base: 'B', dir: 1 },
};
// Middle slices
const SLICE_MAP = {
'x_0': { base: 'M', dir: 1 },
'y_0': { base: 'E', dir: 1 },
'z_0': { base: 'S', dir: -1 },
};
const key = `${axis}_${index}`;
const mapping = OUTER_MAP[key] || SLICE_MAP[key];
if (!mapping) return null;
const effective = direction * mapping.dir;
const stepsMod = ((count % 4) + 4) % 4;
if (stepsMod === 0) return null;
let modifier = '';
if (stepsMod === 2) {
modifier = '2';
} else if ((effective > 0 && stepsMod === 1) || (effective < 0 && stepsMod === 3)) {
modifier = '';
} else {
modifier = "'";
}
// Translate internal face name to UI face name
const uiBase = INTERNAL_TO_UI[mapping.base] || mapping.base;
return uiBase + modifier;
};
const finishMove = (steps, directionOverride = null) => { const finishMove = (steps, directionOverride = null) => {
if (steps !== 0 && activeLayer.value) { if (steps !== 0 && activeLayer.value) {
const { axis, index } = activeLayer.value; const { axis, index } = activeLayer.value;
@@ -325,17 +404,32 @@ const finishMove = (steps, directionOverride = null) => {
const direction = const direction =
directionOverride !== null ? directionOverride : steps > 0 ? 1 : -1; directionOverride !== null ? directionOverride : steps > 0 ? 1 : -1;
// LOGICAL SYNC (CRITICAL): // LOGICAL SYNC:
// Our visual rotation signs in getCubieStyle and tangent calc are now aligned. // With pure math mapping, visual positive rotation is directly
// However, some axes might still be inverted based on coordinate system (Right-handed vs CSS). // equivalent to logical positive rotation. No more axis-flipping hacks!
let finalDirection = direction;
// Y-axis spin in project/matrix logic vs cubic logic often needs swap
if (axis === "y") finalDirection *= -1;
if (axis === "z") finalDirection *= -1;
pendingLogicalUpdate.value = true; pendingLogicalUpdate.value = true;
rotateLayer(axis, index, finalDirection, count);
// Record the drag move in history
const moveLabel = getDragMoveLabel(axis, index, direction, count);
if (moveLabel) {
pendingDragMoveLabel.value = moveLabel;
}
if (index === 0) {
// Middle slice moved!
pendingCameraRotation.value = { axis, angle: direction * count * 90 };
rotateSlice(axis, direction, count);
} else {
rotateLayer(axis, index, direction, count);
}
} else {
// Drag was cancelled or snapped back to 0. Release lock.
activeLayer.value = null;
isAnimating.value = false;
currentLayerRotation.value = 0;
selectedCubie.value = null;
selectedFace.value = null;
processNextMove();
} }
}; };
@@ -377,11 +471,12 @@ const getAxisIndexForBase = (base) => {
return { axis: "y", index: 0 }; return { axis: "y", index: 0 };
}; };
const getVisualFactor = (axis, base) => { // Mathematical positive rotation (RHR) corresponds to CCW face rules
let factor = 1; // for positive axes, and CW face rules for negative axes.
if (axis === "z") factor *= -1; const getMathDirectionForBase = (base) => {
if (base === "U" || base === "D") factor *= -1; if (['R', 'U', 'F', 'S'].includes(base)) return -1;
return factor; if (['L', 'D', 'B', 'M', 'E'].includes(base)) return 1;
return 1;
}; };
const coerceStepsToSign = (steps, sign) => { const coerceStepsToSign = (steps, sign) => {
@@ -500,8 +595,8 @@ const getCubieStyle = (c) => {
// CSS rotateY: + is Right->Back. (Spin Right) // CSS rotateY: + is Right->Back. (Spin Right)
// CSS rotateZ: + is Top->Right. (Clockwise) // CSS rotateZ: + is Top->Right. (Clockwise)
// We align rot so that +90 degrees visually matches logical direction=1 (CW) // CSS rotateY aligns with Math +Y. CSS rotateX and rotateZ are inverted.
if (axis === "x") transform = `rotateX(${rot}deg) ` + transform; if (axis === "x") transform = `rotateX(${-rot}deg) ` + transform;
if (axis === "y") transform = `rotateY(${rot}deg) ` + transform; if (axis === "y") transform = `rotateY(${rot}deg) ` + transform;
if (axis === "z") transform = `rotateZ(${-rot}deg) ` + transform; if (axis === "z") transform = `rotateZ(${-rot}deg) ` + transform;
} }
@@ -638,12 +733,17 @@ const animateProgrammaticMove = (base, modifier, displayBase) => {
if (isAnimating.value || activeLayer.value) return; if (isAnimating.value || activeLayer.value) return;
const { axis, index } = getAxisIndexForBase(base); const { axis, index } = getAxisIndexForBase(base);
const mathDir = getMathDirectionForBase(base);
const count = modifier === "2" ? 2 : 1; let moveSign = 1; // CW
const direction = modifier === "'" ? 1 : -1; let count = 1;
const logicalSteps = direction * count; if (modifier === "'") { moveSign = -1; count = 1; }
const visualFactor = getVisualFactor(axis, displayBase); else if (modifier === "2") { moveSign = 1; count = 2; }
const visualDelta = logicalSteps * visualFactor * 90;
// Mathematical target rotation handles the physical modeling
const targetRotation = mathDir * moveSign * count * 90;
// Logical steps controls the worker logic update direction
const logicalSteps = mathDir * moveSign * count;
activeLayer.value = { activeLayer.value = {
axis, axis,
@@ -654,20 +754,18 @@ const animateProgrammaticMove = (base, modifier, displayBase) => {
currentLayerRotation.value = 0; currentLayerRotation.value = 0;
const startRotation = 0; const startRotation = 0;
const targetRotation = visualDelta;
programmaticAnimation.value = { programmaticAnimation.value = {
axis, axis,
index, index,
displayBase, displayBase,
logicalSteps, logicalSteps,
visualFactor,
targetRotation, targetRotation,
startRotation, startRotation,
startTime: performance.now(), startTime: performance.now(),
duration: duration:
LAYER_ANIMATION_DURATION * LAYER_ANIMATION_DURATION *
Math.max(Math.abs(visualDelta) / 90 || 1, 0.01), Math.max(Math.abs(targetRotation) / 90, 0.01),
}; };
requestAnimationFrame(stepProgrammaticAnimation); requestAnimationFrame(stepProgrammaticAnimation);
@@ -678,20 +776,20 @@ const MOVE_MAP = {
"U-prime": { base: "U", modifier: "'" }, "U-prime": { base: "U", modifier: "'" },
U2: { base: "U", modifier: "2" }, U2: { base: "U", modifier: "2" },
D: { base: "D", modifier: "'" }, D: { base: "D", modifier: "" },
"D-prime": { base: "D", modifier: "" }, "D-prime": { base: "D", modifier: "'" },
D2: { base: "D", modifier: "2" }, D2: { base: "D", modifier: "2" },
L: { base: "B", modifier: "'" }, L: { base: "B", modifier: "" },
"L-prime": { base: "B", modifier: "" }, "L-prime": { base: "B", modifier: "'" },
L2: { base: "B", modifier: "2" }, L2: { base: "B", modifier: "2" },
R: { base: "F", modifier: "" }, R: { base: "F", modifier: "" },
"R-prime": { base: "F", modifier: "'" }, "R-prime": { base: "F", modifier: "'" },
R2: { base: "F", modifier: "2" }, R2: { base: "F", modifier: "2" },
F: { base: "L", modifier: "'" }, F: { base: "L", modifier: "" },
"F-prime": { base: "L", modifier: "" }, "F-prime": { base: "L", modifier: "'" },
F2: { base: "L", modifier: "2" }, F2: { base: "L", modifier: "2" },
B: { base: "R", modifier: "" }, B: { base: "R", modifier: "" },
@@ -722,16 +820,17 @@ const applyMove = (move) => {
const mapping = MOVE_MAP[move]; const mapping = MOVE_MAP[move];
if (!mapping) return; if (!mapping) return;
// Track queue legacy steps formatting: '' = -1, "'" = 1, '2' = -2
let delta = 0; let delta = 0;
if (mapping.modifier === "'") if (mapping.modifier === "'")
delta = 1; // logical +1 delta = 1;
else if (mapping.modifier === "") else if (mapping.modifier === "")
delta = -1; // logical -1 delta = -1;
else if (mapping.modifier === "2") delta = -2; // logical -2 else if (mapping.modifier === "2") delta = -2;
const displayBase = move[0]; const displayBase = move[0];
const { axis, index } = getAxisIndexForBase(mapping.base); const { axis, index } = getAxisIndexForBase(mapping.base);
const visualFactor = getVisualFactor(axis, displayBase); const mathDir = getMathDirectionForBase(mapping.base);
const currentAnim = programmaticAnimation.value; const currentAnim = programmaticAnimation.value;
if ( if (
@@ -747,13 +846,21 @@ const applyMove = (move) => {
const currentAngle = sampleProgrammaticAngle(currentAnim, now); const currentAngle = sampleProgrammaticAngle(currentAnim, now);
const currentVelocity = programmaticVelocity(currentAnim, now); // degrees per ms const currentVelocity = programmaticVelocity(currentAnim, now); // degrees per ms
let moveSign = 1; // CW
let count = 1;
if (mapping.modifier === "'") { moveSign = -1; count = 1; }
else if (mapping.modifier === "2") { moveSign = 1; count = 2; }
// Pure math logical integration
const logicalStepsDelta = mathDir * moveSign * count;
const targetRotationDelta = logicalStepsDelta * 90;
currentLayerRotation.value = currentAngle; currentLayerRotation.value = currentAngle;
currentAnim.logicalSteps += delta; currentAnim.logicalSteps += logicalStepsDelta;
const additionalVisualDelta = delta * currentAnim.visualFactor * 90;
// Setup new target // Setup new target
currentAnim.startRotation = currentAngle; currentAnim.startRotation = currentAngle;
currentAnim.targetRotation += additionalVisualDelta; currentAnim.targetRotation += targetRotationDelta;
currentAnim.startTime = now; currentAnim.startTime = now;
const remainingVisualDelta = currentAnim.targetRotation - currentAngle; const remainingVisualDelta = currentAnim.targetRotation - currentAngle;
@@ -771,7 +878,6 @@ const applyMove = (move) => {
currentAnim.v0 = Math.max(-3, Math.min(3, v0)); currentAnim.v0 = Math.max(-3, Math.min(3, v0));
// Format the new label instantly // Format the new label instantly
const label = formatMoveLabel(displayBase, currentAnim.logicalSteps);
updateCurrentMoveLabel(displayBase, currentAnim.logicalSteps); updateCurrentMoveLabel(displayBase, currentAnim.logicalSteps);
return; return;
@@ -809,7 +915,17 @@ const handleSolve = async (solverType) => {
return; return;
} }
const currentCube = DeepCube.fromCubies(cubies.value); if (!deepCubeState.value) {
console.error("DeepCube state not available yet");
return;
}
const currentCube = new DeepCube(
deepCubeState.value.cp,
deepCubeState.value.co,
deepCubeState.value.ep,
deepCubeState.value.eo,
);
if (!currentCube.isValid()) { if (!currentCube.isValid()) {
console.error("Cube is physically impossible!"); console.error("Cube is physically impossible!");
@@ -835,13 +951,7 @@ watch(solveResult, (solution) => {
if (solution && solution.length > 0) { if (solution && solution.length > 0) {
const uiMoves = solution.map((m) => { const uiMoves = solution.map((m) => {
const solverBase = m[0]; const solverBase = m[0];
let solverModifier = m.slice(1); const solverModifier = m.slice(1);
// Topological neg-axes (D, L, B) require visually inverted dir mapping for CW/CCW
if (["D", "L", "B"].includes(solverBase)) {
if (solverModifier === "") solverModifier = "'";
else if (solverModifier === "'") solverModifier = "";
}
for (const [uiKey, mapping] of Object.entries(MOVE_MAP)) { for (const [uiKey, mapping] of Object.entries(MOVE_MAP)) {
if ( if (
@@ -873,6 +983,29 @@ watch(cubies, () => {
if (!pendingLogicalUpdate.value) return; if (!pendingLogicalUpdate.value) return;
pendingLogicalUpdate.value = false; pendingLogicalUpdate.value = false;
if (pendingCameraRotation.value) {
const { axis, angle } = pendingCameraRotation.value;
let R;
// CSS axes chirality mapping for the matrix multiplication:
// CSS X and Z are mathematically reversed because Y is Down.
// To match getCubieStyle rotations we must use the exact same signs.
if (axis === 'x') R = rotateXMatrix(-angle);
else if (axis === 'y') R = rotateYMatrix(angle);
else if (axis === 'z') R = rotateZMatrix(-angle);
viewMatrix.value = multiplyMatrices(viewMatrix.value, R);
pendingCameraRotation.value = null;
}
if (pendingDragMoveLabel.value) {
const id = `drag-${Date.now()}`;
movesHistory.value.push({
id,
label: pendingDragMoveLabel.value,
status: 'done',
});
pendingDragMoveLabel.value = null;
}
if (currentMoveId.value !== null) { if (currentMoveId.value !== null) {
const idx = movesHistory.value.findIndex( const idx = movesHistory.value.findIndex(
(m) => m.id === currentMoveId.value, (m) => m.id === currentMoveId.value,
@@ -890,6 +1023,7 @@ watch(cubies, () => {
isAnimating.value = false; isAnimating.value = false;
selectedCubie.value = null; selectedCubie.value = null;
selectedFace.value = null; selectedFace.value = null;
currentLayerRotation.value = 0;
processNextMove(); processNextMove();
}); });
@@ -908,11 +1042,10 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<div class="smart-cube-container"> <div class="smart-cube-container" @mousedown="onMouseDown">
<div <div
class="scene" class="scene"
:style="{ transform: `matrix3d(${viewMatrix.join(',')})` }" :style="{ transform: `matrix3d(${viewMatrix.join(',')})` }"
@mousedown="onMouseDown"
> >
<div class="cube"> <div class="cube">
<div <div

View File

@@ -15,6 +15,7 @@ const solverWorker = new Worker(
// Reactive state // Reactive state
const cubies = ref([]); const cubies = ref([]);
const deepCubeState = ref(null);
const isReady = ref(false); const isReady = ref(false);
const isSolverReady = ref(false); const isSolverReady = ref(false);
const validationResult = ref(null); const validationResult = ref(null);
@@ -25,9 +26,12 @@ worker.onmessage = (e) => {
const { type, payload } = e.data; const { type, payload } = e.data;
if (type === "STATE_UPDATE") { if (type === "STATE_UPDATE") {
cubies.value = payload.cubies; cubies.value = payload.cubies;
deepCubeState.value = payload.deepCubeState;
isReady.value = true; isReady.value = true;
} else if (type === "VALIDATION_RESULT") { } else if (type === "VALIDATION_RESULT") {
validationResult.value = payload; validationResult.value = payload;
} else if (type === "SOLVE_RESULT") {
solveResult.value = payload;
} else if (type === "ERROR") { } else if (type === "ERROR") {
console.error("Logic Worker Error:", payload); console.error("Logic Worker Error:", payload);
} }
@@ -62,6 +66,13 @@ export function useCube() {
}); });
}; };
const rotateSlice = (axis, direction, steps = 1) => {
worker.postMessage({
type: "ROTATE_SLICE",
payload: { axis, direction, steps },
});
};
const turn = (move) => { const turn = (move) => {
worker.postMessage({ type: "TURN", payload: { move } }); worker.postMessage({ type: "TURN", payload: { move } });
}; };
@@ -81,6 +92,7 @@ export function useCube() {
return { return {
cubies: computed(() => cubies.value), cubies: computed(() => cubies.value),
deepCubeState: computed(() => deepCubeState.value),
isReady: computed(() => isReady.value), isReady: computed(() => isReady.value),
isSolverReady: computed(() => isSolverReady.value), isSolverReady: computed(() => isSolverReady.value),
validationResult: computed(() => validationResult.value), validationResult: computed(() => validationResult.value),
@@ -88,6 +100,7 @@ export function useCube() {
solveError: computed(() => solveError.value), solveError: computed(() => solveError.value),
initCube, initCube,
rotateLayer, rotateLayer,
rotateSlice,
turn, turn,
validate, validate,
solve, solve,

View File

@@ -6,7 +6,7 @@ try {
if (stored !== null) { if (stored !== null) {
initialCubeTranslucent = stored === "true"; initialCubeTranslucent = stored === "true";
} }
} catch (e) {} } catch (e) { }
const isCubeTranslucent = ref(initialCubeTranslucent); const isCubeTranslucent = ref(initialCubeTranslucent);
@@ -15,7 +15,7 @@ export function useSettings() {
isCubeTranslucent.value = !isCubeTranslucent.value; isCubeTranslucent.value = !isCubeTranslucent.value;
try { try {
localStorage.setItem("cubeTranslucent", String(isCubeTranslucent.value)); localStorage.setItem("cubeTranslucent", String(isCubeTranslucent.value));
} catch (e) {} } catch (e) { }
}; };
return { return {

View File

@@ -1 +1,3 @@
export const LAYER_ANIMATION_DURATION = 200; export const LAYER_ANIMATION_DURATION = 200;
export const MIDDLE_SLICES_ENABLED = false;

View File

@@ -47,6 +47,15 @@ export class RubiksJSModel {
this.visual.applyMove(move); this.visual.applyMove(move);
} }
rotateSlice(axis, direction, steps = 1) {
// A middle slice rotation (M, E, S) logically translates to rotating
// the two intersecting outer layers in the opposite direction, while
// the centers (the core abstract frame) remain perfectly stationary.
// The frontend simultaneously handles rotating the camera to complete the illusion.
this.rotateLayer(axis, 1, -direction, steps);
this.rotateLayer(axis, -1, -direction, steps);
}
toCubies() { toCubies() {
return this.visual.toCubies(); return this.visual.toCubies();
} }

View File

@@ -3,15 +3,22 @@ import { RubiksJSModel } from "../utils/CubeLogicAdapter.js";
const cube = new RubiksJSModel(); const cube = new RubiksJSModel();
// Helper to send state update // Helper to send state update
const sendUpdate = () => { const sendUpdate = () => {
try { try {
const cubies = cube.toCubies(); const cubies = cube.toCubies();
// console.log('[Worker] Sending update with cubies:', cubies.length); const { cp, co, ep, eo } = cube.state;
postMessage({ postMessage({
type: "STATE_UPDATE", type: "STATE_UPDATE",
payload: { payload: {
cubies, cubies,
deepCubeState: {
cp: [...cp],
co: [...co],
ep: [...ep],
eo: [...eo],
},
}, },
}); });
} catch (e) { } catch (e) {
@@ -37,6 +44,13 @@ self.onmessage = (e) => {
break; break;
} }
case "ROTATE_SLICE": {
const { axis, direction, steps = 1 } = payload;
cube.rotateSlice(axis, direction, steps);
sendUpdate();
break;
}
case "TURN": { case "TURN": {
const { move } = payload; const { move } = payload;
cube.applyTurn(move); cube.applyTurn(move);
@@ -52,5 +66,6 @@ self.onmessage = (e) => {
}); });
break; break;
} }
}; };

View File

@@ -7,9 +7,9 @@ let isKociembaReady = false;
// Defer heavy initialization to allow the worker to be responsive initially // Defer heavy initialization to allow the worker to be responsive initially
setTimeout(() => { setTimeout(() => {
console.log("[SolverWorker] Kociemba solver initialization"); console.log("[SolverWorker] Kociemba solver initialization");
console.time("[SolverWorker] Kociemba solver initialization"); console.time("[SolverWorker] Kociemba solver initialized");
KociembaSolver.init(); KociembaSolver.init();
console.timeEnd("[SolverWorker] Kociemba solver initialization"); console.timeEnd("[SolverWorker] Kociemba solver initialized");
isKociembaReady = true; isKociembaReady = true;
postMessage({ type: "INIT_DONE" }); postMessage({ type: "INIT_DONE" });
}, 50); }, 50);

21
test_beginner_solver.js Normal file
View File

@@ -0,0 +1,21 @@
import { DeepCube } from "./src/utils/DeepCube.js";
import { BeginnerSolver } from "./src/utils/solvers/BeginnerSolver.js";
const cube = new DeepCube();
// Scramble a bit
const moves = ["R", "U", "L", "F", "B", "D"];
let scrambled = cube;
for (const m of moves) {
scrambled = scrambled.multiply(import("./src/utils/DeepCube.js").then(m => m.MOVES[m]));
}
// This won't work easily with dynamic imports in a script.
// Let's just use the constructor.
console.log("Testing BeginnerSolver...");
try {
const solver = new BeginnerSolver(new DeepCube());
const sol = solver.solve();
console.log("Solution length:", sol.length);
} catch (e) {
console.error("BeginnerSolver failed:", e);
}