@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
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Rubiks Cube Web Component
2
2
 
3
- A Rubiks Cube web component built with Three.js, WebGPU, and GSAP. The cube renders into a shadow‑DOM canvas and exposes a small, promise‑based API for cube moves, rotations, reset, and camera “peek” positions.
3
+ A Rubik's Cube web component built with Three.js and GSAP. The cube renders into a shadow‑DOM canvas and exposes a
4
+ small, promise‑based API for cube moves, rotations, reset, state setting, and camera "peek" positions. Supports 2x2,
5
+ 3x3, 4x4, 5x5, 6x6, and 7x7 Rubik's cubes.
6
+
7
+ The package also ships a headless cube state class, a standalone Three.js cube object, and the underlying movement
8
+ parser, so you can use any layer of the stack on its own.
4
9
 
5
10
  ![cube](cube.png)
6
11
 
@@ -14,13 +19,41 @@ bun add @houstonp/rubiks-cube
14
19
  npm install @houstonp/rubiks-cube
15
20
  ```
16
21
 
22
+ ## Which one do I want?
23
+
24
+ The package ships four primary classes; each plays a different role.
25
+
26
+ | I want to... | Use |
27
+ | --------------------------------------------------------------------- | -------------------------------------------- |
28
+ | Drop a cube into my page with no setup | `RubiksCubeElement` from `/view` |
29
+ | Add a cube to my own three.js scene | `RubiksCube3D` from `/three` |
30
+ | Drive cube state from my own renderer / view | `RubiksCubeController` from `/controller` |
31
+ | Track cube state with no rendering (solver, scrambler, headless test) | `RubiksCubeState` from `/state` |
32
+
33
+ `RubiksCubeElement` is built on top of `RubiksCube3D` + `RubiksCubeController` + `RubiksCubeState`, so most users
34
+ only need the first row.
35
+
36
+ ## Package layout
37
+
38
+ The package exposes several subpath entry points so you only pull in the parts you need.
39
+
40
+ | Subpath | Exports |
41
+ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
42
+ | `@houstonp/rubiks-cube/view` | `RubiksCubeElement`, `AttributeNames`, `PeekActions`, `PeekStates`, `AnimationStyles` |
43
+ | `@houstonp/rubiks-cube/three` | `RubiksCube3D`, `RubiksCube3DSettings` |
44
+ | `@houstonp/rubiks-cube/controller` | `RubiksCubeController` |
45
+ | `@houstonp/rubiks-cube/core` | `Movements`, `Rotations`, `Faces`, `CubeTypes`, `LayerCount`, `isMovement`, `IsRotation`, `reverse`, `translate` |
46
+ | `@houstonp/rubiks-cube/state` | `RubiksCubeState`, `Axi`, `GetMovementSlice`, `GetRotationSlice` |
47
+
48
+ There is no bare-package root export — every class lives on a subpath that names its layer.
49
+
17
50
  ## Adding the component
18
51
 
19
52
  Register the custom element and then use the tag in your HTML.
20
53
 
21
54
  ```js
22
55
  // index.js
23
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
56
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
24
57
 
25
58
  // Registers <rubiks-cube> (you can pass a different tag name if you prefer)
26
59
  RubiksCubeElement.register();
@@ -34,7 +67,14 @@ RubiksCubeElement.register();
34
67
  <title>Rubiks Cube Demo</title>
35
68
  </head>
36
69
  <body>
37
- <rubiks-cube animation-speed-ms="1000" animation-style="exponential" piece-gap="1.04" camera-speed-ms="100"></rubiks-cube>
70
+ <!-- Create a 3x3 cube with custom settings -->
71
+ <rubiks-cube cube-type="Three" animation-speed-ms="1000" animation-style="exponential" piece-gap="1.04" camera-speed-ms="100"></rubiks-cube>
72
+
73
+ <!-- Or create a 2x2 cube -->
74
+ <rubiks-cube cube-type="Two"></rubiks-cube>
75
+
76
+ <!-- Or create a 7x7 cube -->
77
+ <rubiks-cube cube-type="Seven"></rubiks-cube>
38
78
 
39
79
  <script type="module" src="index.js"></script>
40
80
  </body>
@@ -43,109 +83,492 @@ RubiksCubeElement.register();
43
83
 
