@houstonp/rubiks-cube 2.1.0 → 3.0.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 (76) hide show
  1. package/README.md +304 -77
  2. package/package.json +18 -8
  3. package/src/{core.js → core/index.js} +72 -41
  4. package/src/rubiksCube/index.js +3 -0
  5. package/src/rubiksCube/rubiksCubeController.js +111 -0
  6. package/src/{three → rubiksCube3D}/centerPiece.js +37 -2
  7. package/src/{three → rubiksCube3D}/cornerPiece.js +56 -2
  8. package/src/rubiksCube3D/cubeConfig.js +87 -0
  9. package/src/rubiksCube3D/cubeSettings.js +30 -0
  10. package/src/{three → rubiksCube3D}/edgePiece.js +2 -1
  11. package/src/rubiksCube3D/index.js +3 -0
  12. package/src/rubiksCube3D/rubiksCube3D.js +383 -0
  13. package/src/{three → rubiksCube3D}/sticker.js +5 -4
  14. package/src/state/index.js +4 -0
  15. package/src/state/rubiksCubeState.js +471 -0
  16. package/src/state/slice.js +236 -0
  17. package/src/state/stickerState.js +185 -0
  18. package/src/{camera → webComponent}/cameraState.js +17 -25
  19. package/src/webComponent/constants.js +67 -0
  20. package/src/webComponent/index.js +7 -0
  21. package/src/webComponent/rubiksCubeElement.js +379 -0
  22. package/src/{settings.js → webComponent/settings.js} +36 -23
  23. package/tests/common.js +3 -20
  24. package/tests/core.test.js +56 -0
  25. package/tests/rubiksCube.solves.test.js +41 -0
  26. package/tests/rubiksCube3D.solves.test.js +185 -0
  27. package/tests/rubiksCubeState.solves.test.js +35 -0
  28. package/tests/testScrambles.js +194 -0
  29. package/types/{core.d.ts → core/index.d.ts} +45 -48
  30. package/types/rubiksCube/index.d.ts +3 -0
  31. package/types/rubiksCube/rubiksCubeController.d.ts +62 -0
  32. package/types/rubiksCube3D/centerPiece.d.ts +27 -0
  33. package/types/rubiksCube3D/cornerPiece.d.ts +38 -0
  34. package/types/rubiksCube3D/cubeConfig.d.ts +32 -0
  35. package/types/rubiksCube3D/cubeSettings.d.ts +33 -0
  36. package/types/{three → rubiksCube3D}/edgePiece.d.ts +5 -3
  37. package/types/rubiksCube3D/index.d.ts +3 -0
  38. package/types/rubiksCube3D/rubiksCube3D.d.ts +120 -0
  39. package/types/rubiksCube3D/sticker.d.ts +18 -0
  40. package/types/state/index.d.ts +5 -0
  41. package/types/state/rubiksCubeState.d.ts +108 -0
  42. package/types/state/slice.d.ts +46 -0
  43. package/types/state/stickerState.d.ts +34 -0
  44. package/types/webComponent/cameraState.d.ts +22 -0
  45. package/types/webComponent/constants.d.ts +57 -0
  46. package/types/webComponent/index.d.ts +6 -0
  47. package/types/webComponent/rubiksCubeElement.d.ts +89 -0
  48. package/types/{settings.d.ts → webComponent/settings.d.ts} +5 -8
  49. package/src/cube/animationSlice.js +0 -205
  50. package/src/cube/animationState.js +0 -96
  51. package/src/cube/cubeSettings.js +0 -19
  52. package/src/cube/cubeState.js +0 -337
  53. package/src/cube/stickerState.js +0 -188
  54. package/src/index.js +0 -621
  55. package/src/three/cube.js +0 -492
  56. package/tests/cube.five.test.js +0 -126
  57. package/tests/cube.four.test.js +0 -126
  58. package/tests/cube.seven.test.js +0 -126
  59. package/tests/cube.six.test.js +0 -126
  60. package/tests/cube.three.test.js +0 -151
  61. package/tests/cube.two.test.js +0 -125
  62. package/types/camera/cameraState.d.ts +0 -19
  63. package/types/cube/animationSlice.d.ts +0 -26
  64. package/types/cube/animationState.d.ts +0 -41
  65. package/types/cube/cubeSettings.d.ts +0 -17
  66. package/types/cube/cubeState.d.ts +0 -47
  67. package/types/cube/stickerState.d.ts +0 -21
  68. package/types/index.d.ts +0 -87
  69. package/types/three/centerPiece.d.ts +0 -15
  70. package/types/three/cornerPiece.d.ts +0 -24
  71. package/types/three/cube.d.ts +0 -130
  72. package/types/three/sticker.d.ts +0 -15
  73. /package/src/{debouncer.js → webComponent/debouncer.js} +0 -0
  74. /package/src/{globals.ts → webComponent/globals.ts} +0 -0
  75. /package/types/{debouncer.d.ts → webComponent/debouncer.d.ts} +0 -0
  76. /package/types/{globals.d.ts → webComponent/globals.d.ts} +0 -0
