@houstonp/rubiks-cube 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +304 -77
  2. package/package.json +18 -8
  3. package/src/{core.js → core/index.js} +72 -41
  4. package/src/rubiksCube/index.js +3 -0
  5. package/src/rubiksCube/rubiksCubeController.js +111 -0
  6. package/src/{three → rubiksCube3D}/centerPiece.js +37 -2
  7. package/src/{three → rubiksCube3D}/cornerPiece.js +56 -2
  8. package/src/rubiksCube3D/cubeConfig.js +87 -0
  9. package/src/rubiksCube3D/cubeSettings.js +30 -0
  10. package/src/{three → rubiksCube3D}/edgePiece.js +2 -1
  11. package/src/rubiksCube3D/index.js +3 -0
  12. package/src/rubiksCube3D/rubiksCube3D.js +383 -0
  13. package/src/{three → rubiksCube3D}/sticker.js +5 -4
  14. package/src/state/index.js +4 -0
  15. package/src/state/rubiksCubeState.js +471 -0
  16. package/src/state/slice.js +236 -0
  17. package/src/state/stickerState.js +185 -0
  18. package/src/{camera → webComponent}/cameraState.js +17 -25
  19. package/src/webComponent/constants.js +67 -0
  20. package/src/webComponent/index.js +7 -0
  21. package/src/webComponent/rubiksCubeElement.js +379 -0
  22. package/src/{settings.js → webComponent/settings.js} +36 -23
  23. package/tests/common.js +3 -20
  24. package/tests/core.test.js +56 -0
  25. package/tests/rubiksCube.solves.test.js +41 -0
  26. package/tests/rubiksCube3D.solves.test.js +185 -0
  27. package/tests/rubiksCubeState.solves.test.js +35 -0
  28. package/tests/testScrambles.js +194 -0
  29. package/types/{core.d.ts → core/index.d.ts} +45 -48
  30. package/types/rubiksCube/index.d.ts +3 -0
  31. package/types/rubiksCube/rubiksCubeController.d.ts +62 -0
  32. package/types/rubiksCube3D/centerPiece.d.ts +27 -0
  33. package/types/rubiksCube3D/cornerPiece.d.ts +38 -0
  34. package/types/rubiksCube3D/cubeConfig.d.ts +32 -0
  35. package/types/rubiksCube3D/cubeSettings.d.ts +33 -0
  36. package/types/{three → rubiksCube3D}/edgePiece.d.ts +5 -3
  37. package/types/rubiksCube3D/index.d.ts +3 -0
  38. package/types/rubiksCube3D/rubiksCube3D.d.ts +120 -0
  39. package/types/rubiksCube3D/sticker.d.ts +18 -0
  40. package/types/state/index.d.ts +5 -0
  41. package/types/state/rubiksCubeState.d.ts +108 -0
  42. package/types/state/slice.d.ts +46 -0
  43. package/types/state/stickerState.d.ts +34 -0
  44. package/types/webComponent/cameraState.d.ts +22 -0
  45. package/types/webComponent/constants.d.ts +57 -0
  46. package/types/webComponent/index.d.ts +6 -0
  47. package/types/webComponent/rubiksCubeElement.d.ts +89 -0
  48. package/types/{settings.d.ts → webComponent/settings.d.ts} +5 -8
  49. package/src/cube/animationSlice.js +0 -205
  50. package/src/cube/animationState.js +0 -96
  51. package/src/cube/cubeSettings.js +0 -19
  52. package/src/cube/cubeState.js +0 -337
  53. package/src/cube/stickerState.js +0 -188
  54. package/src/index.js +0 -621
  55. package/src/three/cube.js +0 -492
  56. package/tests/cube.five.test.js +0 -126
  57. package/tests/cube.four.test.js +0 -126
  58. package/tests/cube.seven.test.js +0 -126
  59. package/tests/cube.six.test.js +0 -126
  60. package/tests/cube.three.test.js +0 -151
  61. package/tests/cube.two.test.js +0 -125
  62. package/types/camera/cameraState.d.ts +0 -19
  63. package/types/cube/animationSlice.d.ts +0 -26
  64. package/types/cube/animationState.d.ts +0 -41
  65. package/types/cube/cubeSettings.d.ts +0 -17
  66. package/types/cube/cubeState.d.ts +0 -47
  67. package/types/cube/stickerState.d.ts +0 -21
  68. package/types/index.d.ts +0 -87
  69. package/types/three/centerPiece.d.ts +0 -15
  70. package/types/three/cornerPiece.d.ts +0 -24
  71. package/types/three/cube.d.ts +0 -130
  72. package/types/three/sticker.d.ts +0 -15
  73. /package/src/{debouncer.js → webComponent/debouncer.js} +0 -0
  74. /package/src/{globals.ts → webComponent/globals.ts} +0 -0
  75. /package/types/{debouncer.d.ts → webComponent/debouncer.d.ts} +0 -0
  76. /package/types/{globals.d.ts → webComponent/globals.d.ts} +0 -0
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Rubiks Cube Web Component
2
2
 
