@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.
Files changed (53) hide show
  1. package/README.md +258 -54
  2. package/package.json +6 -6
  3. package/src/{cameraState.js → camera/cameraState.js} +3 -3
  4. package/src/core.js +387 -67
  5. package/src/cube/animationSlice.js +205 -0
  6. package/src/cube/animationState.js +96 -0
  7. package/src/cube/cubeSettings.js +6 -5
  8. package/src/cube/cubeState.js +284 -139
  9. package/src/cube/stickerState.js +188 -0
  10. package/src/debouncer.js +1 -1
  11. package/src/index.js +153 -28
  12. package/src/settings.js +20 -8
  13. package/src/three/centerPiece.js +44 -0
  14. package/src/three/cornerPiece.js +60 -0
  15. package/src/three/cube.js +492 -0
  16. package/src/three/edgePiece.js +50 -0
  17. package/src/three/sticker.js +37 -0
  18. package/tests/common.js +27 -0
  19. package/tests/cube.five.test.js +126 -0
  20. package/tests/cube.four.test.js +126 -0
  21. package/tests/cube.seven.test.js +126 -0
  22. package/tests/cube.six.test.js +126 -0
  23. package/tests/cube.three.test.js +151 -0
  24. package/tests/cube.two.test.js +125 -0
  25. package/tests/setup.js +36 -0
  26. package/types/{cameraState.d.ts → camera/cameraState.d.ts} +4 -4
  27. package/types/core.d.ts +396 -67
  28. package/types/cube/animationSlice.d.ts +26 -0
  29. package/types/cube/animationState.d.ts +41 -0
  30. package/types/cube/cubeSettings.d.ts +7 -7
  31. package/types/cube/cubeState.d.ts +38 -7
  32. package/types/cube/stickerState.d.ts +21 -0
  33. package/types/index.d.ts +23 -1
  34. package/types/settings.d.ts +8 -4
  35. package/types/three/centerPiece.d.ts +15 -0
  36. package/types/three/cornerPiece.d.ts +24 -0
  37. package/types/three/cube.d.ts +130 -0
  38. package/types/three/edgePiece.d.ts +16 -0
  39. package/types/three/sticker.d.ts +15 -0
  40. package/src/cube/cube.js +0 -324
  41. package/src/cube/cubeRotation.js +0 -79
  42. package/src/cube/slice.js +0 -143
  43. package/src/schema.js +0 -22
  44. package/src/threejs/materials.js +0 -54
  45. package/src/threejs/pieces.js +0 -100
  46. package/src/threejs/stickers.js +0 -40
  47. package/types/cube/cube.d.ts +0 -102
  48. package/types/cube/cubeRotation.d.ts +0 -33
  49. package/types/cube/slice.d.ts +0 -15
  50. package/types/schema.d.ts +0 -11
  51. package/types/threejs/materials.d.ts +0 -21
  52. package/types/threejs/pieces.d.ts +0 -28
  53. 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
+ }