@houstonp/rubiks-cube 2.0.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 +494 -63
  2. package/package.json +22 -12
  3. package/src/core/index.js +478 -0
  4. package/src/rubiksCube/index.js +3 -0
  5. package/src/rubiksCube/rubiksCubeController.js +111 -0
  6. package/src/rubiksCube3D/centerPiece.js +79 -0
  7. package/src/rubiksCube3D/cornerPiece.js +114 -0
  8. package/src/rubiksCube3D/cubeConfig.js +87 -0
  9. package/src/rubiksCube3D/cubeSettings.js +30 -0
  10. package/src/rubiksCube3D/edgePiece.js +51 -0
  11. package/src/rubiksCube3D/index.js +3 -0
  12. package/src/rubiksCube3D/rubiksCube3D.js +383 -0
  13. package/src/rubiksCube3D/sticker.js +38 -0
  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/{cameraState.js → webComponent/cameraState.js} +17 -25
  19. package/src/webComponent/constants.js +67 -0
  20. package/src/{debouncer.js → webComponent/debouncer.js} +1 -1
  21. package/src/webComponent/index.js +7 -0
  22. package/src/webComponent/rubiksCubeElement.js +379 -0
  23. package/src/{settings.js → webComponent/settings.js} +47 -22
  24. package/tests/common.js +10 -0
  25. package/tests/core.test.js +56 -0
  26. package/tests/rubiksCube.solves.test.js +41 -0
  27. package/tests/rubiksCube3D.solves.test.js +185 -0
  28. package/tests/rubiksCubeState.solves.test.js +35 -0
  29. package/tests/setup.js +36 -0
  30. package/tests/testScrambles.js +194 -0
  31. package/types/core/index.d.ts +451 -0
  32. package/types/rubiksCube/index.d.ts +3 -0
  33. package/types/rubiksCube/rubiksCubeController.d.ts +62 -0
  34. package/types/rubiksCube3D/centerPiece.d.ts +27 -0
  35. package/types/rubiksCube3D/cornerPiece.d.ts +38 -0
  36. package/types/rubiksCube3D/cubeConfig.d.ts +32 -0
  37. package/types/rubiksCube3D/cubeSettings.d.ts +33 -0
  38. package/types/rubiksCube3D/edgePiece.d.ts +18 -0
  39. package/types/rubiksCube3D/index.d.ts +3 -0
  40. package/types/rubiksCube3D/rubiksCube3D.d.ts +120 -0
  41. package/types/rubiksCube3D/sticker.d.ts +18 -0
  42. package/types/state/index.d.ts +5 -0
  43. package/types/state/rubiksCubeState.d.ts +108 -0
  44. package/types/state/slice.d.ts +46 -0
  45. package/types/state/stickerState.d.ts +34 -0
  46. package/types/webComponent/cameraState.d.ts +22 -0
  47. package/types/webComponent/constants.d.ts +57 -0
  48. package/types/webComponent/index.d.ts +6 -0
  49. package/types/webComponent/rubiksCubeElement.d.ts +89 -0
  50. package/types/{settings.d.ts → webComponent/settings.d.ts} +9 -8
  51. package/src/core.js +0 -127
  52. package/src/cube/cube.js +0 -324
  53. package/src/cube/cubeRotation.js +0 -79
  54. package/src/cube/cubeSettings.js +0 -18
  55. package/src/cube/cubeState.js +0 -192
  56. package/src/cube/slice.js +0 -143
  57. package/src/index.js +0 -496
  58. package/src/schema.js +0 -22
  59. package/src/threejs/materials.js +0 -54
  60. package/src/threejs/pieces.js +0 -100
  61. package/src/threejs/stickers.js +0 -40
  62. package/types/cameraState.d.ts +0 -19
  63. package/types/core.d.ts +0 -125
  64. package/types/cube/cube.d.ts +0 -102
  65. package/types/cube/cubeRotation.d.ts +0 -33
  66. package/types/cube/cubeSettings.d.ts +0 -17
  67. package/types/cube/cubeState.d.ts +0 -16
  68. package/types/cube/slice.d.ts +0 -15
  69. package/types/index.d.ts +0 -65
  70. package/types/schema.d.ts +0 -11
  71. package/types/threejs/materials.d.ts +0 -21
  72. package/types/threejs/pieces.d.ts +0 -28
  73. package/types/threejs/stickers.d.ts +0 -6
  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,30 +1,40 @@
1
1
  // @ts-check
2
- const defaultSettings = {
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,
3
10
  animationSpeedMs: 100,
4
- /** @type {import("./cube/cubeSettings").AnimationStyle} */
5
- animationStyle: 'fixed',
11
+ animationStyle: 'linear',
6
12
  pieceGap: 1.04,
13
+ };
14
+
15
+ const defaultSettings = {
7
16
  cameraSpeedMs: 100,
8
17
  cameraRadius: 5,
9
18
  cameraPeekAngleHorizontal: 0.6,
10
19
  cameraPeekAngleVertical: 0.6,
11
20
  cameraFieldOfView: 75,
12
21
  };