@@ -0,0 +1,379 @@
1
+ // @ts-check
2
+ /// <reference path="./globals.ts" preserve="true" />
3
+ import { AmbientLight, DirectionalLight, PerspectiveCamera, Scene, Spherical, WebGLRenderer } from 'three';
4
+ import { OrbitControls } from 'three/examples/jsm/Addons.js';
5
+ import { debounce } from './debouncer';
6
+ import { gsap } from 'gsap';
7
+ import Settings from './settings';
8
+ import { CameraState } from './cameraState';
9
+ import { RubiksCubeController } from '../rubiksCube';
10
+ import RubiksCube3D from '../rubiksCube3D/rubiksCube3D';
11
+ import { AttributeNames, PeekActions } from './constants';
12
+
13
+ /** @import {Rotation, Movement, CubeType} from '../core' */
14
+ /** @import {PeekAction, PeekState, CameraOptions} from './constants' */
15
+ /** @import {AnimationOptions} from '../rubiksCube' */
16
+
17
+ const maxAzimuthAngle = (5 * Math.PI) / 16;
18
+ const polarAngleOffset = Math.PI / 2;
19
+ const maxPolarAngle = (5 * Math.PI) / 16;
20
+ const notInitialisedMessage = 'RubiksCubeElement is not initialised — element must be connected to the DOM before calling this method.';
21
+ const InternalEvents = Object.freeze({
22
+ cameraRadiusChanged: 'cameraRadiusChanged',
23
+ cameraSettingsChanged: 'cameraSettingsChanged',
24
+ cameraFieldOfViewChanged: 'cameraFieldOfViewChanged',
25
+ cameraPeek: 'cameraPeek',
26
+ cameraPeekComplete: 'cameraPeekComplete',
27
+ });
28
+
29
+ export class RubiksCubeElement extends HTMLElement {
30
+ constructor() {
31
+ super();
32
+ this.attachShadow({ mode: 'open' });
33
+ const root = /** @type {ShadowRoot} */ (this.shadowRoot);
34
+ root.innerHTML = `<canvas id="cube-canvas" style="display:block;"></canvas>`;
35
+ /** @private @type {HTMLCanvasElement} */
36
+ this.canvas = /** @type {HTMLCanvasElement} */ (root.getElementById('cube-canvas'));
37
+ /** @private @type {Settings} */
38
+ this.settings = new Settings();
39
+ /** @private @type {RubiksCube3D?} */
40
+ this._rubiksCube3D = null;
41
+ /** @private @type {RubiksCubeController?} */
42
+ this._rubiksCube = null;
43
+ }
44
+
45
+ /**
46
+ * @param {string} tagName the name of the tag to register the web component under
47
+ */
48
+ static register(tagName = 'rubiks-cube') {
49
+ customElements.define(tagName, this);
50
+ }
51
+
52
+ static get observedAttributes() {
53
+ return [
54
+ AttributeNames.cubeType,
55
+ AttributeNames.pieceGap,
56
+ AttributeNames.animationSpeed,
57
+ AttributeNames.animationStyle,
58
+ AttributeNames.cameraSpeed,
59
+ AttributeNames.cameraRadius,
60
+ AttributeNames.cameraFieldOfView,
61
+ AttributeNames.cameraPeekAngleHorizontal,
62
+ AttributeNames.cameraPeekAngleVertical,
63
+ AttributeNames.logo,
64
+ ];
65
+ }
66
+
67
+ connectedCallback() {
68
+ for (const attr of RubiksCubeElement.observedAttributes) {
69
+ if (this.hasAttribute(attr)) {
70
+ this.attributeChangedCallback(attr, null, this.getAttribute(attr));
71
+ }
72
+ }
73
+ this.init();
74
+ }
75
+
76
+ /**
77
+ * @param {string} name
78
+ * @param {string?} oldVal
79
+ * @param {string?} newVal
80
+ * */
81
+ attributeChangedCallback(name, oldVal, newVal) {
82
+ switch (name) {
83
+ case AttributeNames.cubeType:
84
+ this.settings.setCubeType(newVal);
85
+ if (this._rubiksCube !== null) {
86
+ this._rebuild();
87
+ }
88
+ break;
89
+ case AttributeNames.pieceGap:
90
+ this.settings.setPieceGap(newVal);
91
+ break;
92
+ case AttributeNames.animationSpeed:
93
+ this.settings.setAnimationSpeed(newVal);
94
+ break;
95
+ case AttributeNames.animationStyle:
96
+ this.settings.setAnimationStyle(newVal);
97
+ break;
98
+ case AttributeNames.cameraSpeed:
99
+ this.settings.setCameraSpeed(newVal);
100
+ break;
101
+ case AttributeNames.cameraRadius:
102
+ this.settings.setCameraRadius(newVal);
103
+ if (oldVal !== newVal && oldVal !== null) {
104
+ this.animateCameraRadius();
105
+ }
106
+ break;
107
+ case AttributeNames.cameraFieldOfView:
108
+ this.settings.setCameraFieldOfView(newVal);
109
+ if (oldVal !== newVal && oldVal !== null) {
110
+ this.updateCameraFOV();
111
+ }
112
+ break;
113
+ case AttributeNames.cameraPeekAngleHorizontal:
114
+ this.settings.setCameraPeekAngleHorizontal(newVal);
115
+ if (oldVal !== newVal && oldVal !== null) {
116
+ this.animateCameraSetting();
117
+ }
118
+ break;
119
+ case AttributeNames.cameraPeekAngleVertical:
120
+ this.settings.setCameraPeekAngleVertical(newVal);
121
+ if (oldVal !== newVal && oldVal !== null) {
122
+ this.animateCameraSetting();
123
+ }
124
+ break;
125
+ case AttributeNames.logo:
126
+ this.settings.setLogo(newVal);
127
+ }
128
+ }
129
+
130
+ /** @private */
131
+ animateCameraSetting() {
132
+ this.dispatchEvent(new CustomEvent(InternalEvents.cameraSettingsChanged));
133
+ }
134
+
135
+ /** @private */
136
+ animateCameraRadius() {
137
+ this.dispatchEvent(new CustomEvent(InternalEvents.cameraRadiusChanged));
138
+ }
139
+
140
+ /** @private */
141
+ updateCameraFOV() {
142
+ this.dispatchEvent(new CustomEvent(InternalEvents.cameraFieldOfViewChanged));
143
+ }
144
+
145
+ /** @internal @typedef {{eventId: string, move: Movement, reason: string}} MovementFailedEventData */
146
+ /**
147
+ * @param {Movement} move
148
+ * @param {AnimationOptions} [options]
149
+ * @returns {Promise<string>}
150
+ */
151
+ move(move, options) {
152
+ if (this._rubiksCube == null) {
153
+ return Promise.reject(new Error(notInitialisedMessage));
154
+ }
155
+ return this._rubiksCube.movement(move, options);
156
+ }
157
+
158
+ /**
159
+ * @param {Rotation} rotation
160
+ * @param {AnimationOptions} [options]
161
+ * @returns {Promise<string>}
162
+ */
163
+ rotate(rotation, options) {
164
+ if (this._rubiksCube == null) {
165
+ return Promise.reject(new Error(notInitialisedMessage));
166
+ }
167
+ return this._rubiksCube.rotation(rotation, options);
168
+ }
169
+
170
+ /**
171
+ * @returns {string}
172
+ */
173
+ reset() {
174
+ if (this._rubiksCube == null) {
175
+ throw new Error(notInitialisedMessage);
176
+ }
177
+ return this._rubiksCube.reset();
178
+ }
179
+
180
+ /**
181
+ * @param {string} kociembaState
182
+ * @returns {boolean}
183
+ */
184
+ setState(kociembaState) {
185
+ if (this._rubiksCube == null) {
186
+ throw new Error(notInitialisedMessage);
187
+ }
188
+ return this._rubiksCube.setState(kociembaState);
189
+ }
190
+
191
+ /**
192
+ * @returns {string}
193
+ */
194
+ getState() {
195
+ if (this._rubiksCube == null) {
196
+ throw new Error(notInitialisedMessage);
197
+ }
198
+ return this._rubiksCube.getState();
199
+ }
200
+
201
+ /**
202
+ * @param {CubeType} cubeType
203
+ * @returns {string}
204
+ */
205
+ setType(cubeType) {
206
+ this.setAttribute(AttributeNames.cubeType, cubeType);
207
+ return this.getState();
208
+ }
209
+
210
+ /**
211
+ * @private
212
+ **/
213
+ _rebuild() {
214
+ if (this._rubiksCube == null) {
215
+ throw new Error(notInitialisedMessage);
216
+ }
217
+ return this._rubiksCube.setType(this.settings.rubiksCube3DSettings.cubeType);
218
+ }
219
+
220
+ /** @internal @typedef {{eventId: string, action: PeekAction, options: CameraOptions?}} CameraPeekEventData */
221
+ /** @internal @typedef {{eventId: string, peekState: PeekState }} CameraPeekCompleteEventData */
222
+ /**
223
+ * Animates the camera to a new "peek" position.
224
+ *
225
+ * The camera tracks two independent boolean axes (horizontal: Right/Left, vertical: Up/Down), giving four
226
+ * reachable positions (the {@link PeekState}s). Each `PeekAction` operates on this state machine: actions like
227
+ * `RightUp` set both axes; `Up`/`Right`/`Left`/`Down` set one axis and leave the other untouched;
228
+ * `Horizontal`/`Vertical` toggle one axis relative to its current value. The result of a partial action
229
+ * therefore depends on the current peek state.
230
+ *
231
+ * @param {PeekAction} action
232
+ * @param {CameraOptions?} options
233
+ * @returns {Promise<PeekState>}
234
+ */
235
+ peek(action, options = null) {
236
+ if (this._rubiksCube3D == null) {
237
+ return Promise.reject(new Error(notInitialisedMessage));
238
+ }
239
+ if (!Object.values(PeekActions).includes(action)) {
240
+ return Promise.reject(new Error(`Invalid peek action: ${action}. Valid actions are ${Object.values(PeekActions).join(', ')}`));
241
+ }
242
+ /** @type {CameraPeekEventData} */
243
+ const data = { eventId: crypto.randomUUID(), action, options };
244
+ return new Promise((resolve) => {
245
+ /** @param {CustomEvent<CameraPeekCompleteEventData> | Event} event */ const handler = (event) => {
246
+ const customEvent = /** @type {CustomEvent<CameraPeekCompleteEventData>} */ (event);
247
+ if (customEvent.detail.eventId === data.eventId) {
248
+ this.removeEventListener(InternalEvents.cameraPeekComplete, handler);
249
+ resolve(customEvent.detail.peekState);
250
+ }
251
+ };
252
+ this.addEventListener(InternalEvents.cameraPeekComplete, handler);
253
+ this.dispatchEvent(new CustomEvent(InternalEvents.cameraPeek, { detail: data }));
254
+ });
255
+ }
256
+
257
+ /** @private */
258
+ init() {
259
+ this._rubiksCube3D = new RubiksCube3D(this.settings.rubiksCube3DSettings);
260
+ this._rubiksCube = new RubiksCubeController(this.settings.rubiksCube3DSettings.cubeType, this._rubiksCube3D);
261
+
262
+ // defined core threejs objects
263
+ const canvas = this.canvas;
264
+ const scene = new Scene();
265
+ const renderer = new WebGLRenderer({
266
+ alpha: true,
267
+ canvas,
268
+ antialias: true,
269
+ });
270
+ renderer.setSize(this.clientWidth, this.clientHeight);
271
+ renderer.setPixelRatio(window.devicePixelRatio);
272
+
273
+ //update renderer and camera when container resizes. debouncing events to reduce frequency
274
+ new ResizeObserver(
275
+ debounce((/** @type {{ contentRect: { width: number; height: number; }; }[]} */ entries) => {
276
+ const { width, height } = entries[0].contentRect;
277
+ camera.aspect = width / height;
278
+ camera.updateProjectionMatrix();
279
+ renderer.setSize(width, height);
280
+ }, 30),
281
+ ).observe(this);
282
+
283
+ // add camera
284
+ /**
285
+ * @returns {Spherical}
286
+ */
287
+ const cameraState = new CameraState();
288
+ const getTargetCameraSpherical = () => {
289
+ const phi = polarAngleOffset + (cameraState.Up ? -this.settings.cameraPeekAngleVertical : this.settings.cameraPeekAngleVertical) * maxPolarAngle;
290
+ const theta = (cameraState.Right ? this.settings.cameraPeekAngleHorizontal : -this.settings.cameraPeekAngleHorizontal) * maxAzimuthAngle;
291
+ return new Spherical(this.settings.cameraRadius, phi, theta);
292
+ };
293
+ const camera = new PerspectiveCamera(this.settings.cameraFieldOfView, this.clientWidth / this.clientHeight, 1, 2000);
294
+ const cameraSpherical = getTargetCameraSpherical();
295
+ camera.position.setFromSpherical(cameraSpherical);
296
+
297
+ // add orbit controls for camera
298
+ const controls = new OrbitControls(camera, renderer.domElement);
299
+ controls.enableZoom = false;
300
+ controls.enablePan = false;
301
+ controls.enableDamping = true;
302
+
303
+ // add lighting to scene
304
+ const ambientLight = new AmbientLight('white', 0.4);
305
+ const spotLight1 = new DirectionalLight('white', 2);
306
+ const spotLight2 = new DirectionalLight('white', 2);
307
+ const spotLight3 = new DirectionalLight('white', 2);
308
+ const spotLight4 = new DirectionalLight('white', 2);
309
+ spotLight1.position.set(5, 5, 5);
310
+ spotLight2.position.set(-5, 5, 5);
311
+ spotLight3.position.set(5, -5, 0);
312
+ spotLight4.position.set(-10, -5, -5);
313
+ scene.add(ambientLight, spotLight1, spotLight2, spotLight3, spotLight4);
314
+
315
+ // create cube and add to scene
316
+ const cube = this._rubiksCube3D;
317
+ scene.add(cube);
318
+
319
+ // animation loop
320
+ function animate() {
321
+ controls.update();
322
+ renderer.render(scene, camera);
323
+ }
324
+
325
+ renderer.setAnimationLoop(animate);
326
+
327
+ // Camera Events
328
+
329
+ /**
330
+ * @param {Spherical} targetSpherical
331
+ * @param {number} cameraSpeedMs
332
+ * @param {gsap.EaseString | gsap.EaseFunction | undefined} ease
333
+ * @param { undefined | (() => void) } completedCallback
334
+ */
335
+ const updateCameraPosition = (targetSpherical, cameraSpeedMs, ease, completedCallback = undefined) => {
336
+ const startSpherical = new Spherical().setFromVector3(camera.position);
337
+ gsap.to(startSpherical, {
338
+ radius: targetSpherical.radius,
339
+ theta: targetSpherical.theta,
340
+ phi: targetSpherical.phi,
341
+ duration: (cameraSpeedMs ? cameraSpeedMs : this.settings.cameraSpeedMs) / 1000,
342
+ ease: ease,
343
+ overwrite: false,
344
+ onUpdate: () => {
345
+ camera.position.setFromSpherical(startSpherical);
346
+ camera.lookAt(cube.position);
347
+ controls.update();
348
+ },
349
+ onComplete: completedCallback,
350
+ });
351
+ };
352
+
353
+ this.addEventListener(InternalEvents.cameraPeek, (event) => {
354
+ const customEvent = /** @type {CustomEvent<CameraPeekEventData>} */ (event);
355
+ cameraState.peekCamera(customEvent.detail.action);
356
+ /** @type {CameraPeekCompleteEventData} */
357
+ const data = { eventId: customEvent.detail.eventId, peekState: cameraState.toPeekState() };
358
+ const completedCallback = () => this.dispatchEvent(new CustomEvent(InternalEvents.cameraPeekComplete, { detail: data }));
359
+ const targetSpherical = getTargetCameraSpherical();
360
+ updateCameraPosition(targetSpherical, customEvent.detail.options?.cameraSpeedMs ?? this.settings.cameraSpeedMs, 'none', completedCallback);
361
+ });
362
+
363
+ this.addEventListener(InternalEvents.cameraSettingsChanged, () => {
364
+ const targetSpherical = getTargetCameraSpherical();
365
+ updateCameraPosition(targetSpherical, this.settings.cameraSpeedMs, 'none');
366
+ });
367
+
368
+ this.addEventListener(InternalEvents.cameraRadiusChanged, () => {
369
+ const targetSpherical = new Spherical().setFromVector3(camera.position);
370
+ targetSpherical.radius = this.settings.cameraRadius;
371
+ updateCameraPosition(targetSpherical, this.settings.cameraSpeedMs, 'none');
372
+ });
373
+
374
+ this.addEventListener(InternalEvents.cameraFieldOfViewChanged, () => {
375
+ camera.fov = this.settings.cameraFieldOfView;
376
+ camera.updateProjectionMatrix();
377
+ });
378
+ }
379
+ }
@@ -1,32 +1,40 @@
1
1
  // @ts-check