3
- A Rubik's 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, state setting, and camera "peek" positions. Supports 2x2, 3x3, 4x4, 5x5, 6x6, and 7x7 Rubik's cubes.
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();
@@ -50,11 +83,11 @@ RubiksCubeElement.register();
50
83
 
51
84
  ## Component attributes
52
85
 
53
- These attributes control animation, spacing, camera behavior, and cube type. The available attributes
54
- 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.
55
88
 
56
89
  ```js
57
- import { RubiksCubeElement, AttributeNames } from '@houstonp/rubiks-cube';
90
+ import { RubiksCubeElement, AttributeNames } from '@houstonp/rubiks-cube/view';
58
91
  import { CubeTypes, AnimationStyles } from '@houstonp/rubiks-cube/core';
59
92
 
60
93
  const cube = document.querySelector('rubiks-cube');
@@ -74,28 +107,33 @@ cube.setAttribute(AttributeNames.cameraPeekAngleHorizontal, '0.7');
74
107
  cube.setAttribute(AttributeNames.cameraPeekAngleVertical, '0.7');
75
108
  ```
76
109
 
77
- | attribute | accepted values | Description |
78
- | ---------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
79
- | cube-type | `"Two"`, `"Three"`, `"Four"`, `"Five"`, `"Six"`, `"Seven"` | Sets the cube size (2x2 through 7x7). Default is `"Three"` |
80
- | animation-speed-ms | integer greater than or equal to 0 | Sets the duration of cube animations in milliseconds |
81
- | 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 |
82
- | piece-gap | greater than 1 | Sets the gap between Rubik's Cube pieces |
83
- | camera-speed-ms | greater than or equal to 0 | Sets the duration of camera animations in milliseconds |
84
- | camera-radius | greater than or equal to 4 | Sets the camera radius |
85
- | camera-peek-angle-horizontal | decimal between 0 and 1 | Sets the horizontal peek angle |
86
- | camera-peek-angle-vertical | decimal between 0 and 1 | Sets the vertical peek angle |
87
- | 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` |
88
121
 
89
122
  ## Programmatic control
90
123
 
91
- 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.
92
126
 
93
127
  ### Move
94
128
 
95
129
  Performs a cube movement and resolves with the new state string.
96
130
 
131
+ ```ts
132
+ move(move: Movement, options?: AnimationOptions): Promise<string>
133
+ ```
134
+
97
135
  ```js
98
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
136
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
99
137
  import { Movements } from '@houstonp/rubiks-cube/core';
100
138
 
101
139
  const cube = document.querySelector('rubiks-cube');
@@ -108,8 +146,8 @@ await cube.move(Movements.Single.U); // Upper face clockwise
108
146
  await cube.move(Movements.Single.FP); // Front face counter-clockwise
109
147
 
110
148
  // Wide moves
111
- await cube.move(Movements.Wide.Rw); // Wide right move
112
- await cube.move(Movements.Wide.r); // Right two layers
149
+ await cube.move(Movements.Wide.Rw); // Right two layers (Rw)
150
+ await cube.move(Movements.Wide.r); // Right two layers (r)
113
151
 
114
152
  // Layer-specific moves (for 4x4+ cubes)
115
153
  await cube.move(Movements.Two.R); // Second layer right
@@ -121,6 +159,15 @@ await cube.move(Movements.Single.M); // Middle layer
121
159
  await cube.move(Movements.Single.E); // Equatorial layer
122
160
  await cube.move(Movements.Single.S); // Standing layer
123
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
+
124
171
  // Chain multiple moves
125
172
  const moves = [Movements.Single.R, Movements.Single.U, Movements.Single.RP, Movements.Single.UP];