44
84
  ## Component attributes
45
85
 
46
- These attributes control animation, spacing, and camera behavior. The available attributes
47
- can be imported so that they can be get and set easily.
86
+ These attributes control animation, spacing, camera behavior, and cube type. The available attributes can be imported
87
+ so that they can be get and set easily.
48
88
 
49
89
  ```js
50
- import { Attributes } from '@houstonp/rubiks-cube/schema';
90
+ import { RubiksCubeElement, AttributeNames } from '@houstonp/rubiks-cube/view';
91
+ import { CubeTypes, AnimationStyles } from '@houstonp/rubiks-cube/core';
51
92
 
52
93
  const cube = document.querySelector('rubiks-cube');
94
+
95
+ // Get an attribute value
53
96
  const animationSpeed = cube.getAttribute(AttributeNames.animationSpeed);
54
- cube.getAttribute(AttributeNames.animationSpeed, animationSpeed + 1);
97
+ console.log('Current animation speed:', animationSpeed);
98
+
99
+ // Set an attribute value
100
+ cube.setAttribute(AttributeNames.animationSpeed, '500');
101
+ cube.setAttribute(AttributeNames.cubeType, CubeTypes.Four); // Change to 4x4 cube
102
+ cube.setAttribute(AttributeNames.animationStyle, AnimationStyles.Exponential);
103
+ cube.setAttribute(AttributeNames.pieceGap, '1.05');
104
+ cube.setAttribute(AttributeNames.cameraRadius, '6');
105
+ cube.setAttribute(AttributeNames.cameraFieldOfView, '80');
106
+ cube.setAttribute(AttributeNames.cameraPeekAngleHorizontal, '0.7');
107
+ cube.setAttribute(AttributeNames.cameraPeekAngleVertical, '0.7');
55
108
  ```
56
109
 
57
- | attribute | accepted values | Description |
58
- | ---------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
59
- | animation-speed-ms | integer greater than or equal to 0 | Sets the duration of cube animations in milliseconds |
60
- | animation-style | `"exponential"`, `"next"`, `"fixed"`, `"match"` | `fixed`: fixed animation lengths, `next`: skips to next animation, `exponential`: speeds up successive animations, `match`: matches the speed to the frequency of events |
61
- | piece-gap | greater than 1 | Sets the gap between Rubik’s Cube pieces |
62
- | camera-speed-ms | greater than or equal to 0 | Sets the duration of camera animations in milliseconds |
63
- | camera-radius | greater than or equal to 4 | Sets the camera radius |
64
- | camera-peek-angle-horizontal | decimal between 0 and 1 | Sets the horizontal peek angle |
65
- | camera-peek-angle-vertical | decimal between 0 and 1 | Sets the vertical peek angle |
66
- | camera-field-of-view | integer between 40 and 100 | Sets the field of view of the camera |
110
+ | attribute | accepted values | Description |
111
+ | ---------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
112
+ | cube-type | `"Two"`, `"Three"`, `"Four"`, `"Five"`, `"Six"`, `"Seven"` | Sets the cube size (2x2 through 7x7). Default is `"Three"` |
113
+ | animation-speed-ms | number greater than or equal to 0 | Sets the duration of cube animations in milliseconds. Default is `100` |
114
+ | animation-style | `"exponential"`, `"linear"`, `"next"`, `"fixed"`, `"match"` | `fixed`: fixed animation lengths, `next`: skips to next animation, `linear`: ramps speed linearly with backlog, `exponential`: speeds up successive animations, `match`: matches event frequency. |
115
+ | piece-gap | number between 1 and 1.1 | Sets the gap between Rubik's Cube pieces. Default is `1.04` |
116
+ | camera-speed-ms | number greater than or equal to 0 | Sets the duration of camera animations in milliseconds. Default is `100` |
117
+ | camera-radius | number greater than or equal to 4 | Sets the camera radius. Default is `5` |
118
+ | camera-peek-angle-horizontal | decimal between 0 and 1 | Sets the horizontal peek angle. Default is `0.6` |
119
+ | camera-peek-angle-vertical | decimal between 0 and 1 | Sets the vertical peek angle. Default is `0.6` |
120
+ | camera-field-of-view | integer between 30 and 100 | Sets the field of view of the camera. Default is `75` |
67
121
 
