import { Group, Mesh, Vector3 } from 'three'; import { 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 'top': sign = step.direction === 'clockwise' ? -1 : 1; axis = 'y'; break; case 'bottom': 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 'top': case 'bottom': 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(); } 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 'top': return this.cubes.filter((m) => m.position.y > 0); case 'bottom': return this.cubes.filter((m) => m.position.y < 0); } } initializeFaces() { ['front', 'back', 'right', 'left', 'top', 'bottom'].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}`; }); }); } getStatus() { const rotationsPy: Array = []; const status = ['front', 'back', 'right', 'left', 'top', 'bottom'].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); 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]}]`, ); } } return indices.map((i) => i.face.userData.faceColorIndex); }); console.log('Python Gym Step Code:'); console.log(rotationsPy.join('\n')); return status; } setCubeSpeed(cubeSpeed: number) { this.cubeSpeed = cubeSpeed; } 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);