126
173
  for (const move of moves) {
@@ -133,8 +180,12 @@ for (const move of moves) {
133
180
 
134
181
  Rotates the entire cube and resolves with the new state string.
135
182
 
183
+ ```ts
184
+ rotate(rotation: Rotation, options?: AnimationOptions): Promise<string>
185
+ ```
186
+
136
187
  ```js
137
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
188
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
138
189
  import { Rotations } from '@houstonp/rubiks-cube/core';
139
190
 
140
191
  const cube = document.querySelector('rubiks-cube');
@@ -153,87 +204,164 @@ await cube.rotate(Rotations.yP); // 90 degrees counter-clockwise
153
204
  await cube.rotate(Rotations.z); // 90 degrees clockwise
154
205
  await cube.rotate(Rotations.z2); // 180 degrees
155
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 });
156
213
  ```
157
214
 
158
215
  ### Reset
159
216
 
160
- Resets the cube to the solved state and resolves with the new state string.
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
222
+ ```
161
223
 
162
224
  ```js
163
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
225
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
164
226
  import { Movements } from '@houstonp/rubiks-cube/core';
165
227
 
166
228
  const cube = document.querySelector('rubiks-cube');
167
229
 
168
230
  // Reset to solved state
169
- const solvedState = await cube.reset();
231
+ const solvedState = cube.reset();
170
232
  console.log('Cube reset to solved state:', solvedState);
171
233
 
172
234
  // Reset after performing some moves
173
235
  await cube.move(Movements.Single.R);
174
236
  await cube.move(Movements.Single.U);
175
- const resetState = await cube.reset();
237
+ const resetState = cube.reset();
176
238
  ```
177
239
 
178
- ### SetState
240
+ ### SetState / GetState
179
241
 
180
- Sets the cube to a specific state using a Kociemba-format state string. This allows you to restore a previously saved state or set up specific cube configurations.
242
+ Sets the cube to a specific state using a Kociembaformat 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
+ ```
181
249
 
182
250
  ```js
183
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
251
+ import { RubiksCubeElement } from '@houstonp/rubiks-cube/view';
184
252
  import { Movements } from '@houstonp/rubiks-cube/core';
185
253
 
186
254
  const cube = document.querySelector('rubiks-cube');
187
255
 
188
256
  // Save current state
189
- const currentState = await cube.move(Movements.Single.R);
257
+ await cube.move(Movements.Single.R);
258
+ const currentState = cube.getState();
190
259
 
191
260
  // Later, restore that state
192
- try {
193
- const restoredState = await cube.setState(currentState);
194
- console.log('State restored:', restoredState);
195
- } catch (error) {
196
- console.error('Failed to set state:', error);
197
- // Error occurs if the state string is invalid for the current cube type
261
+ const ok = cube.setState(currentState);
262
+ if (!ok) {
263
+ console.error('Failed to set state — string did not match a supported cube size');
198
264
  }
199
265
 
200
266
  // Set a specific scrambled state (example for 3x3)
201
267
  const scrambledState = 'UULUUFUUFRRUBRRURRFFDFFUFFFDDRDDDDDDBLLLLLLLLBRRBBBBBB';
202
- await cube.setState(scrambledState);
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
280
+ ```
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
203
290
  ```
204
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
+
205
295
  ### Peek
206
296
 
207
297
  Animates the camera to a new "peek" position and resolves with the new peek state.
208
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
+
209
316
  ```js
210
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
211
- import { PeekTypes, PeekStates } from '@houstonp/rubiks-cube/core';
317
+ import { RubiksCubeElement, PeekActions, PeekStates } from '@houstonp/rubiks-cube/view';
212
318
 
213
319
  const cube = document.querySelector('rubiks-cube');
214
320
 
215
- // Peek in different directions
216
- await cube.peek(PeekTypes.Right); // Peek right
217
- await cube.peek(PeekTypes.Left); // Peek left
218
- await cube.peek(PeekTypes.Up); // Peek up
219
- await cube.peek(PeekTypes.Down); // Peek down
220
- await cube.peek(PeekTypes.RightUp); // Peek right and up
221
- await cube.peek(PeekTypes.RightDown); // Peek right and down
222
- await cube.peek(PeekTypes.LeftUp); // Peek left and up
223
- await cube.peek(PeekTypes.LeftDown); // Peek left and down
224
- await cube.peek(PeekTypes.Horizontal); // Reset horizontal peek
225
- await cube.peek(PeekTypes.Vertical); // Reset vertical peek
226
-
227
- // The peek method returns the current peek state
228
- const peekState = await cube.peek(PeekTypes.RightUp);
229
- console.log('Current peek state:', peekState); // e.g., 'rightUp'
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 });
230
339
  ```
231
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
+
232
358
  ### Complete Example
233
359
 
234
360
  ```js
235
- import { RubiksCubeElement, AttributeNames } from '@houstonp/rubiks-cube';
236
- import { Movements, Rotations, PeekTypes, CubeTypes, AnimationStyles } from '@houstonp/rubiks-cube/core';
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();
237
365
 
238
366
  const cube = document.querySelector('rubiks-cube');
239
367
 
@@ -249,35 +377,105 @@ await cube.rotate(Rotations.y);
249
377
  await cube.move(Movements.Wide.Rw);
250
378
 
251
379
  // Peek to see a different angle
252
- await cube.peek(PeekTypes.RightUp);
380
+ await cube.peek(PeekActions.RightUp);
253
381
 
254
382
  // Save the current state
255
- const currentState = await cube.move(Movements.Single.F);
383
+ await cube.move(Movements.Single.F);
384
+ const currentState = cube.getState();
256
385
 
257
386
  // Reset and restore