68
122
  ## Programmatic control
69
123
 
70
- The `RubiksCubeElement` instance exposes async methods that return the cube state after the operation completes:
124
+ The `RubiksCubeElement` instance exposes the methods below. `move`, `rotate`, and `peek` are async and resolve once the
125
+ animation completes; `reset`, `setState`, `getState`, and `setType` are synchronous and apply to the cube immediately.
126
+
127
+ ### Move
128
+
129
+ Performs a cube movement and resolves with the new state string.
130
+
131
+ ```ts
132
+ move(move: Movement, options?: AnimationOptions): Promise<string>
133
+ ```
71
134
 
72
135
  ```js
73
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
136
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
137
+ import { Movements } from '@houstonp/rubiks-cube/core';
138
+
74
139
  const cube = document.querySelector('rubiks-cube');
75
140
 
76
- // Reset the cube; resolves with the new state string
77
- const stateAfterReset = await cube.reset();
141
+ // Single layer moves
142
+ await cube.move(Movements.Single.R); // Right face clockwise
143
+ await cube.move(Movements.Single.R2); // Right face 180 degrees
144
+ await cube.move(Movements.Single.RP); // Right face counter-clockwise
145
+ await cube.move(Movements.Single.U); // Upper face clockwise
146
+ await cube.move(Movements.Single.FP); // Front face counter-clockwise
147
+
148
+ // Wide moves
149
+ await cube.move(Movements.Wide.Rw); // Right two layers (Rw)
150
+ await cube.move(Movements.Wide.r); // Right two layers (r)
151
+
152
+ // Layer-specific moves (for 4x4+ cubes)
153
+ await cube.move(Movements.Two.R); // Second layer right
154
+ await cube.move(Movements.Three.R); // Third layer right (for 4x4+)
155
+ await cube.move(Movements.Four.R); // Fourth layer right (for 5x5+)
156
+
157
+ // Middle layer moves
158
+ await cube.move(Movements.Single.M); // Middle layer
159
+ await cube.move(Movements.Single.E); // Equatorial layer
160
+ await cube.move(Movements.Single.S); // Standing layer
161
+
162
+ // Override animation speed for a single move
163
+ await cube.move(Movements.Single.R, { animationSpeedMs: 200 });
164
+
165
+ // Reverse the move direction (R is treated as R')
166
+ await cube.move(Movements.Single.R, { reverse: true });
167
+
168
+ // Translate 3x3 notation to big cube notation (e.g. r on a 7x7 becomes 6r)
169
+ await cube.move(Movements.Wide.r, { translate: true });
170
+
171
+ // Chain multiple moves
172
+ const moves = [Movements.Single.R, Movements.Single.U, Movements.Single.RP, Movements.Single.UP];
173
+ for (const move of moves) {
174
+ const state = await cube.move(move);
175
+ console.log('State after', move, ':', state);
176
+ }
177
+ ```
78
178
 
79
- // Perform a move (see “Rubiks Cube Notation” below for allowed moves)
80
- // The concrete Movement/Rotation types are exported from the package types.
81
- cube.move(move).then((state) => {
82
- console.log('state after move:', state);
83
- });
179
+ ### Rotate
180
+
181
+ Rotates the entire cube and resolves with the new state string.
182
+
183
+ ```ts
184
+ rotate(rotation: Rotation, options?: AnimationOptions): Promise<string>
185
+ ```
186
+
187
+ ```js
188
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
189
+ import { Rotations } from '@houstonp/rubiks-cube/core';
190
+
191
+ const cube = document.querySelector('rubiks-cube');
192
+
193
+ // Rotate cube on x-axis (like R move)
194
+ await cube.rotate(Rotations.x); // 90 degrees clockwise
195
+ await cube.rotate(Rotations.x2); // 180 degrees
196
+ await cube.rotate(Rotations.xP); // 90 degrees counter-clockwise
197
+
198
+ // Rotate cube on y-axis (like U move)
199
+ await cube.rotate(Rotations.y); // 90 degrees clockwise
200
+ await cube.rotate(Rotations.y2); // 180 degrees
201
+ await cube.rotate(Rotations.yP); // 90 degrees counter-clockwise
202
+
203
+ // Rotate cube on z-axis (like F move)
204
+ await cube.rotate(Rotations.z); // 90 degrees clockwise
205
+ await cube.rotate(Rotations.z2); // 180 degrees
206
+ await cube.rotate(Rotations.zP); // 90 degrees counter-clockwise
207
+
208
+ // Override animation speed for a single rotation
209
+ await cube.rotate(Rotations.y, { animationSpeedMs: 600 });
210
+
211
+ // Reverse rotation direction (y is treated as y')
212
+ await cube.rotate(Rotations.y, { reverse: true });
213
+ ```
214
+
215
+ ### Reset
216
+
217
+ Resets the cube to the solved state and returns the new state string. Any in‑flight animation is snapped to its end
218
+ position before the reset is applied.
219
+
220
+ ```ts
221
+ reset(): string
84
222
  ```
