import { Group, Mesh, MeshStandardMaterial, Vector3 } from 'three'; import { Actions, Color2Index, FacingDirection, RotationStep } from './consts'; export class RotationController { private static instance: RotationController; private cubeGroup?: Group; private cubes: Array; private cubeSpeed: number; private rotationSteps: Array; private rotatingStep: RotationStep | null; private rotatingGroup: Group; constructor() { this.cubes = []; this.cubeSpeed = 2; this.rotationSteps = []; this.rotatingStep = null; this.rotatingGroup = new Group(); } private rotate(step: RotationStep, group: Group, delta: number) { let sign = 0; let axis: 'x' | 'y' | 'z' = 'x'; switch (step.faceDirection) { case 'front': sign = step.direction === 'clockwise' ? -1 : 1; axis = 'z'; break; case 'back': sign = step.direction === 'clockwise' ? 1 : -1; axis = 'z'; break; case 'left': sign = step.direction === 'clockwise' ? 1 : -1; axis = 'x'; break; case 'right': sign = step.direction === 'clockwise' ? -1 : 1; axis = 'x'; break; case 'up': sign = step.direction === 'clockwise' ? -1 : 1; axis = 'y'; break; case 'down': sign = step.direction === 'clockwise' ? 1 : -1; axis = 'y'; break; } group.rotation[axis] += sign * delta * this.cubeSpeed; if (Math.abs(group.rotation[axis]) > Math.PI / 2) { group.rotation[axis] = (Math.PI / 2) * sign; return true; } return false; } private getCubeFaceData(mesh: Mesh, faceDirection: FacingDirection) { const faces = mesh.children.filter((child) => child.userData.isFace); let axis: 'x' | 'y' | 'z' = 'x'; switch (faceDirection) { case 'front': case 'back': axis = 'z'; break; case 'right': case 'left': axis = 'x'; break; case 'up': case 'down': axis = 'y'; break; } let maxFace: Mesh | null = null; let maxValue = -Infinity; for (const face of faces) { const worldPosition = new Vector3(); face.getWorldPosition(worldPosition); const axisValue = Math.abs(worldPosition[axis]); if (axisValue > maxValue) { maxValue = axisValue; maxFace = face as Mesh; } } if (!maxFace) throw new Error('maxFace is null'); // this should never happen const worldPosition = new Vector3(); maxFace.getWorldPosition(worldPosition); const axis2 = ['x', 'y', 'z'].filter((x) => x !== axis) as Array<'x' | 'y' | 'z'>; const rank = worldPosition[axis2[0]] * 100 + worldPosition[axis2[1]] * 10; return { face: maxFace, worldPosition, rank, }; } static getInstance() { if (!RotationController.instance) { RotationController.instance = new RotationController(); if (typeof window !== 'undefined') { // @ts-expect-error window is defined window.rotationController = RotationController.instance; } } return RotationController.instance; } stopRotation(cb: () => void) { this.rotationSteps = []; const cancel = setInterval(() => { if (!this.rotatingStep) { clearInterval(cancel); cb(); } }, 50); } setCubeGroup(cubeGroup: Group) { this.cubeGroup = cubeGroup; } addCube(cube: Mesh) { if (!this.cubes.includes(cube)) { this.cubes.push(cube); } } getCubes(faceDirection: FacingDirection) { switch (faceDirection) { case 'front': return this.cubes.filter((m) => m.position.z > 0); case 'back': return this.cubes.filter((m) => m.position.z < 0); case 'right': return this.cubes.filter((m) => m.position.x > 0); case 'left': return this.cubes.filter((m) => m.position.x < 0); case 'up': return this.cubes.filter((m) => m.position.y > 0); case 'down': return this.cubes.filter((m) => m.position.y < 0); } } initializeFaces() { ['front', 'back', 'right', 'left', 'up', 'down'].forEach((f) => { const faceDirection = f as FacingDirection; const cubes = this.getCubes(faceDirection); const indices = cubes.map((cube) => this.getCubeFaceData(cube, faceDirection)).sort((a, b) => a.rank - b.rank); indices.forEach((i, index) => { i.face.userData.name = `${faceDirection[0].toUpperCase()}${index}`; }); }); } setState(stateArray: Array>) { const colors = ['', '', '', '', '', '']; Object.entries(Color2Index).forEach(([c, i]) => { colors[i] = c; }); ['front', 'back', 'right', 'left', 'up', 'down'].forEach((f, idx1) => { const faceDirection = f as FacingDirection; const cubes = this.getCubes(faceDirection); const indices = cubes.map((cube) => this.getCubeFaceData(cube, faceDirection)).sort((a, b) => a.rank - b.rank); indices.forEach((i, idx2) => { const colorIdx = stateArray[idx1][idx2]; i.face.userData.faceColor = colors[colorIdx]; i.face.userData.faceColorIndex = colorIdx; const material = i.face.material as MeshStandardMaterial; material.color.set(colors[colorIdx]); }); }); this.initializeFaces(); } getState() { const status = ['front', 'back', 'right', 'left', 'up', 'down'].map((f) => { const faceDirection = f as FacingDirection; const cubes = this.getCubes(faceDirection); const indices = cubes.map((cube) => this.getCubeFaceData(cube, faceDirection)).sort((a, b) => a.rank - b.rank); return indices.map((i) => i.face.userData.faceColorIndex); }); return status; } _printStateTransitions() { const rotationsPy: Array = []; ['front', 'back', 'right', 'left', 'up', 'down'].forEach((f) => { const faceDirection = f as FacingDirection; const cubes = this.getCubes(faceDirection); const indices = cubes.map((cube) => this.getCubeFaceData(cube, faceDirection)).sort((a, b) => a.rank - b.rank); const positionNames = indices.map((i) => i.face.userData.name); for (let i = 0; i < positionNames.length; i++) { const positionName = positionNames[i]; if (positionName[0] !== f[0].toUpperCase() || positionName[1] !== i.toString()) { rotationsPy.push( `new_state[${f[0].toUpperCase()}, ${i}] = self.state[${positionName[0]}, ${positionName[1]}]`, ); } } }); console.log(rotationsPy.join('\n')); } setCubeSpeed(cubeSpeed: number) { this.cubeSpeed = cubeSpeed; } addRotationStepCode(...codes: Array) { this.addRotationStep(...codes.map((code) => Actions[code])); } addRotationStep(...step: Array) { this.rotationSteps.push(...step); } frameCallback(state: unknown, delta: number) { if (!this.cubeGroup) return; if (this.rotationSteps.length === 0 && !this.rotatingStep) return; if (!this.rotatingStep) { const step = this.rotationSteps.shift(); if (!step) return; this.rotatingStep = step; const cubes = this.getCubes(step.faceDirection); this.rotatingGroup = new Group(); this.cubeGroup?.add(this.rotatingGroup); cubes.forEach((cube) => this.rotatingGroup.attach(cube)); } const done = this.rotate(this.rotatingStep, this.rotatingGroup, delta); if (done) { this.rotatingStep = null; const children = [...this.rotatingGroup.children]; children.forEach((child) => this.cubeGroup?.attach(child)); this.cubeGroup?.remove(this.rotatingGroup); } } } export const rotationController = RotationController.getInstance(); export const frameCallback = rotationController.frameCallback.bind(rotationController);