258
- await cube.reset();
259
- await cube.setState(currentState);
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' });
260
448
  ```
261
449
 
262
- All methods return promises that resolve with the new state string (or peek state for `peek`). They reject if the operation fails or times out.
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`).
263
456
 
264
457
  ## Rubiks Cube Notation
265
458
 
266
459
  Notations can include the number of rotations of a face. For example, `U2` means rotate the upper face 180 degrees.
267
460
 
268
- 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.
269
463
 
270
- Notations can also include a layer identifier for larger cubes. For example `3R2'` means rotate the Third layer from the right face counter-clockwise twice.
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.
271
466
 
272
- 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.
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.
273
469
 
274
- Valid notation constants are available via the `core` export. Use these constants instead of string literals for better type safety and autocomplete support.
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.
275
472
 
276
- Duplicate or equivalent notations are not provided in the `core` export. For example `R3` and `R'` are equivalent and only `R'` is provided in the export.
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.
277
475
 
278
476
  ```js
279
- import { RubiksCubeElement } from '@houstonp/rubiks-cube';
280
- import { Rotations, Movements, PeekTypes, PeekStates, CubeTypes, AnimationStyles } 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';
281
479
 
282
480
  const cube = document.querySelector('rubiks-cube');
283
481
 
@@ -291,21 +489,22 @@ cube.rotate(Rotations.x);
291
489
  cube.rotate(Rotations.y2);
292
490
  cube.rotate(Rotations.zP);
293
491
 
294
- // Use constants for peek types
295
- cube.peek(PeekTypes.Right);
296
- cube.peek(PeekTypes.RightUp);
492
+ // Use constants for peek actions
493
+ cube.peek(PeekActions.Right);
494
+ cube.peek(PeekActions.RightUp);
297
495
 
298
496
  // Use constants for cube types
299
- cube.setAttribute(AttributeNames.CubeType, CubeTypes.Four);
497
+ cube.setAttribute(AttributeNames.cubeType, CubeTypes.Four);
300
498
 
301
499
  // Use constants for animation styles
302
- cube.setAttribute(AttributeNames.AnimationStyle, AnimationStyles.Exponential);
500
+ cube.setAttribute(AttributeNames.animationStyle, AnimationStyles.Exponential);
303
501
  ```
304
502
 
305
503
  Notation must match the following Regex
306
504
 
307
505
  `/([1234567]|[123456]-[1234567])?([RLUDFB]w|[RLUDFBMES]|[rludfbmes])([123])?(\')?$/`
308
- some notation may not work as intended as there is no known interpretation. eg `2M`
506
+
507
+ Some notation may not work as intended as there is no known interpretation. e.g. `2M`.
309
508
 
310
509
  Standard Notation
311
510
 
@@ -331,9 +530,35 @@ Big Cube Notation. Not all listed for brevity.
331
530
 
332
531
  | Notation | Movement |
333
532
  | -------- | ----------------------------------------------- |
334
- | NR | Nth Right most layer |
335
- | NRw | All Right layers up to the Nth Right most layer |
336
- | Nr | All Right layers up to the Nth Right most layer |
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'"
557
+ ```
558
+
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.
337
562
 
338
563
  Rotation Notation
339
564
 
@@ -367,4 +592,6 @@ This repository is set up as an npm package and uses **Bun** for scripts and typ
367
592
  bun run build:types
368
593
  ```
369
594
 
370
- 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houstonp/rubiks-cube",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Rubiks Cube Web Component built with threejs",
5
5
  "author": "Houston Pearse",
6
6
  "license": "MIT",
@@ -11,16 +11,26 @@
11
11
  "renderer",
12
12
  "threejs"
13
13
  ],
14
- "main": "./src/index.js",
15
- "types": "./types/index.d.ts",
16
14
  "exports": {
17
- ".": {
18
- "types": "./types/index.d.ts",
19
- "default": "./src/index.js"
15
+ "./view": {
16
+ "types": "./types/webComponent/index.d.ts",
17
+ "default": "./src/webComponent/index.js"
18
+ },
19
+ "./three": {
20
+ "types": "./types/rubiksCube3D/index.d.ts",
21
+ "default": "./src/rubiksCube3D/index.js"
22
+ },
23
+ "./controller": {
24
+ "types": "./types/rubiksCube/index.d.ts",
25
+ "default": "./src/rubiksCube/index.js"
20
26
  },
21
27
  "./core": {
22
- "types": "./types/core.d.ts",
23
- "default": "./src/core.js"
28
+ "types": "./types/core/index.d.ts",
29
+ "default": "./src/core/index.js"
30
+ },
31
+ "./state": {
32
+ "types": "./types/state/index.d.ts",
33
+ "default": "./src/state/index.js"
24
34
  }
25
35
  },
26
36
  "scripts": {