85
223
 
86
- Available methods:
224
+ ```js
225
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
226
+ import { Movements } from '@houstonp/rubiks-cube/core';
87
227
 
88
- - `cube.move(move)` – performs a cube movement and resolves with the new state string.
89
- - `cube.rotate(rotation)` – rotates the entire cube and resolves with the new state string.
90
- - `cube.reset()` – resets the cube to the solved state and resolves with the new state string.
91
- - `cube.peek(peekType)` – animates the camera to a new “peek” position and resolves with the new peek state.
228
+ const cube = document.querySelector('rubiks-cube');
92
229
 
93
- All methods time out and reject if the underlying animation does not complete within an expected window.
230
+ // Reset to solved state
231
+ const solvedState = cube.reset();
232
+ console.log('Cube reset to solved state:', solvedState);
94
233
 
95
- ## Camera Actions
234
+ // Reset after performing some moves
235
+ await cube.move(Movements.Single.R);
236
+ await cube.move(Movements.Single.U);
237
+ const resetState = cube.reset();
238
+ ```
96
239
 
97
- The camera position can be changed with the peek method available on the component.
240
+ ### SetState / GetState
241
+
242
+ Sets the cube to a specific state using a Kociemba‑format state string, or reads the current state. `setState` returns
243
+ `true` on success and `false` if the input string is not valid for any supported cube type.
244
+
245
+ ```ts
246
+ setState(kociembaState: string): boolean
247
+ getState(): string
248
+ ```
98
249
 
99
250
  ```js
100
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
101
- import { Rotations, Movements, PeekTypes, PeekState } from '@houstonp/rubiks-cube/core';
251
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
252
+ import { Movements } from '@houstonp/rubiks-cube/core';
102
253
 
103
254
  const cube = document.querySelector('rubiks-cube');
104
- cube.peek(PeekTypes.right);
105
- cube.peek(PeekState.rightUp);
255
+
256
+ // Save current state
257
+ await cube.move(Movements.Single.R);
258
+ const currentState = cube.getState();
259
+
260
+ // Later, restore that state
261
+ const ok = cube.setState(currentState);
262
+ if (!ok) {
263
+ console.error('Failed to set state — string did not match a supported cube size');
264
+ }
265
+
266
+ // Set a specific scrambled state (example for 3x3)
267
+ const scrambledState = 'UULUUFUUFRRUBRRURRFFDFFUFFFDDRDDDDDDBLLLLLLLLBRRBBBBBB';
268
+ cube.setState(scrambledState);
269
+ ```
270
+
271
+ ### SetType
272
+
273
+ Switches the cube to a different size at runtime by reflecting the value to the `cube-type` attribute, which
274
+ triggers an internal rebuild. Returns the cube's state string after the call. If the new type matches the
275
+ current type, `setType` is a no-op and returns the current state — call `reset()` if you also want to clear
276
+ the cube to solved.
277
+
278
+ ```ts
279
+ setType(cubeType: CubeType): string
106
280
  ```
107
281
 
