rlcube / src /components /rotation-controller.ts
imwithye's picture
show python action
0b3d91e
raw
history blame
6.47 kB
import { Group, Mesh, Vector3 } from 'three';
import { FacingDirection, RotationStep } from './consts';
export class RotationController {
private static instance: RotationController;
private cubeGroup?: Group;
private cubes: Array<Mesh>;
private cubeSpeed: number;
private rotationSteps: Array<RotationStep>;
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<string> = [];
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<RotationStep>) {
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);