@houstonp/rubiks-cube 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -54
- package/package.json +6 -6
- package/src/{cameraState.js → camera/cameraState.js} +3 -3
- package/src/core.js +387 -67
- package/src/cube/animationSlice.js +205 -0
- package/src/cube/animationState.js +96 -0
- package/src/cube/cubeSettings.js +6 -5
- package/src/cube/cubeState.js +284 -139
- package/src/cube/stickerState.js +188 -0
- package/src/debouncer.js +1 -1
- package/src/index.js +153 -28
- package/src/settings.js +20 -8
- package/src/three/centerPiece.js +44 -0
- package/src/three/cornerPiece.js +60 -0
- package/src/three/cube.js +492 -0
- package/src/three/edgePiece.js +50 -0
- package/src/three/sticker.js +37 -0
- package/tests/common.js +27 -0
- package/tests/cube.five.test.js +126 -0
- package/tests/cube.four.test.js +126 -0
- package/tests/cube.seven.test.js +126 -0
- package/tests/cube.six.test.js +126 -0
- package/tests/cube.three.test.js +151 -0
- package/tests/cube.two.test.js +125 -0
- package/tests/setup.js +36 -0
- package/types/{cameraState.d.ts → camera/cameraState.d.ts} +4 -4
- package/types/core.d.ts +396 -67
- package/types/cube/animationSlice.d.ts +26 -0
- package/types/cube/animationState.d.ts +41 -0
- package/types/cube/cubeSettings.d.ts +7 -7
- package/types/cube/cubeState.d.ts +38 -7
- package/types/cube/stickerState.d.ts +21 -0
- package/types/index.d.ts +23 -1
- package/types/settings.d.ts +8 -4
- package/types/three/centerPiece.d.ts +15 -0
- package/types/three/cornerPiece.d.ts +24 -0
- package/types/three/cube.d.ts +130 -0
- package/types/three/edgePiece.d.ts +16 -0
- package/types/three/sticker.d.ts +15 -0
- package/src/cube/cube.js +0 -324
- package/src/cube/cubeRotation.js +0 -79
- package/src/cube/slice.js +0 -143
- package/src/schema.js +0 -22
- package/src/threejs/materials.js +0 -54
- package/src/threejs/pieces.js +0 -100
- package/src/threejs/stickers.js +0 -40
- package/types/cube/cube.d.ts +0 -102
- package/types/cube/cubeRotation.d.ts +0 -33
- package/types/cube/slice.d.ts +0 -15
- package/types/schema.d.ts +0 -11
- package/types/threejs/materials.d.ts +0 -21
- package/types/threejs/pieces.d.ts +0 -28
- package/types/threejs/stickers.d.ts +0 -6
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { BoxGeometry, ExtrudeGeometry, Mesh, MeshBasicMaterial, Object3D } from 'three';
|
|
3
|
+
import { SVGLoader } from 'three/examples/jsm/Addons.js';
|
|
4
|
+
import { Sticker } from './sticker';
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ positon: import('three').Vector3Like, rotation: import('three').Vector3Like }} CenterPieceUserData */
|
|
7
|
+
|
|
8
|
+
export class CenterPiece extends Object3D {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
const boxGeom = new BoxGeometry(1, 1, 1);
|
|
12
|
+
const boxMesh = new Mesh(boxGeom, new MeshBasicMaterial({ color: 'black' }));
|
|
13
|
+
this.add(boxMesh);
|
|
14
|
+
/** @type {CenterPieceUserData} */
|
|
15
|
+
this.userData = { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } };
|
|
16
|
+
|
|
17
|
+
this.frontSticker = new CenterSticker();
|
|
18
|
+
this.frontSticker.position.set(0, 0, 0.5);
|
|
19
|
+
this.frontSticker.rotation.set(0, 0, 0);
|
|
20
|
+
this.add(this.frontSticker);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get stickers() {
|
|
24
|
+
return [this.frontSticker];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const loader = new SVGLoader();
|
|
29
|
+
const centerSVG = loader.parse(`
|
|
30
|
+
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
|
31
|
+
<path d="M 120 0 L 380 0 C 450 0 500 50 500 120 L 500 380 C 500 450 450 500 380 500 L 120 500 C 50 500 0 450 0 380 L 0 120 C 0 50 50 0 120 0 Z"></path>
|
|
32
|
+
</svg>
|
|
33
|
+
`);
|
|
34
|
+
const centerGeometry = new ExtrudeGeometry(SVGLoader.createShapes(centerSVG.paths[0])[0], {
|
|
35
|
+
depth: 15,
|
|
36
|
+
})
|
|
37
|
+
.scale(0.002, 0.002, 0.002)
|
|
38
|
+
.translate(-0.5, -0.5, 0);
|
|
39
|
+
|
|
40
|
+
export class CenterSticker extends Sticker {
|
|
41
|
+
constructor() {
|
|
42
|
+
super(centerGeometry);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/// @ts-check
|
|
2
|
+
import { BoxGeometry, ExtrudeGeometry, Material, Mesh, MeshBasicMaterial, Object3D } from 'three';
|
|
3
|
+
import { Sticker } from './sticker';
|
|
4
|
+
import { SVGLoader } from 'three/examples/jsm/Addons.js';
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ positon: import('three').Vector3Like, rotation: import('three').Vector3Like }} CornerPieceUserData */
|
|
7
|
+
/**
|
|
8
|
+
* @param {Material} frontMaterial
|
|
9
|
+
* @param {Material} rightMaterial
|
|
10
|
+
* @param {Material} topMaterial
|
|
11
|
+
* @param {Material} coreMaterial
|
|
12
|
+
* @returns {Group}
|
|
13
|
+
*/
|
|
14
|
+
export class CornerPiece extends Object3D {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
const boxGeom = new BoxGeometry(1, 1, 1);
|
|
18
|
+
const boxMesh = new Mesh(boxGeom, new MeshBasicMaterial({ color: 'black' }));
|
|
19
|
+
this.add(boxMesh);
|
|
20
|
+
/** @type {CornerPieceUserData} */
|
|
21
|
+
this.userData = { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } };
|
|
22
|
+
|
|
23
|
+
this.frontSticker = new CornerSticker();
|
|
24
|
+
this.frontSticker.position.set(0, 0, 0.5);
|
|
25
|
+
this.frontSticker.rotation.set(0, 0, 0);
|
|
26
|
+
this.add(this.frontSticker);
|
|
27
|
+
|
|
28
|
+
this.rightSticker = new CornerSticker();
|
|
29
|
+
this.rightSticker.position.set(0.5, 0, 0);
|
|
30
|
+
this.rightSticker.rotation.set(Math.PI / 2, Math.PI / 2, 0);
|
|
31
|
+
this.add(this.rightSticker);
|
|
32
|
+
|
|
33
|
+
this.topSticker = new CornerSticker();
|
|
34
|
+
this.topSticker.position.set(0, 0.5, 0);
|
|
35
|
+
this.topSticker.rotation.set(-Math.PI / 2, 0, -Math.PI / 2);
|
|
36
|
+
this.add(this.topSticker);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get stickers() {
|
|
40
|
+
return [this.frontSticker, this.rightSticker, this.topSticker];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const loader = new SVGLoader();
|
|
45
|
+
const cornerSVG = loader.parse(`
|
|
46
|
+
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
|
|
47
|
+
<path d="M 25 0 H 500 V 500 H 0 V 25 A 25 25 0 0 1 25 0 Z" bx:shape="rect 0 0 500 500 25 0 0 0 1@a864c1ee"/>
|
|
48
|
+
</svg>
|
|
49
|
+
`);
|
|
50
|
+
const cornerGeometry = new ExtrudeGeometry(SVGLoader.createShapes(cornerSVG.paths[0])[0], {
|
|
51
|
+
depth: 15,
|
|
52
|
+
})
|
|
53
|
+
.scale(0.002, 0.002, 0.002)
|
|
54
|
+
.translate(-0.5, -0.5, 0);
|
|
55
|
+
|
|
56
|
+
export class CornerSticker extends Sticker {
|
|
57
|
+
constructor() {
|
|
58
|
+
super(cornerGeometry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { Group, Mesh, MeshBasicMaterial, Object3D, SphereGeometry, Vector3 } from 'three';
|
|
3
|
+
import { CornerPiece } from './cornerPiece';
|
|
4
|
+
import CubeSettings from '../cube/cubeSettings';
|
|
5
|
+
import { ColorToFace, FaceColors, getCubeInfo } from '../cube/cubeState';
|
|
6
|
+
import { EdgePiece } from './edgePiece';
|
|
7
|
+
import { CenterPiece } from './centerPiece';
|
|
8
|
+
import { fromKociemba, getEmptyStickerState, toKociemba } from '../cube/stickerState';
|
|
9
|
+
import { AnimationStyles, CubeTypes } from '../core';
|
|
10
|
+
import { AnimationState, AnimationStatus } from '../cube/animationState';
|
|
11
|
+
import { Axi, GetLayerSlice, GetRotationSlice } from '../cube/animationSlice';
|
|
12
|
+
|
|
13
|
+
const ERROR_MARGIN = 0.0001;
|
|
14
|
+
|
|
15
|
+
export default class RubiksCube3D extends Object3D {
|
|
16
|
+
/**
|
|
17
|
+
* @public
|
|
18
|
+
* @param {CubeSettings} cubeSettings
|
|
19
|
+
*/
|
|
20
|
+
constructor(cubeSettings) {
|
|
21
|
+
super();
|
|
22
|
+
/** @type {CubeSettings} */
|
|
23
|
+
this._cubeSettings = cubeSettings ?? new CubeSettings(1.04, 150, 'fixed', CubeTypes.Three);
|
|
24
|
+
/** @type {number} */
|
|
25
|
+
this._pieceGap = cubeSettings.pieceGap;
|
|
26
|
+
/** @type {import('../core').CubeType} */
|
|
27
|
+
this._cubeType = cubeSettings.cubeType;
|
|
28
|
+
/** @type {import('../cube/cubeState').CubeInfo} */
|
|
29
|
+
this._cubeInfo = getCubeInfo(cubeSettings.cubeType);
|
|
30
|
+
/** @type {Group} */
|
|
31
|
+
this._mainGroup = this.createCubeGroup();
|
|
32
|
+
/** @type {Group} */
|
|
33
|
+
this._rotationGroup = new Group();
|
|
34
|
+
/** @type {AnimationState[]} */
|
|
35
|
+
this._rotationQueue = [];
|
|
36
|
+
/** @type {AnimationState | undefined} */
|
|
37
|
+
this._currentRotation = undefined;
|
|
38
|
+
/** @type {number | undefined} */
|
|
39
|
+
this._matchSpeed = undefined;
|
|
40
|
+
this.add(this._mainGroup, this._rotationGroup);
|
|
41
|
+
this.setStickerState(this._cubeInfo.initialStickerState);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates the main group containing all the pieces of the cube in their default position and rotation. Should only be called once during initialization.
|
|
46
|
+
* @private
|
|
47
|
+
**/
|
|
48
|
+
createCubeGroup() {
|
|
49
|
+
const cubeInfo = this._cubeInfo;
|
|
50
|
+
const pieceGap = this._pieceGap;
|
|
51
|
+
const outerLayerMultiplier = cubeInfo.outerLayerMultiplier;
|
|
52
|
+
const outerLayerOffset = (cubeInfo.pieceSize * (outerLayerMultiplier - 1)) / 2;
|
|
53
|
+
const group = new Group();
|
|
54
|
+
const core = new Mesh(new SphereGeometry(cubeInfo.coreSize), new MeshBasicMaterial({ color: 'black' }));
|
|
55
|
+
group.add(core);
|
|
56
|
+
for (const piece of cubeInfo.corners) {
|
|
57
|
+
const corner = new CornerPiece();
|
|
58
|
+
corner.scale.set(cubeInfo.pieceSize * outerLayerMultiplier, cubeInfo.pieceSize * outerLayerMultiplier, cubeInfo.pieceSize * outerLayerMultiplier);
|
|
59
|
+
corner.position.set(
|
|
60
|
+
piece.position.x * (pieceGap + outerLayerOffset),
|
|
61
|
+
piece.position.y * (pieceGap + outerLayerOffset),
|
|
62
|
+
piece.position.z * (pieceGap + outerLayerOffset),
|
|
63
|
+
);
|
|
64
|
+
corner.rotation.set(piece.rotation.x, piece.rotation.y, piece.rotation.z);
|
|
65
|
+
corner.userData = {
|
|
66
|
+
position: Object.assign({}, piece.position),
|
|
67
|
+
rotation: Object.assign({}, piece.rotation),
|
|
68
|
+
};
|
|
69
|
+
group.add(corner);
|
|
70
|
+
}
|
|
71
|
+
for (const piece of cubeInfo.edges) {
|
|
72
|
+
const edge = new EdgePiece();
|
|
73
|
+
edge.scale.set(cubeInfo.pieceSize, cubeInfo.pieceSize * outerLayerMultiplier, cubeInfo.pieceSize * outerLayerMultiplier);
|
|
74
|
+
edge.position.set(
|
|
75
|
+
piece.position.x * (pieceGap + (Math.abs(piece.position.x) == 1 ? outerLayerOffset : 0)),
|
|
76
|
+
piece.position.y * (pieceGap + (Math.abs(piece.position.y) == 1 ? outerLayerOffset : 0)),
|
|
77
|
+
piece.position.z * (pieceGap + (Math.abs(piece.position.z) == 1 ? outerLayerOffset : 0)),
|
|
78
|
+
);
|
|
79
|
+
edge.rotation.set(piece.rotation.x, piece.rotation.y, piece.rotation.z);
|
|
80
|
+
edge.userData = {
|
|
81
|
+
position: Object.assign({}, piece.position),
|
|
82
|
+
rotation: Object.assign({}, piece.rotation),
|
|
83
|
+
};
|
|
84
|
+
group.add(edge);
|
|
85
|
+
}
|
|
86
|
+
for (const piece of cubeInfo.centers) {
|
|
87
|
+
const center = new CenterPiece();
|
|
88
|
+
center.scale.set(cubeInfo.pieceSize, cubeInfo.pieceSize, cubeInfo.pieceSize * outerLayerMultiplier);
|
|
89
|
+
center.position.set(
|
|
90
|
+
piece.position.x * (pieceGap + (Math.abs(piece.position.x) == 1 ? outerLayerOffset : 0)),
|
|
91
|
+
piece.position.y * (pieceGap + (Math.abs(piece.position.y) == 1 ? outerLayerOffset : 0)),
|
|
92
|
+
piece.position.z * (pieceGap + (Math.abs(piece.position.z) == 1 ? outerLayerOffset : 0)),
|
|
93
|
+
);
|
|
94
|
+
center.rotation.set(piece.rotation.x, piece.rotation.y, piece.rotation.z);
|
|
95
|
+
center.userData = {
|
|
96
|
+
position: Object.assign({}, piece.position),
|
|
97
|
+
rotation: Object.assign({}, piece.rotation),
|
|
98
|
+
};
|
|
99
|
+
group.add(center);
|
|
100
|
+
}
|
|
101
|
+
return group;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns the sticker state of the cube. Can only be called when an Animation is not in progress as not all pieces would be in the main group.
|
|
106
|
+
* @private
|
|
107
|
+
* @returns {import('../cube/stickerState').StickerState}
|
|
108
|
+
*/
|
|
109
|
+
getStickerState() {
|
|
110
|
+
let state = getEmptyStickerState(this._cubeInfo.cubeType);
|
|
111
|
+
const corners = this._mainGroup.children.filter((x) => x instanceof CornerPiece);
|
|
112
|
+
const edges = this._mainGroup.children.filter((x) => x instanceof EdgePiece);
|
|
113
|
+
const centers = this._mainGroup.children.filter((x) => x instanceof CenterPiece);
|
|
114
|
+
[...corners, ...edges, ...centers].forEach((piece) => {
|
|
115
|
+
piece.stickers.forEach((sticker) => {
|
|
116
|
+
const face = ColorToFace(sticker.color);
|
|
117
|
+
const piecepos = new Vector3();
|
|
118
|
+
piecepos.copy(piece.userData.position);
|
|
119
|
+
const stickerpos = new Vector3();
|
|
120
|
+
sticker.getWorldPosition(stickerpos);
|
|
121
|
+
stickerpos.sub(piecepos);
|
|
122
|
+
stickerpos.normalize();
|
|
123
|
+
stickerpos.round();
|
|
124
|
+
if (stickerpos.x === 1) {
|
|
125
|
+
const i = Math.round((1 - piecepos.y) / this._cubeInfo.pieceSize);
|
|
126
|
+
const j = Math.round((1 - piecepos.z) / this._cubeInfo.pieceSize);
|
|
127
|
+
state.right[i][j] = face;
|
|
128
|
+
} else if (stickerpos.x === -1) {
|
|
129
|
+
const i = Math.round((1 - piecepos.y) / this._cubeInfo.pieceSize);
|
|
130
|
+
const j = Math.round((1 + piecepos.z) / this._cubeInfo.pieceSize);
|
|
131
|
+
state.left[i][j] = face;
|
|
132
|
+
} else if (stickerpos.y === 1) {
|
|
133
|
+
const i = Math.round((1 + piecepos.z) / this._cubeInfo.pieceSize);
|
|
134
|
+
const j = Math.round((1 + piecepos.x) / this._cubeInfo.pieceSize);
|
|
135
|
+
state.up[i][j] = face;
|
|
136
|
+
} else if (stickerpos.y === -1) {
|
|
137
|
+
const i = Math.round((1 - piecepos.z) / this._cubeInfo.pieceSize);
|
|
138
|
+
const j = Math.round((1 + piecepos.x) / this._cubeInfo.pieceSize);
|
|
139
|
+
state.down[i][j] = face;
|
|
140
|
+
} else if (stickerpos.z === 1) {
|
|
141
|
+
const i = Math.round((1 - piecepos.y) / this._cubeInfo.pieceSize);
|
|
142
|
+
const j = Math.round((1 + piecepos.x) / this._cubeInfo.pieceSize);
|
|
143
|
+
state.front[i][j] = face;
|
|
144
|
+
} else if (stickerpos.z === -1) {
|
|
145
|
+
const i = Math.round((1 - piecepos.y) / this._cubeInfo.pieceSize);
|
|
146
|
+
const j = Math.round((1 - piecepos.x) / this._cubeInfo.pieceSize);
|
|
147
|
+
state.back[i][j] = face;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
return state;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Sets the sticker state of the cube. Can only be called when an Animation is not in progress as not all pieces would be in the main group.
|
|
156
|
+
* @private
|
|
157
|
+
* @param {import('../cube/stickerState').StickerState} stickerState
|
|
158
|
+
*/
|
|
159
|
+
setStickerState(stickerState) {
|
|
160
|
+
const corners = this._mainGroup.children.filter((x) => x instanceof CornerPiece);
|
|
161
|
+
const edges = this._mainGroup.children.filter((x) => x instanceof EdgePiece);
|
|
162
|
+
const centers = this._mainGroup.children.filter((x) => x instanceof CenterPiece);
|
|
163
|
+
[...corners, ...edges, ...centers].forEach((piece) => {
|
|
164
|
+
piece.stickers.forEach((sticker) => {
|
|
165
|
+
const piecepos = new Vector3();
|
|
166
|
+
piecepos.copy(piece.userData.position);
|
|
167
|
+
const stickerpos = new Vector3();
|
|
168
|
+
sticker.getWorldPosition(stickerpos);
|
|
169
|
+
stickerpos.sub(piecepos);
|
|
170
|
+
stickerpos.normalize();
|
|
171
|
+
stickerpos.round();
|
|
172
|
+
if (stickerpos.x === 1) {
|
|
173
|
+
const i = Math.round((1 - piecepos.y) / this._cubeInfo.pieceSize);
|
|
174
|
+
const j = Math.round((1 - piecepos.z) / this._cubeInfo.pieceSize);
|
|
175
|
+
const face = stickerState.right[i][j];
|
|
176
|
+
sticker.color = FaceColors[face];
|
|
177
|
+
} else if (stickerpos.x === -1) {
|
|
178
|
+
const i = Math.round((1 - piecepos.y) / this._cubeInfo.pieceSize);
|
|
179
|
+
const j = Math.round((1 + piecepos.z) / this._cubeInfo.pieceSize);
|
|
180
|
+
const face = stickerState.left[i][j];
|
|
181
|
+
sticker.color = FaceColors[face];
|
|
182
|
+
} else if (stickerpos.y === 1) {
|
|
183
|
+
const i = Math.round((1 + piecepos.z) / this._cubeInfo.pieceSize);
|
|
184
|
+
const j = Math.round((1 + piecepos.x) / this._cubeInfo.pieceSize);
|
|
185
|
+
const face = stickerState.up[i][j];
|
|
186
|
+
sticker.color = FaceColors[face];
|
|
187
|
+
} else if (stickerpos.y === -1) {
|
|
188
|
+
const i = Math.round((1 - piecepos.z) / this._cubeInfo.pieceSize);
|
|
189
|
+
const j = Math.round((1 + piecepos.x) / this._cubeInfo.pieceSize);
|
|
190
|
+
const face = stickerState.down[i][j];
|
|
191
|
+
sticker.color = FaceColors[face];
|
|
192
|
+
} else if (stickerpos.z === 1) {
|
|
193
|
+
const i = Math.round((1 - piecepos.y) / this._cubeInfo.pieceSize);
|
|
194
|
+
const j = Math.round((1 + piecepos.x) / this._cubeInfo.pieceSize);
|
|
195
|
+
const face = stickerState.front[i][j];
|
|
196
|
+
sticker.color = FaceColors[face];
|
|
197
|
+
} else if (stickerpos.z === -1) {
|
|
198
|
+
const i = Math.round((1 - piecepos.y) / this._cubeInfo.pieceSize);
|
|
199
|
+
const j = Math.round((1 - piecepos.x) / this._cubeInfo.pieceSize);
|
|
200
|
+
const face = stickerState.back[i][j];
|
|
201
|
+
sticker.color = FaceColors[face];
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns the pieces that should be rotated for a given slice. If the slice has no layers, all pieces will be returned. Should only be called before an Animation is started.
|
|
209
|
+
* @private
|
|
210
|
+
* @param {import('../cube/animationSlice').Slice} slice
|
|
211
|
+
* @returns {Object3D[]}
|
|
212
|
+
*/
|
|
213
|
+
getRotationLayer(slice) {
|
|
214
|
+
const corners = this._mainGroup.children.filter((x) => x instanceof CornerPiece);
|
|
215
|
+
const edges = this._mainGroup.children.filter((x) => x instanceof EdgePiece);
|
|
216
|
+
const centers = this._mainGroup.children.filter((x) => x instanceof CenterPiece);
|
|
217
|
+
return [...corners, ...edges, ...centers].filter((piece) => {
|
|
218
|
+
switch (slice.axis) {
|
|
219
|
+
case Axi.x:
|
|
220
|
+
return slice.layers.some((layer) => Math.abs(layer - piece.userData.position.x) < ERROR_MARGIN);
|
|
221
|
+
case Axi.y:
|
|
222
|
+
return slice.layers.some((layer) => Math.abs(layer - piece.userData.position.y) < ERROR_MARGIN);
|
|
223
|
+
case Axi.z:
|
|
224
|
+
return slice.layers.some((layer) => Math.abs(layer - piece.userData.position.z) < ERROR_MARGIN);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Updates the gap of the pieces. To be used when the cube is not rotating
|
|
231
|
+
* @private
|
|
232
|
+
* @param {number} pieceGap
|
|
233
|
+
* @returns {void}
|
|
234
|
+
*/
|
|
235
|
+
updateGap(pieceGap) {
|
|
236
|
+
this._pieceGap = pieceGap;
|
|
237
|
+
const outerLayerMultiplier = this._cubeInfo.outerLayerMultiplier;
|
|
238
|
+
const outerLayerOffset = (this._cubeInfo.pieceSize * (outerLayerMultiplier - 1)) / 2;
|
|
239
|
+
const corners = this._mainGroup.children.filter((x) => x instanceof CornerPiece);
|
|
240
|
+
const edges = this._mainGroup.children.filter((x) => x instanceof EdgePiece);
|
|
241
|
+
const centers = this._mainGroup.children.filter((x) => x instanceof CenterPiece);
|
|
242
|
+
[...corners, ...edges, ...centers].forEach((piece) => {
|
|
243
|
+
let xOuterLayer = Math.abs(Math.abs(piece.userData.position.x) - 1) < ERROR_MARGIN;
|
|
244
|
+
let yOuterLayer = Math.abs(Math.abs(piece.userData.position.y) - 1) < ERROR_MARGIN;
|
|
245
|
+
let zOuterLayer = Math.abs(Math.abs(piece.userData.position.z) - 1) < ERROR_MARGIN;
|
|
246
|
+
piece.position.set(
|
|
247
|
+
piece.userData.position.x * (pieceGap + (xOuterLayer ? outerLayerOffset : 0)),
|
|
248
|
+
piece.userData.position.y * (pieceGap + (yOuterLayer ? outerLayerOffset : 0)),
|
|
249
|
+
piece.userData.position.z * (pieceGap + (zOuterLayer ? outerLayerOffset : 0)),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @private
|
|
256
|
+
* @param {import('../core').CubeType} cubeType
|
|
257
|
+
*/
|
|
258
|
+
updateCubeType(cubeType) {
|
|
259
|
+
this._cubeType = cubeType;
|
|
260
|
+
this._cubeInfo = getCubeInfo(cubeType);
|
|
261
|
+
this.remove(this._mainGroup);
|
|
262
|
+
this._mainGroup = this.createCubeGroup();
|
|
263
|
+
this.add(this._mainGroup);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* finish current rotation and clear rotation queue
|
|
268
|
+
* @private
|
|
269
|
+
*/
|
|
270
|
+
stop() {
|
|
271
|
+
this._rotationQueue.forEach((cubeRotation) => cubeRotation.failedCallback('Movement Interrupted.'));
|
|
272
|
+
this._rotationQueue = [];
|
|
273
|
+
if (this._currentRotation) {
|
|
274
|
+
const percentage = this._currentRotation.update(0);
|
|
275
|
+
this.rotateGroupByPercent(this._currentRotation, percentage);
|
|
276
|
+
if (this._currentRotation.status !== AnimationStatus.Complete) {
|
|
277
|
+
throw new Error('Failed to complete current rotation during stop');
|
|
278
|
+
}
|
|
279
|
+
this.clearRotationGroup();
|
|
280
|
+
const state = toKociemba(this.getStickerState());
|
|
281
|
+
this._currentRotation.complete(state);
|
|
282
|
+
this._currentRotation = undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Adds pieces in the rotationGroup back into the main group.
|
|
288
|
+
* Updates the position and rotation of the pieces according to their world position and rotation, then resets the rotation of the rotation group.
|
|
289
|
+
* Should only be called when a rotation is in progress.
|
|
290
|
+
* @private
|
|
291
|
+
* @returns {void}
|
|
292
|
+
*/
|
|
293
|
+
clearRotationGroup() {
|
|
294
|
+
const cubeInfo = this._cubeInfo;
|
|
295
|
+
const pieceGap = this._pieceGap;
|
|
296
|
+
const outerLayerMultiplier = cubeInfo.outerLayerMultiplier;
|
|
297
|
+
const outerLayerOffset = (cubeInfo.pieceSize * (outerLayerMultiplier - 1)) / 2;
|
|
298
|
+
const corners = this._rotationGroup.children.filter((x) => x instanceof CornerPiece);
|
|
299
|
+
const edges = this._rotationGroup.children.filter((x) => x instanceof EdgePiece);
|
|
300
|
+
const centers = this._rotationGroup.children.filter((x) => x instanceof CenterPiece);
|
|
301
|
+
[...centers, ...corners, ...edges].forEach((piece) => {
|
|
302
|
+
piece.getWorldPosition(piece.position);
|
|
303
|
+
piece.getWorldQuaternion(piece.quaternion);
|
|
304
|
+
if (cubeInfo.layers.some((layer) => Math.abs(layer - piece.position.x / pieceGap) < ERROR_MARGIN)) {
|
|
305
|
+
piece.userData.position.x = piece.position.x / pieceGap;
|
|
306
|
+
} else {
|
|
307
|
+
piece.userData.position.x = piece.position.x / (pieceGap + outerLayerOffset);
|
|
308
|
+
}
|
|
309
|
+
if (cubeInfo.layers.some((layer) => Math.abs(layer - piece.position.y / pieceGap) < ERROR_MARGIN)) {
|
|
310
|
+
piece.userData.position.y = piece.position.y / pieceGap;
|
|
311
|
+
} else {
|
|
312
|
+
piece.userData.position.y = piece.position.y / (pieceGap + outerLayerOffset);
|
|
313
|
+
}
|
|
314
|
+
if (cubeInfo.layers.some((layer) => Math.abs(layer - piece.position.z / pieceGap) < ERROR_MARGIN)) {
|
|
315
|
+
piece.userData.position.z = piece.position.z / pieceGap;
|
|
316
|
+
} else {
|
|
317
|
+
piece.userData.position.z = piece.position.z / (pieceGap + outerLayerOffset);
|
|
318
|
+
}
|
|
319
|
+
piece.userData.rotation.x = piece.rotation.x;
|
|
320
|
+
piece.userData.rotation.y = piece.rotation.y;
|
|
321
|
+
piece.userData.rotation.z = piece.rotation.z;
|
|
322
|
+
});
|
|
323
|
+
this._mainGroup.add(...this._rotationGroup.children);
|
|
324
|
+
this._rotationGroup.rotation.set(0, 0, 0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Rotates the pieces in the rotation group according to the percentage of completion of the current animation. Should only be called when a rotation is in progress.
|
|
329
|
+
* @private
|
|
330
|
+
* @param {number} percentage
|
|
331
|
+
* @param {AnimationState} animationState
|
|
332
|
+
*/
|
|
333
|
+
rotateGroupByPercent(animationState, percentage) {
|
|
334
|
+
const radians = (Math.abs(animationState.slice.direction) * ((percentage / 100) * Math.PI)) / 2;
|
|
335
|
+
this._rotationGroup.rotateOnWorldAxis(
|
|
336
|
+
new Vector3(
|
|
337
|
+
animationState.slice.axis === Axi.x ? animationState.slice.direction : 0,
|
|
338
|
+
animationState.slice.axis === Axi.y ? animationState.slice.direction : 0,
|
|
339
|
+
animationState.slice.axis === Axi.z ? animationState.slice.direction : 0,
|
|
340
|
+
).normalize(),
|
|
341
|
+
radians,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
*
|
|
347
|
+
* calculates the current speed of the current rotation in ms.
|
|
348
|
+
* calculation is dependent on animation style and animation speed settings
|
|
349
|
+
* - exponential: speeds up rotations depending on the queue length
|
|
350
|
+
* - next: an animation speed of 0 when there is another animation in the queue
|
|
351
|
+
* - match: will match the speed of rotations to the frequency of key presses.
|
|
352
|
+
* - fixed: will return a constant value
|
|
353
|
+
* @private
|
|
354
|
+
* @returns {number}
|
|
355
|
+
*/
|
|
356
|
+
getRotationSpeed() {
|
|
357
|
+
if (this._cubeSettings.animationStyle === AnimationStyles.Exponential) {
|
|
358
|
+
return this._cubeSettings.animationSpeedMs / 2 ** this._rotationQueue.length;
|
|
359
|
+
}
|
|
360
|
+
if (this._cubeSettings.animationStyle === AnimationStyles.Next) {
|
|
361
|
+
return this._rotationQueue.length > 0 ? 0 : this._cubeSettings.animationSpeedMs;
|
|
362
|
+
}
|
|
363
|
+
if (this._cubeSettings.animationStyle === AnimationStyles.Match) {
|
|
364
|
+
if (this._rotationQueue.length > 0) {
|
|
365
|
+
const gaps = this._rotationQueue.map((state, index) => {
|
|
366
|
+
if (index == 0 && this._currentRotation != null) {
|
|
367
|
+
return state.timestampMs - this._currentRotation.timestampMs;
|
|
368
|
+
}
|
|
369
|
+
if (index == 0) {
|
|
370
|
+
return this._matchSpeed ?? this._cubeSettings.animationSpeedMs;
|
|
371
|
+
}
|
|
372
|
+
return state.timestampMs - this._rotationQueue[index - 1].timestampMs;
|
|
373
|
+
});
|
|
374
|
+
this._matchSpeed = Math.min(...gaps);
|
|
375
|
+
}
|
|
376
|
+
if (this._matchSpeed !== undefined) {
|
|
377
|
+
return this._matchSpeed;
|
|
378
|
+
}
|
|
379
|
+
return this._cubeSettings.animationSpeedMs;
|
|
380
|
+
}
|
|
381
|
+
if (this._cubeSettings.animationStyle === AnimationStyles.Fixed) {
|
|
382
|
+
return this._cubeSettings.animationSpeedMs;
|
|
383
|
+
}
|
|
384
|
+
return this._cubeSettings.animationSpeedMs;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Update the cube and continue any rotations. If a rotation is in progress, it will be updated. If no rotation is in progress, the next rotation in the queue will be started.
|
|
389
|
+
* @public
|
|
390
|
+
*/
|
|
391
|
+
update() {
|
|
392
|
+
if (this._currentRotation === undefined) {
|
|
393
|
+
if (this._pieceGap !== this._cubeSettings.pieceGap) {
|
|
394
|
+
this.updateGap(this._cubeSettings.pieceGap);
|
|
395
|
+
}
|
|
396
|
+
if (this._cubeType !== this._cubeSettings.cubeType) {
|
|
397
|
+
this.updateCubeType(this._cubeSettings.cubeType);
|
|
398
|
+
}
|
|
399
|
+
this._currentRotation = this._rotationQueue.shift();
|
|
400
|
+
if (this._currentRotation === undefined) {
|
|
401
|
+
this._matchSpeed = undefined; // reset speed for the match animation options
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (this._currentRotation.slice.layers.length === 0) {
|
|
405
|
+
console.error('current rotation has no layers. ');
|
|
406
|
+
this._currentRotation.complete(toKociemba(this.getStickerState()));
|
|
407
|
+
this._currentRotation = undefined;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (this._currentRotation.status === AnimationStatus.Pending) {
|
|
412
|
+
this._rotationGroup.add(...this.getRotationLayer(this._currentRotation.slice));
|
|
413
|
+
this._currentRotation.initialise();
|
|
414
|
+
}
|
|
415
|
+
if (this._currentRotation.status === AnimationStatus.Initialised || this._currentRotation.status === AnimationStatus.InProgress) {
|
|
416
|
+
var speed = this.getRotationSpeed();
|
|
417
|
+
const percentage = this._currentRotation.update(speed);
|
|
418
|
+
this.rotateGroupByPercent(this._currentRotation, percentage);
|
|
419
|
+
}
|
|
420
|
+
if (this._currentRotation.status === AnimationStatus.Complete) {
|
|
421
|
+
this.clearRotationGroup();
|
|
422
|
+
this._currentRotation.complete(toKociemba(this.getStickerState()));
|
|
423
|
+
this._currentRotation = undefined;
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* resets the cube to the default state and clears any queued rotations. If a rotation is in progress, it will be completed instantly before the reset.
|
|
430
|
+
* @public
|
|
431
|
+
* @param {(state: string) => boolean} completedCallback
|
|
432
|
+
*/
|
|
433
|
+
reset(completedCallback) {
|
|
434
|
+
this.stop();
|
|
435
|
+
this.setStickerState(this._cubeInfo.initialStickerState);
|
|
436
|
+
if (!completedCallback(toKociemba(this.getStickerState()))) {
|
|
437
|
+
console.error('Failed to invoke reset completedCallback');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* sets the state of the cube
|
|
443
|
+
* @public
|
|
444
|
+
* @param {string} state
|
|
445
|
+
* @param {(state: string) => boolean} completedCallback
|
|
446
|
+
* @param {(reason: string) => boolean} failedCallback
|
|
447
|
+
*/
|
|
448
|
+
setState(state, completedCallback, failedCallback) {
|
|
449
|
+
const stickerState = fromKociemba(state, this._cubeType);
|
|
450
|
+
if (stickerState == null) {
|
|
451
|
+
if (!failedCallback('Invalid Kociemba State')) {
|
|
452
|
+
console.error('Failed to invoke setState failedCallback');
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.stop();
|
|
457
|
+
this.setStickerState(stickerState);
|
|
458
|
+
if (!completedCallback(toKociemba(this.getStickerState()))) {
|
|
459
|
+
console.error('Failed to invoke setState completedCallback');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* @public
|
|
465
|
+
* @param {import('../core').Rotation} rotation
|
|
466
|
+
* @param {((state: string) => void )} completedCallback
|
|
467
|
+
* @param {((reason: string) => void )} failedCallback
|
|
468
|
+
*/
|
|
469
|
+
rotate(rotation, completedCallback, failedCallback) {
|
|
470
|
+
const slice = GetRotationSlice(rotation, this._cubeInfo.cubeType);
|
|
471
|
+
if (slice == null) {
|
|
472
|
+
failedCallback('Invalid Rotation');
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
this._rotationQueue.push(new AnimationState(slice, completedCallback, failedCallback));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @public
|
|
480
|
+
* @param {import('../core').Movement} movement
|
|
481
|
+
* @param {((state: string) => void )} completedCallback
|
|
482
|
+
* @param {((reason: string) => void )} failedCallback
|
|
483
|
+
*/
|
|
484
|
+
movement(movement, completedCallback, failedCallback) {
|
|
485
|
+
const slice = GetLayerSlice(movement, this._cubeInfo.cubeType);
|
|
486
|
+
if (slice == null) {
|
|
487
|
+
failedCallback('Invalid Movement');
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
this._rotationQueue.push(new AnimationState(slice, completedCallback, failedCallback));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { BoxGeometry, ExtrudeGeometry, Mesh, MeshBasicMaterial, Object3D } from 'three';
|
|
3
|
+
import { SVGLoader } from 'three/examples/jsm/Addons.js';
|
|
4
|
+
import { Sticker } from './sticker';
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ positon: import('three').Vector3Like, rotation: import('three').Vector3Like }} EdgePieceUserData*/
|
|
7
|
+
|
|
8
|
+
export class EdgePiece extends Object3D {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
const boxGeom = new BoxGeometry(1, 1, 1);
|
|
12
|
+
const boxMesh = new Mesh(boxGeom, new MeshBasicMaterial({ color: 'black' }));
|
|
13
|
+
this.add(boxMesh);
|
|
14
|
+
|
|
15
|
+
/** @type {EdgePieceUserData} */
|
|
16
|
+
this.userData = { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } };
|
|
17
|
+
|
|
18
|
+
this.frontSticker = new EdgeSticker();
|
|
19
|
+
this.frontSticker.position.set(0, 0, 0.5);
|
|
20
|
+
this.frontSticker.rotation.set(0, 0, 0);
|
|
21
|
+
this.add(this.frontSticker);
|
|
22
|
+
|
|
23
|
+
this.topSticker = new EdgeSticker();
|
|
24
|
+
this.topSticker.position.set(0, 0.5, 0);
|
|
25
|
+
this.topSticker.rotation.set(-Math.PI / 2, 0, Math.PI);
|
|
26
|
+
this.add(this.topSticker);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get stickers() {
|
|
30
|
+
return [this.frontSticker, this.topSticker];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const loader = new SVGLoader();
|
|
35
|
+
const edgeSVG = loader.parse(`
|
|
36
|
+
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
|
37
|
+
<path d="M 150 0 L 350 0 C 450 0 500 50 500 120 L 500 500 L 0 500 L 0 120 C 0 50 50 0 150 0 Z"></path>
|
|
38
|
+
</svg>
|
|
39
|
+
`);
|
|
40
|
+
const edgeGeometry = new ExtrudeGeometry(SVGLoader.createShapes(edgeSVG.paths[0])[0], {
|
|
41
|
+
depth: 15,
|
|
42
|
+
})
|
|
43
|
+
.scale(0.002, 0.002, 0.002)
|
|
44
|
+
.translate(-0.5, -0.5, 0);
|
|
45
|
+
|
|
46
|
+
export class EdgeSticker extends Sticker {
|
|
47
|
+
constructor() {
|
|
48
|
+
super(edgeGeometry);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { Mesh, MeshStandardMaterial } from 'three';
|
|
3
|
+
|
|
4
|
+
export class Sticker extends Mesh {
|
|
5
|
+
/**
|
|
6
|
+
* @param {import("three").BufferGeometry} geometry
|
|
7
|
+
*/
|
|
8
|
+
constructor(geometry) {
|
|
9
|
+
super(
|
|
10
|
+
geometry,
|
|
11
|
+
new MeshStandardMaterial({
|
|
12
|
+
color: 'white',
|
|
13
|
+
metalness: 0,
|
|
14
|
+
roughness: 0.4,
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
/** @type {{ color: import('three').ColorRepresentation }} */
|
|
18
|
+
this.userData = { color: 'white' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {import('three').ColorRepresentation} color
|
|
23
|
+
*/
|
|
24
|
+
set color(color) {
|
|
25
|
+
const material = /** @type {MeshStandardMaterial} */ (this.material);
|
|
26
|
+
material.color.set(color);
|
|
27
|
+
this.userData.color = color;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns {import('three').ColorRepresentation} color
|
|
32
|
+
*/
|
|
33
|
+
get color() {
|
|
34
|
+
const material = /** @type {MeshStandardMaterial} */ (this.material);
|
|
35
|
+
return this.userData.color;
|
|
36
|
+
}
|
|
37
|
+
}
|