282
+ ```js
283
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
284
+ import { CubeTypes } from '@houstonp/rubiks-cube/core';
285
+
286
+ const cube = document.querySelector('rubiks-cube');
287
+
288
+ const newState = cube.setType(CubeTypes.Five); // Rebuild as a 5x5; returns the solved 5x5 state
289
+ cube.setType(CubeTypes.Five); // No-op; returns whatever the current state is
290
+ ```
291
+
292
+ Setting the `cube-type` attribute directly (`cube.setAttribute('cube-type', 'Five')`) is equivalent to calling
293
+ `setType`, since both go through the same attribute-change path.
294
+
295
+ ### Peek
296
+
297
+ Animates the camera to a new "peek" position and resolves with the new peek state.
298
+
299
+ ```ts
300
+ peek(action: PeekAction, options?: CameraOptions | null): Promise<PeekState>
301
+ ```
302
+
303
+ The camera tracks **two independent boolean axes** — horizontal (Right / Left) and vertical (Up / Down) — giving
304
+ **four reachable positions** (the `PeekState`s: `RightUp`, `RightDown`, `LeftUp`, `LeftDown`). The 10 `PeekAction`
305
+ values are inputs that operate on this state machine, in three categories:
306
+
307
+ | Category | Actions | Effect |
308
+ | ---------------------- | ---------------------------------------------- | ------------------------------------------------------------ |
309
+ | Set both axes | `RightUp`, `RightDown`, `LeftUp`, `LeftDown` | Move directly to that position |
310
+ | Set one axis | `Right`, `Left`, `Up`, `Down` | Set that axis only; the other axis keeps its current value |
311
+ | Toggle one axis | `Horizontal`, `Vertical` | Flip that axis relative to its current value |
312
+
313
+ Because the second and third categories only affect one axis, the result of e.g. `peek(Up)` depends on the prior
314
+ peek state. The promise always resolves with the new full `PeekState`.
315
+
316
+ ```js
317
+ import { RubiksCubeElement, PeekActions, PeekStates } from '@houstonp/rubiks-cube/view';
318
+
319
+ const cube = document.querySelector('rubiks-cube');
320
+
321
+ // Move directly to a position (sets both axes)
322
+ await cube.peek(PeekActions.RightUp); // → PeekStates.RightUp
323
+ await cube.peek(PeekActions.LeftDown); // → PeekStates.LeftDown
324
+
325
+ // Set one axis, leave the other untouched
326
+ await cube.peek(PeekActions.Right); // sets horizontal to Right; vertical unchanged
327
+ await cube.peek(PeekActions.Up); // sets vertical to Up; horizontal unchanged
328
+
329
+ // Toggle one axis relative to its current value
330
+ await cube.peek(PeekActions.Horizontal); // flips horizontal
331
+ await cube.peek(PeekActions.Vertical); // flips vertical
332
+
333
+ // The promise resolves with the new full peek state
334
+ const peekState = await cube.peek(PeekActions.RightUp);
335
+ console.log('Current peek state:', peekState); // 'rightUp'
336
+
337
+ // Override camera animation speed for a single peek
338
+ await cube.peek(PeekActions.Left, { cameraSpeedMs: 150 });
339
+ ```
340
+
341
+ ### Options
342
+
343
+ `AnimationOptions` can be passed to `move` and `rotate` to customise individual operations, taking precedence over the
344
+ corresponding element attributes.
345
+
346
+ | Option | Type | Description |
347
+ | ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------ |
348
+ | `animationSpeedMs` | `number` | Duration of the animation in milliseconds. Overrides the `animation-speed-ms` attribute for this call only. |
349
+ | `reverse` | `boolean` | Reverses the direction of the move or rotation (e.g. `R` is treated as `R'`). |
350
+ | `translate` | `boolean` | Translates 3x3 notation to the equivalent big-cube notation (e.g. `r` on a 7x7 is treated as `6r`). Movement only. |
351
+
352
+ `CameraOptions` can be passed to `peek` to customise the camera animation.
353
+
354
+ | Option | Type | Description |
355
+ | --------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
356
+ | `cameraSpeedMs` | `number` | Duration of the camera animation in milliseconds. Overrides the `camera-speed-ms` attribute for this call only. |
357
+
358
+ ### Complete Example
359
+
360
+ ```js
361
+ import { RubiksCubeElement, AttributeNames, PeekActions } from '@houstonp/rubiks-cube/view';
362
+ import { Movements, Rotations, CubeTypes, AnimationStyles } from '@houstonp/rubiks-cube/core';
363
+
364
+ RubiksCubeElement.register();
365
+
366
+ const cube = document.querySelector('rubiks-cube');
367
+
368
+ // Configure cube settings
369
+ cube.setAttribute(AttributeNames.cubeType, CubeTypes.Four); // Use 4x4 cube
370
+ cube.setAttribute(AttributeNames.animationSpeed, '800');
371
+ cube.setAttribute(AttributeNames.animationStyle, AnimationStyles.Exponential);
372
+
373
+ // Perform a sequence of moves
374
+ await cube.move(Movements.Single.R);
375
+ await cube.move(Movements.Single.U);
376
+ await cube.rotate(Rotations.y);
377
+ await cube.move(Movements.Wide.Rw);
378
+
379
+ // Peek to see a different angle
380
+ await cube.peek(PeekActions.RightUp);
381
+
382
+ // Save the current state
383
+ await cube.move(Movements.Single.F);
384
+ const currentState = cube.getState();
385
+
386
+ // Reset and restore
387
+ cube.reset();
388
+ cube.setState(currentState);
389
+ ```
390
+
391
+ ## Headless cube state
392
+
393
+ If you don't need rendering you can drive the cube state directly with `RubiksCubeState`. It tracks the cube using
394
+ the same parser as the web component and exposes both raw sticker state and Kociemba string helpers.
395
+
396
+ ```js
397
+ import { RubiksCubeState } from '@houstonp/rubiks-cube/state';
398
+ import { CubeTypes, Movements, Rotations } from '@houstonp/rubiks-cube/core';
399
+
400
+ const cube = new RubiksCubeState(CubeTypes.Three);
401
+
402
+ cube.move(Movements.Single.R);
403
+ cube.rotate(Rotations.y);
404
+
405
+ // Apply a sequence in one call
406
+ cube.do([Movements.Single.R, Movements.Single.U, Movements.Single.RP, Movements.Single.UP]);
407
+
408
+ // Read the current state as a Kociemba string
409
+ const kociemba = cube.getKociemba();
410
+ console.log(kociemba);
411
+
412
+ // Restore from a Kociemba string
413
+ const restored = new RubiksCubeState(CubeTypes.Three);
414
+ const ok = restored.setKociemba(kociemba); // false if the string is not valid for this cube size
415
+ ```
416
+
417
+ `getState()` / `setState()` round‑trip the raw sticker array if you want to skip the Kociemba encoding. For
418
+ lower‑level access to slices, the same subpath also exports `GetMovementSlice`, `GetRotationSlice`, and the `Axi`
419
+ enum.
420
+
421
+ ## Standalone 3D object
422
+
423
+ `RubiksCube3D` is a `THREE.Object3D` you can drop into your own scene. The web component uses it internally; you can
424
+ use it directly when you want full control over the renderer, camera, and animation loop.
425
+
426
+ ```js
427
+ import { RubiksCube3D, RubiksCube3DSettings } from '@houstonp/rubiks-cube/three';
428
+ import { CubeTypes, Movements } from '@houstonp/rubiks-cube/core';
429
+ import { GetMovementSlice } from '@houstonp/rubiks-cube/state';
430
+ import { Scene, PerspectiveCamera, WebGLRenderer } from 'three';
431
+
432
+ const settings = new RubiksCube3DSettings({
433
+ cubeType: CubeTypes.Three,
434
+ pieceGap: 1.04,
435
+ animationSpeedMs: 150,
436
+ animationStyle: 'sine',
437
+ });
438
+ const cube = new RubiksCube3D(settings);
439
+ // or, if the defaults are fine:
440
+ // const cube = new RubiksCube3D(new RubiksCube3DSettings());
441
+
442
+ const scene = new Scene();
443
+ scene.add(cube);
444
+
445
+ // Drive a slice manually
446
+ const slice = GetMovementSlice(Movements.Single.R, 3);
447
+ await cube.slice(slice, { animationSpeedMs: 200, ease: 'sine.inOut' });
448
+ ```
449
+
450
+ The `animationStyle` argument accepts any GSAP ease (string or function), since each slice is animated by GSAP under the
451
+ hood.
452
+
453
+ If you want the higher‑level "movement / rotation" API but with a custom 3D view, import `RubiksCubeController`
454
+ from `@houstonp/rubiks-cube/controller`. It composes a `RubiksCubeState` with any object implementing the small
455
+ `RubiksCubeViewInterface` (`slice`, `setState`, `reset`).
456
+
108
457
  ## Rubiks Cube Notation