2
- import { AnimationStyles, CubeTypes } from './core';
3
- const defaultSettings = {
4
- cubeType: CubeTypes.Seven,
2
+ import { CubeTypes } from '../core';
3
+ import RubiksCube3DSettings from '../rubiksCube3D/cubeSettings';
4
+ import { AnimationStyles } from './constants';
5
+ /** @import {CubeType} from '../core' */
6
+ /** @import {AnimationStyle} from './constants' */
7
+
8
+ const defaultCubeSettings = {
9
+ cubeType: CubeTypes.Three,
5
10
  animationSpeedMs: 100,
6
- /** @type {import('./core').AnimationStyle} */
7
- animationStyle: 'fixed',
11
+ animationStyle: 'linear',
8
12
  pieceGap: 1.04,
13
+ };
14
+
15
+ const defaultSettings = {
9
16
  cameraSpeedMs: 100,
10
17
  cameraRadius: 5,
11
18
  cameraPeekAngleHorizontal: 0.6,
12
19
  cameraPeekAngleVertical: 0.6,
13
20
  cameraFieldOfView: 75,
14
21
  };
22
+
15
23
  const minGap = 1;
24
+ const maxGap = 1.1;
16
25
  const minRadius = 4;
17
26
  const minFieldOfView = 30;
18
27
  const maxFieldOfView = 100;
19
28
 
20
29
  export default class Settings {
21
30
  constructor() {
22
- /** @type {import("./core").CubeType} */
23
- this.cubeType = defaultSettings.cubeType;
24
- /** @type {number} */
25
- this.pieceGap = defaultSettings.pieceGap;
26
- /** @type {number} */
27
- this.animationSpeedMs = defaultSettings.animationSpeedMs;
28
- /** @type {import("./core").AnimationStyle} */
29
- this.animationStyle = defaultSettings.animationStyle;
31
+ /** @type {RubiksCube3DSettings} */
32
+ this.rubiksCube3DSettings = new RubiksCube3DSettings({
33
+ pieceGap: defaultCubeSettings.pieceGap,
34
+ animationSpeedMs: defaultCubeSettings.animationSpeedMs,
35
+ cubeType: defaultCubeSettings.cubeType,
36
+ animationStyle: defaultCubeSettings.animationStyle,
37
+ });
30
38
  /** @type {number} */
31
39
  this.cameraSpeedMs = defaultSettings.cameraSpeedMs;
32
40
  /** @type {number} */
@@ -42,8 +50,8 @@ export default class Settings {
42
50
  /** @param {any} value */
43
51
  setCubeType(value) {
44
52
  if (value && Object.values(CubeTypes).includes(value)) {
45
- const cubeType = /** @type {import('./core').CubeType} */ (value);
46
- this.cubeType = cubeType;
53
+ const cubeType = /** @type {CubeType} */ (value);
54
+ this.rubiksCube3DSettings.cubeType = cubeType;
47
55
  return;
48
56
  }
49
57
  console.warn(`Invalid cube type value. Accepted Values are [${Object.values(CubeTypes).join(', ')}] Value is ${value}`);
@@ -52,18 +60,18 @@ export default class Settings {
52
60
  /** @param {string | null} value*/
53
61
  setPieceGap(value) {
54
62
  const gap = Number(value);
55
- if (gap >= minGap && value != null) {
56
- this.pieceGap = gap;
63
+ if (gap >= minGap && gap <= maxGap && value != null) {
64
+ this.rubiksCube3DSettings.pieceGap = gap;
57
65
  return;
58
66
  }
59
- console.warn(`Invalid pieceGap value. Min is ${minGap}. Value is ${value}`);
67
+ console.warn(`Invalid pieceGap value. Min is ${minGap}. Max is ${maxGap}. Value is ${value}`);
60
68
  }
61
69
 
62
70
  /** @param {string | null} value in ms */
63
71
  setAnimationSpeed(value) {
64
72
  var speed = Number(value);
65
73
  if (speed >= 0 && value != null) {
66
- this.animationSpeedMs = speed;
74
+ this.rubiksCube3DSettings.animationSpeedMs = speed;
67
75
  return;
68
76
  }
69
77
  console.warn(`Invalid animation speed value. Min is 0. Value is ${value}`);
@@ -72,8 +80,8 @@ export default class Settings {
72
80
  /** @param {any} value */
73
81
  setAnimationStyle(value) {
74
82
  if (value && Object.values(AnimationStyles).includes(value)) {
75
- const validStyle = /** @type {import("./core").AnimationStyle} */ (value);
76
- this.animationStyle = validStyle;
83
+ const validStyle = /** @type {AnimationStyle} */ (value);
84
+ this.rubiksCube3DSettings.animationStyle = validStyle;
77
85
  return;
78
86
  }
79
87
  console.warn(`Invalid animation style value. Accepted Values are [${Object.values(AnimationStyles).join(', ')}] Value is ${value}`);
@@ -96,7 +104,7 @@ export default class Settings {
96
104
  this.cameraRadius = radius;
97
105
  return;
98
106
  }
99
- console.warn(`Invalid camera radius value. Min is ${radius}. Value is ${value}`);
107
+ console.warn(`Invalid camera radius value. Min is ${minRadius}. Value is ${value}`);
100
108
  }
101
109
 
102
110
  /** @param {string | null} value in ms */
@@ -127,7 +135,7 @@ export default class Settings {
127
135
  return;
128
136
  }
129
137
  if (fov > maxFieldOfView && value != null) {
130
- console.warn(`Invalid camera FOV value. Min is ${minFieldOfView} Max is ${maxFieldOfView}. Value is ${value} which is aboe the maximum.`);
138
+ console.warn(`Invalid camera FOV value. Min is ${minFieldOfView} Max is ${maxFieldOfView}. Value is ${value} which is above the maximum.`);
131
139
  return;
132
140
  }
133
141
  if (value == null) {
@@ -135,4 +143,9 @@ export default class Settings {
135
143
  }
136
144
  this.cameraFieldOfView = fov;
137
145
  }
146
+
147
+ /** @param {string | null} value in ms */
148
+ setLogo(value) {
149
+ this.rubiksCube3DSettings.logo = value;
150
+ }
138
151
  }
package/tests/common.js CHANGED
@@ -1,27 +1,10 @@
1
- import CubeSettings from '../src/cube/cubeSettings.js';
2
- import RubiksCube3D from '../src/three/cube.js';
1
+ import RubiksCube3DSettings from '../src/rubiksCube3D/cubeSettings.js';
2
+ import RubiksCube3D from '../src/rubiksCube3D/rubiksCube3D.js';
3
3
  /**
4
4
  * @param {import('../src/core.js').CubeType} cubeType
5
5
  * @returns {RubiksCube3D}
6
6
  **/
7
7
  export function createTestCube(cubeType) {
8
- // Use a fixed animation style and zero duration so rotations complete deterministically in tests.
9
- const settings = new CubeSettings(1.04, 0, 'fixed', cubeType);
8
+ const settings = new RubiksCube3DSettings({ pieceGap: 1.04, animationSpeedMs: 0, cubeType, animationStyle: 'sine' });
10
9
  return new RubiksCube3D(settings);
11
10
  }
12
-
13
- /**
14
- *
15
- * @param {RubiksCube3D} cube
16
- * @param {number} maxIterations
17
- */
18
- export function drainUpdates(cube, maxIterations = 20) {
19
- // Step the cube until the current rotation and queue are empty, or until we hit a safety cap.
20
- for (let i = 0; i < maxIterations; i++) {
21
- cube.update();
22
- if (!cube._currentRotation && cube._rotationQueue.length === 0) {
23
- return;
24
- }
25
- }
26
- throw new Error('Animation Queue not empty');
27
- }
@@ -0,0 +1,56 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { CubeTypes, isMovement, IsRotation, Movements, reverse, Rotations, translate } from '../src/core';
3
+
4
+ test("reverse 5B -> 5B'", () => {
5
+ expect(Movements.Five.B).toBe(reverse(Movements.Five.BP));
6
+ });
7
+
8
+ test("reverse 2Uw -> 2Uw'", () => {
9
+ expect(Movements.Two.Uw).toBe(reverse(Movements.Two.UwP));
10
+ });
11
+
12
+ test("reverse f -> f'", () => {
13
+ expect(Movements.Wide.f).toBe(reverse(Movements.Wide.fP));
14
+ });
15
+
16
+ test("reverse 2-3f -> 2-3f'", () => {
17
+ expect('2-3f').toBe(reverse("2-3f'"));
18
+ });
19
+
20
+ test("reverse U -> U'", () => {
21
+ expect(Movements.Single.U).toBe(reverse(Movements.Single.UP));
22
+ });
23
+
24
+ test('translate u -> 5u', () => {
25
+ expect(Movements.Five.u).toBe(translate(Movements.Wide.u, CubeTypes.Six));
26
+ });
27
+
28
+ test('translate Rw -> 4Rw', () => {
29
+ expect(Movements.Four.u).toBe(translate(Movements.Wide.u, CubeTypes.Five));
30
+ });
31
+
32
+ test('translate l -> 6l', () => {
33
+ expect(Movements.Six.u).toBe(translate(Movements.Wide.u, CubeTypes.Seven));
34
+ });
35
+
36
+ const allMovements = Object.values(Movements).flatMap((group) => (typeof group === 'object' ? Object.values(group) : []));
37
+ test.each(allMovements)('IsMovement %s', (movement) => {
38
+ expect(isMovement(movement)).toBe(true);
39
+ });
40
+
41
+ const allRotations = Object.values(Rotations);
42
+ test.each(allRotations)('IsRotation %s', (rotation) => {
43
+ expect(IsRotation(rotation)).toBe(true);
44
+ });
45
+
46
+ const rangeableBases = [...Object.values(Movements.Wide), ...Object.values(Movements.Single)];
47
+ const layerRanges = [];
48
+ for (let lower = 1; lower <= 6; lower++) {
49
+ for (let upper = lower + 1; upper <= 7; upper++) {
50
+ layerRanges.push([lower, upper]);
51
+ }
52
+ }
53
+ const allRangeMovements = layerRanges.flatMap(([lower, upper]) => rangeableBases.map((base) => Movements.Range(lower, upper, base)));
54
+ test.each(allRangeMovements)('IsMovement Range %s', (movement) => {
55
+ expect(isMovement(movement)).toBe(true);
56
+ });
@@ -0,0 +1,41 @@
1
+ // @ts-check
2
+ import './setup.js';
3
+ import { expect, test } from 'bun:test';
4
+ import { scrambles } from './testScrambles.js';
5
+ import { RubiksCubeController } from '../src/rubiksCube';
6
+ import RubiksCube3D from '../src/rubiksCube3D/rubiksCube3D.js';
7
+ import RubiksCube3DSettings from '../src/rubiksCube3D/cubeSettings.js';
8
+ import { toKociemba } from '../src/state/stickerState.js';
9
+ import { IsRotation } from '../src/core/index.js';
10
+
11
+ test.each(scrambles)('RubiksCube with 3D view $cubeType solve with scramble = $scramble', ({ cubeType, scramble, solution }) => {
12
+ // Arrange
13
+ const cube3D = new RubiksCube3D(new RubiksCube3DSettings({ pieceGap: 1, animationSpeedMs: 0, cubeType, animationStyle: 'sine' }));
14
+ const cube = new RubiksCubeController(cubeType, cube3D);
15
+ const initialState = cube.getState();
16
+ const scrambleMoves = /** @type {import('../src/core/index.js').Movement[]} */ (scramble.split(' '));
17
+
18
+ for (const move of scrambleMoves) {
19
+ cube.movement(move);
20
+ }
21
+ const scrambleState3D = toKociemba(cube3D.getStickerState());
22
+ const scrambleState = cube.getState();
23
+
24
+ // Act
25
+ const solutionActions = /** @type {(import('../src/core/index.js').Movement | import('../src/core/index.js').Rotation)[]} */ (solution.split(' '));
26
+ for (const action of solutionActions) {
27
+ if (IsRotation(action)) {
28
+ cube.rotation(/** @type {import('../src/core/index.js').Rotation} */ (action));
29
+ } else {
30
+ cube.movement(/** @type {import('../src/core/index.js').Movement} */ (action));
31
+ }
32
+ }
33
+ const solutionState3D = toKociemba(cube3D.getStickerState());
34
+ const solutionState = cube.getState();
35
+
36
+ // Assert
37
+ expect(scrambleState3D).toBe(scrambleState);
38
+ expect(solutionState3D).toBe(solutionState);
39
+ expect(scrambleState).not.toBe(initialState);
40
+ expect(solutionState).toBe(initialState);
41
+ });