@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.
- package/README.md +304 -77
- package/package.json +18 -8
- package/src/{core.js → core/index.js} +72 -41
- package/src/rubiksCube/index.js +3 -0
- package/src/rubiksCube/rubiksCubeController.js +111 -0
- package/src/{three → rubiksCube3D}/centerPiece.js +37 -2
- package/src/{three → rubiksCube3D}/cornerPiece.js +56 -2
- package/src/rubiksCube3D/cubeConfig.js +87 -0
- package/src/rubiksCube3D/cubeSettings.js +30 -0
- package/src/{three → rubiksCube3D}/edgePiece.js +2 -1
- package/src/rubiksCube3D/index.js +3 -0
- package/src/rubiksCube3D/rubiksCube3D.js +383 -0
- package/src/{three → rubiksCube3D}/sticker.js +5 -4
- package/src/state/index.js +4 -0
- package/src/state/rubiksCubeState.js +471 -0
- package/src/state/slice.js +236 -0
- package/src/state/stickerState.js +185 -0
- package/src/{camera → webComponent}/cameraState.js +17 -25
- package/src/webComponent/constants.js +67 -0
- package/src/webComponent/index.js +7 -0
- package/src/webComponent/rubiksCubeElement.js +379 -0
- package/src/{settings.js → webComponent/settings.js} +36 -23
- package/tests/common.js +3 -20
- package/tests/core.test.js +56 -0
- package/tests/rubiksCube.solves.test.js +41 -0
- package/tests/rubiksCube3D.solves.test.js +185 -0
- package/tests/rubiksCubeState.solves.test.js +35 -0
- package/tests/testScrambles.js +194 -0
- package/types/{core.d.ts → core/index.d.ts} +45 -48
- package/types/rubiksCube/index.d.ts +3 -0
- package/types/rubiksCube/rubiksCubeController.d.ts +62 -0
- package/types/rubiksCube3D/centerPiece.d.ts +27 -0
- package/types/rubiksCube3D/cornerPiece.d.ts +38 -0
- package/types/rubiksCube3D/cubeConfig.d.ts +32 -0
- package/types/rubiksCube3D/cubeSettings.d.ts +33 -0
- package/types/{three → rubiksCube3D}/edgePiece.d.ts +5 -3
- package/types/rubiksCube3D/index.d.ts +3 -0
- package/types/rubiksCube3D/rubiksCube3D.d.ts +120 -0
- package/types/rubiksCube3D/sticker.d.ts +18 -0
- package/types/state/index.d.ts +5 -0
- package/types/state/rubiksCubeState.d.ts +108 -0
- package/types/state/slice.d.ts +46 -0
- package/types/state/stickerState.d.ts +34 -0
- package/types/webComponent/cameraState.d.ts +22 -0
- package/types/webComponent/constants.d.ts +57 -0
- package/types/webComponent/index.d.ts +6 -0
- package/types/webComponent/rubiksCubeElement.d.ts +89 -0
- package/types/{settings.d.ts → webComponent/settings.d.ts} +5 -8
- package/src/cube/animationSlice.js +0 -205
- package/src/cube/animationState.js +0 -96
- package/src/cube/cubeSettings.js +0 -19
- package/src/cube/cubeState.js +0 -337
- package/src/cube/stickerState.js +0 -188
- package/src/index.js +0 -621
- package/src/three/cube.js +0 -492
- package/tests/cube.five.test.js +0 -126
- package/tests/cube.four.test.js +0 -126
- package/tests/cube.seven.test.js +0 -126
- package/tests/cube.six.test.js +0 -126
- package/tests/cube.three.test.js +0 -151
- package/tests/cube.two.test.js +0 -125
- package/types/camera/cameraState.d.ts +0 -19
- package/types/cube/animationSlice.d.ts +0 -26
- package/types/cube/animationState.d.ts +0 -41
- package/types/cube/cubeSettings.d.ts +0 -17
- package/types/cube/cubeState.d.ts +0 -47
- package/types/cube/stickerState.d.ts +0 -21
- package/types/index.d.ts +0 -87
- package/types/three/centerPiece.d.ts +0 -15
- package/types/three/cornerPiece.d.ts +0 -24
- package/types/three/cube.d.ts +0 -130
- package/types/three/sticker.d.ts +0 -15
- /package/src/{debouncer.js → webComponent/debouncer.js} +0 -0
- /package/src/{globals.ts → webComponent/globals.ts} +0 -0
- /package/types/{debouncer.d.ts → webComponent/debouncer.d.ts} +0 -0
- /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 {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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 {
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 {
|
|
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 {
|
|
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 ${
|
|
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
|
|
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
|
|
2
|
-
import RubiksCube3D from '../src/
|
|
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
|
-
|
|
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
|
+
});
|