109
458
 
110
459
  Notations can include the number of rotations of a face. For example, `U2` means rotate the upper face 180 degrees.
111
460
 
112
- Notations can also include a prime symbol `'` to indicate a counter‑clockwise rotation. For example, `U'` means rotate the upper face counter‑clockwise. The direction is always determined relative to the face being moved.
461
+ Notations can also include a prime symbol `'` to indicate a counter‑clockwise rotation. For example, `U'` means rotate
462
+ the upper face counter‑clockwise. The direction is always determined relative to the face being moved.
113
463
 
114
- When both a number and a prime symbol are included, the number is stated before the prime symbol. For example, `U2'` means rotate the upper face 180 degrees counter‑clockwise, and `U'2` is invalid.
464
+ Notations can also include a layer identifier for larger cubes. For example `3R2'` means rotate the third layer from the
465
+ right face counter‑clockwise twice.
115
466
 
116
- Valid Notation is available via an export called core.
467
+ When both a number and a prime symbol are included, the number is stated before the prime symbol. For example, `U2'`
468
+ means rotate the upper face 180 degrees counter‑clockwise, and `U'2` is invalid.
469
+
470
+ Valid notation constants are available via the `core` export. Use these constants instead of string literals for better
471
+ type safety and autocomplete support.
472
+
473
+ Duplicate or equivalent notations are not provided in the `core` export. For example `R3` and `R'` are equivalent and
474
+ only `R'` is provided in the export.
117
475
 