13
- /** @type {import("./cube/cubeSettings").AnimationStyle[]} */
14
- const validAnimationStyles = ['exponential', 'next', 'fixed', 'match'];
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 {number} */
23
- this.pieceGap = defaultSettings.pieceGap;
24
- /** @type {number} */
25
- this.animationSpeedMs = defaultSettings.animationSpeedMs;
26
- /** @type {import("./cube/cubeSettings").AnimationStyle} */
27
- 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
+ });
28
38
  /** @type {number} */
29
39
  this.cameraSpeedMs = defaultSettings.cameraSpeedMs;
30
40
  /** @type {number} */
@@ -37,34 +47,44 @@ export default class Settings {
37
47
  this.cameraPeekAngleVertical = defaultSettings.cameraPeekAngleVertical;
38
48
  }
39
49
 
50
+ /** @param {any} value */
51
+ setCubeType(value) {
52
+ if (value && Object.values(CubeTypes).includes(value)) {
53
+ const cubeType = /** @type {CubeType} */ (value);
54
+ this.rubiksCube3DSettings.cubeType = cubeType;
55
+ return;
56
+ }
57
+ console.warn(`Invalid cube type value. Accepted Values are [${Object.values(CubeTypes).join(', ')}] Value is ${value}`);
58
+ }
59
+
40
60
  /** @param {string | null} value*/
41
61
  setPieceGap(value) {
42
62
  const gap = Number(value);
43
- if (gap >= minGap && value != null) {
44
- this.pieceGap = gap;
63
+ if (gap >= minGap && gap <= maxGap && value != null) {
64
+ this.rubiksCube3DSettings.pieceGap = gap;
45
65
  return;
46
66
  }
47
- 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}`);
48
68
  }
49
69
 
50
70
  /** @param {string | null} value in ms */
51
71
  setAnimationSpeed(value) {
52
72
  var speed = Number(value);
53
73
  if (speed >= 0 && value != null) {
54
- this.animationSpeedMs = speed;
74
+ this.rubiksCube3DSettings.animationSpeedMs = speed;
55
75
  return;
56
76
  }
57
77
  console.warn(`Invalid animation speed value. Min is 0. Value is ${value}`);
58
78
  }
59
79
 
60
- /** @param {string | null} value */
80
+ /** @param {any} value */
61
81
  setAnimationStyle(value) {
62
- if (validAnimationStyles.some((style) => style === value)) {
63
- const validStyle = /** @type {import("./cube/cubeSettings").AnimationStyle} */ (value);
64
- this.animationStyle = validStyle;
82
+ if (value && Object.values(AnimationStyles).includes(value)) {
83
+ const validStyle = /** @type {AnimationStyle} */ (value);
84
+ this.rubiksCube3DSettings.animationStyle = validStyle;
65
85
  return;
66
86
  }
67
- console.warn(`Invalid animation style value. Accepted Values are [${validAnimationStyles.join(', ')}] Value is ${value}`);
87
+ console.warn(`Invalid animation style value. Accepted Values are [${Object.values(AnimationStyles).join(', ')}] Value is ${value}`);
68
88
  }
69
89
 
70
90
  /** @param {string | null} value in ms */
@@ -84,7 +104,7 @@ export default class Settings {
84
104
  this.cameraRadius = radius;
85
105
  return;
86
106
  }
87
- 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}`);
88
108
  }
89
109
 
90
110
  /** @param {string | null} value in ms */
@@ -115,7 +135,7 @@ export default class Settings {
115
135
  return;
116
136
  }
117
137
  if (fov > maxFieldOfView && value != null) {
118
- 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.`);
119
139
  return;
120
140
  }
121
141
  if (value == null) {
@@ -123,4 +143,9 @@ export default class Settings {
123
143
  }
124
144
  this.cameraFieldOfView = fov;
125
145
  }
146
+
147
+ /** @param {string | null} value in ms */
148
+ setLogo(value) {
149
+ this.rubiksCube3DSettings.logo = value;
150
+ }
126
151
  }
@@ -0,0 +1,10 @@
1
+ import RubiksCube3DSettings from '../src/rubiksCube3D/cubeSettings.js';
2
+ import RubiksCube3D from '../src/rubiksCube3D/rubiksCube3D.js';
3
+ /**
4
+ * @param {import('../src/core.js').CubeType} cubeType
5
+ * @returns {RubiksCube3D}
6
+ **/
7
+ export function createTestCube(cubeType) {
8
+ const settings = new RubiksCube3DSettings({ pieceGap: 1.04, animationSpeedMs: 0, cubeType, animationStyle: 'sine' });
9
+ return new RubiksCube3D(settings);
10
+ }
@@ -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
+ });