118
476
  ```js
119
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
120
- import { Rotations, Movements, PeekTypes, PeekState } from '@houstonp/rubiks-cube/core';
477
+ import { RubiksCubeElement, AttributeNames, PeekActions } from '@houstonp/rubiks-cube/view';
478
+ import { Rotations, Movements, CubeTypes, AnimationStyles } from '@houstonp/rubiks-cube/core';
121
479
 
122
480
  const cube = document.querySelector('rubiks-cube');
123
- cube.move(Movements.R);
124
- cube.rotation(Rotations.x2);
481
+
482
+ // Use constants for moves
483
+ cube.move(Movements.Single.R);
484
+ cube.move(Movements.Single.U2);
485
+ cube.move(Movements.Single.FP);
486
+
487
+ // Use constants for rotations
488
+ cube.rotate(Rotations.x);
489
+ cube.rotate(Rotations.y2);
490
+ cube.rotate(Rotations.zP);
491
+
492
+ // Use constants for peek actions
493
+ cube.peek(PeekActions.Right);
494
+ cube.peek(PeekActions.RightUp);
495
+
496
+ // Use constants for cube types
497
+ cube.setAttribute(AttributeNames.cubeType, CubeTypes.Four);
498
+
499
+ // Use constants for animation styles
500
+ cube.setAttribute(AttributeNames.animationStyle, AnimationStyles.Exponential);
501
+ ```
502
+
503
+ Notation must match the following Regex
504
+
505
+ `/([1234567]|[123456]-[1234567])?([RLUDFB]w|[RLUDFBMES]|[rludfbmes])([123])?(\')?$/`
506
+
507
+ Some notation may not work as intended as there is no known interpretation. e.g. `2M`.
508
+
509
+ Standard Notation
510
+
511
+ | Notation | Movement |
512
+ | -------- | ------------------------------------------ |
513
+ | U | Top face clockwise |
514
+ | u | Top two layers clockwise |
515
+ | D | Bottom face clockwise |
516
+ | d | Bottom two layers clockwise |
517
+ | L | Left face clockwise |
518
+ | l | Left two layers clockwise |
519
+ | R | Right face clockwise |
520
+ | r | Right two layers clockwise |
521
+ | F | Front face clockwise |
522
+ | f | Front two layers clockwise |
523
+ | B | Back face clockwise |
524
+ | b | Back two layers clockwise |
525
+ | M | Middle layer clockwise (relative to L) |
526
+ | E | Equatorial layer clockwise (relative to D) |
527
+ | S | Standing layer clockwise (relative to F) |
528
+
529
+ Big Cube Notation. Not all listed for brevity.
530
+
531
+ | Notation | Movement |
532
+ | -------- | ----------------------------------------------- |
533
+ | NR | Nth right‑most layer |
534
+ | NRw | All right layers up to the Nth right‑most layer |
535
+ | Nr | All right layers up to the Nth right‑most layer |
536
+ | X-YRw | Layers X through Y from the right face |
537
+
538
+ Range moves (`X-YRw`) apply to **wide moves**, **single face moves** (`R`, `L`, `U`, `D`, `F`, `B`), and **slice
539
+ moves** (`M`, `E`, `S`). The `Movements.Range` builder validates the inputs at the call site and returns a
540
+ typed string:
541
+
542
+ ```js
543
+ import { Movements } from '@houstonp/rubiks-cube/core';
544
+
545
+ // Wide moves
546
+ await cube.move(Movements.Range(2, 4, Movements.Wide.Rw)); // → '2-4Rw'
547
+ await cube.move(Movements.Range(3, 5, Movements.Wide.r)); // → '3-5r'
548
+ await cube.move(Movements.Range(2, 4, Movements.Wide.RwP)); // → "2-4Rw'"
549
+
550
+ // Single face moves
551
+ await cube.move(Movements.Range(2, 4, Movements.Single.R)); // → '2-4R'
552
+ await cube.move(Movements.Range(2, 3, Movements.Single.LP)); // → "2-3L'"
553
+
554
+ // Slice moves
555
+ await cube.move(Movements.Range(2, 4, Movements.Single.M)); // → '2-4M'
556
+ await cube.move(Movements.Range(2, 3, Movements.Single.SP)); // → "2-3S'"
125
557
  ```
126
558
 
127
- | Notation | Movement |
559
+ `Movements.Range` throws if `lower < 1`, `lower >= upper`, `upper > 7`, or the base move has an existing layer
560
+ prefix (e.g. `2R`, `2-4Rw`). It does not check the range against the current cube size — passing
561
+ `Range(2, 6, ...)` to a 4x4 produces a string that the parser will reject at `move()` time.
562
+
563
+ Rotation Notation
564
+
565
+ | Notation | Rotation |
128
566
  | -------- | ------------------------------------------------ |
129
- | U | Top face clockwise |
130
- | u | Top two layers clockwise |
131
- | D | Bottom face clockwise |
132
- | d | Bottom two layers clockwise |
133
- | L | Left face clockwise |
134
- | l | Left two layers clockwise |
135
- | R | Right face clockwise |
136
- | r | Right two layers clockwise |
137
- | F | Front face clockwise |
138
- | f | Front two layers clockwise |
139
- | B | Back face clockwise |
140
- | b | Back two layers clockwise |
141
- | M | Middle layer clockwise (relative to L) |
142
- | E | Equatorial layer clockwise (relative to D) |
143
- | S | Standing layer clockwise (relative to F) |
144
567
  | x | Rotate cube on x axis clockwise (direction of R) |
145
568
  | y | Rotate cube on y axis clockwise (direction of U) |
146
569
  | z | Rotate cube on z axis clockwise (direction of F) |
147
570
 
148
- These symbols align with the movements and rotations accepted by the components API.
571
+ These symbols align with the movements and rotations accepted by the component's API.
149
572
 
150
573
  ## Development
151
574
 
@@ -157,10 +580,18 @@ This repository is set up as an npm package and uses **Bun** for scripts and typ
157
580
  bun install
158
581
  ```
159
582
 
583
+ - **Run Tests**
584
+
585
+ ```bash
586
+ bun run test
587
+ ```
588
+
160
589
  - **Generate TypeScript declaration files**
161
590
 
162
591
  ```bash
163
592
  bun run build:types
164
593
  ```
165
594
 
166
- The generated `.d.ts` files are emitted into the `types/` directory (ignored in git) and are used for consumers of the package. There is currently no dedicated demo app or automated test suite in this repository; you can import the component into your own app (e.g., Vite, Next.js, or any ES‑module‑aware bundler) to experiment locally.
595
+ The generated `.d.ts` files are emitted into the `types/` directory (ignored in git) and are used for consumers of the
596
+ package. There is currently no dedicated demo app in this repository; you can import the component into your own app
597
+ (e.g., Vite, Next.js, or any ES‑module‑aware bundler) to experiment locally.