@wodzik/cubecore 0.1.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 (279) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +118 -0
  3. package/analyze/client.d.ts +18 -0
  4. package/analyze/client.js +42 -0
  5. package/analyze/index.d.ts +114 -0
  6. package/analyze/index.js +151 -0
  7. package/analyze/rotations.d.ts +3 -0
  8. package/analyze/rotations.js +15 -0
  9. package/analyze/roux.d.ts +67 -0
  10. package/analyze/roux.js +95 -0
  11. package/analyze/worker.d.ts +6 -0
  12. package/analyze/worker.js +31 -0
  13. package/analyze/zz.d.ts +47 -0
  14. package/analyze/zz.js +102 -0
  15. package/bld/index.d.ts +14 -0
  16. package/bld/index.js +14 -0
  17. package/bld/memo.d.ts +66 -0
  18. package/bld/memo.js +139 -0
  19. package/bld/scheme.d.ts +30 -0
  20. package/bld/scheme.js +35 -0
  21. package/bld/skin.d.ts +26 -0
  22. package/bld/skin.js +46 -0
  23. package/bld/tracker.d.ts +68 -0
  24. package/bld/tracker.js +145 -0
  25. package/bluetooth/clock.d.ts +22 -0
  26. package/bluetooth/clock.js +37 -0
  27. package/bluetooth/cubeSkins.d.ts +31 -0
  28. package/bluetooth/cubeSkins.js +32 -0
  29. package/bluetooth/gyro.d.ts +31 -0
  30. package/bluetooth/gyro.js +46 -0
  31. package/bluetooth/index.d.ts +5 -0
  32. package/bluetooth/index.js +5 -0
  33. package/bluetooth/session.d.ts +140 -0
  34. package/bluetooth/session.js +201 -0
  35. package/bluetooth/simulated.d.ts +52 -0
  36. package/bluetooth/simulated.js +80 -0
  37. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.d.ts +38 -0
  38. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.js +51 -0
  39. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.d.ts +35 -0
  40. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.js +70 -0
  41. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.d.ts +273 -0
  42. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.js +846 -0
  43. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.d.ts +38 -0
  44. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.js +340 -0
  45. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.d.ts +11 -0
  46. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.js +118 -0
  47. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.d.ts +2 -0
  48. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.js +15 -0
  49. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.d.ts +18 -0
  50. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.js +152 -0
  51. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.d.ts +74 -0
  52. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.js +161 -0
  53. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.d.ts +7 -0
  54. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.js +11 -0
  55. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.d.ts +2 -0
  56. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.js +36 -0
  57. package/bluetooth/vendor/smartcube-web-bluetooth/index.d.ts +4 -0
  58. package/bluetooth/vendor/smartcube-web-bluetooth/index.js +4 -0
  59. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.d.ts +12 -0
  60. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js +122 -0
  61. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts +11 -0
  62. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.js +55 -0
  63. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.d.ts +21 -0
  64. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.js +47 -0
  65. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.d.ts +6 -0
  66. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.js +24 -0
  67. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.d.ts +6 -0
  68. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.js +57 -0
  69. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.d.ts +11 -0
  70. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.js +67 -0
  71. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.d.ts +8 -0
  72. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.js +98 -0
  73. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.d.ts +8 -0
  74. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.js +128 -0
  75. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.d.ts +5 -0
  76. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.js +60 -0
  77. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.d.ts +2 -0
  78. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.js +8 -0
  79. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.d.ts +7 -0
  80. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.js +142 -0
  81. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.d.ts +6 -0
  82. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.js +21 -0
  83. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.d.ts +32 -0
  84. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.js +1 -0
  85. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.d.ts +6 -0
  86. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.js +68 -0
  87. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.d.ts +3 -0
  88. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.js +178 -0
  89. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.d.ts +16 -0
  90. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.js +141 -0
  91. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.d.ts +11 -0
  92. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.js +10 -0
  93. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.d.ts +26 -0
  94. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.js +8 -0
  95. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.d.ts +3 -0
  96. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.js +265 -0
  97. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.d.ts +3 -0
  98. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.js +345 -0
  99. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.d.ts +15 -0
  100. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.js +382 -0
  101. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.d.ts +3 -0
  102. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.js +352 -0
  103. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.d.ts +57 -0
  104. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.js +243 -0
  105. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.d.ts +3 -0
  106. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.js +454 -0
  107. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.d.ts +3 -0
  108. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.js +424 -0
  109. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.d.ts +78 -0
  110. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.js +1 -0
  111. package/bluetooth/vendor/smartcube-web-bluetooth/utils.d.ts +45 -0
  112. package/bluetooth/vendor/smartcube-web-bluetooth/utils.js +150 -0
  113. package/cfop/caseData.d.ts +16 -0
  114. package/cfop/caseData.js +88 -0
  115. package/cfop/cases.d.ts +42 -0
  116. package/cfop/cases.js +116 -0
  117. package/cfop/f2l.d.ts +28 -0
  118. package/cfop/f2l.js +88 -0
  119. package/cfop/f2lData.d.ts +15 -0
  120. package/cfop/f2lData.js +49 -0
  121. package/cfop/index.d.ts +41 -0
  122. package/cfop/index.js +110 -0
  123. package/cfop/slots.d.ts +2 -0
  124. package/cfop/slots.js +1 -0
  125. package/core/arrows.d.ts +24 -0
  126. package/core/arrows.js +22 -0
  127. package/core/bytes.d.ts +27 -0
  128. package/core/bytes.js +145 -0
  129. package/core/centers.d.ts +32 -0
  130. package/core/centers.js +83 -0
  131. package/core/checks.d.ts +53 -0
  132. package/core/checks.js +85 -0
  133. package/core/cubies.d.ts +45 -0
  134. package/core/cubies.js +169 -0
  135. package/core/frames.d.ts +61 -0
  136. package/core/frames.js +126 -0
  137. package/core/geometry.d.ts +58 -0
  138. package/core/geometry.js +132 -0
  139. package/core/index.d.ts +19 -0
  140. package/core/index.js +19 -0
  141. package/core/masks.d.ts +32 -0
  142. package/core/masks.js +39 -0
  143. package/core/method.d.ts +35 -0
  144. package/core/method.js +30 -0
  145. package/core/moves.d.ts +48 -0
  146. package/core/moves.js +88 -0
  147. package/core/notation.d.ts +48 -0
  148. package/core/notation.js +226 -0
  149. package/core/physical.d.ts +53 -0
  150. package/core/physical.js +99 -0
  151. package/core/practice.d.ts +62 -0
  152. package/core/practice.js +87 -0
  153. package/core/sequence.d.ts +90 -0
  154. package/core/sequence.js +157 -0
  155. package/core/stages.d.ts +90 -0
  156. package/core/stages.js +21 -0
  157. package/core/state.d.ts +55 -0
  158. package/core/state.js +140 -0
  159. package/core/stateCodec.d.ts +12 -0
  160. package/core/stateCodec.js +51 -0
  161. package/core/stream.d.ts +45 -0
  162. package/core/stream.js +75 -0
  163. package/core/tracker.d.ts +62 -0
  164. package/core/tracker.js +121 -0
  165. package/element/alg.d.ts +45 -0
  166. package/element/alg.js +150 -0
  167. package/element/base.d.ts +2 -0
  168. package/element/base.js +3 -0
  169. package/element/bld.d.ts +82 -0
  170. package/element/bld.js +222 -0
  171. package/element/index.d.ts +8 -0
  172. package/element/index.js +21 -0
  173. package/element/model.d.ts +36 -0
  174. package/element/model.js +64 -0
  175. package/element/player.d.ts +196 -0
  176. package/element/player.js +680 -0
  177. package/element/practice.d.ts +75 -0
  178. package/element/practice.js +150 -0
  179. package/element/scramble.d.ts +50 -0
  180. package/element/scramble.js +107 -0
  181. package/element/sequence.d.ts +126 -0
  182. package/element/sequence.js +254 -0
  183. package/element/styles.d.ts +32 -0
  184. package/element/styles.js +196 -0
  185. package/image/cache.d.ts +21 -0
  186. package/image/cache.js +66 -0
  187. package/image/index.d.ts +2 -0
  188. package/image/index.js +2 -0
  189. package/image/png-node.d.ts +6 -0
  190. package/image/png-node.js +10 -0
  191. package/image/svg.d.ts +36 -0
  192. package/image/svg.js +215 -0
  193. package/lbl/index.d.ts +10 -0
  194. package/lbl/index.js +31 -0
  195. package/methods/index.d.ts +20 -0
  196. package/methods/index.js +33 -0
  197. package/package.json +133 -0
  198. package/petrus/index.d.ts +11 -0
  199. package/petrus/index.js +27 -0
  200. package/react/index.d.ts +176 -0
  201. package/react/index.js +228 -0
  202. package/render/arrows.d.ts +68 -0
  203. package/render/arrows.js +190 -0
  204. package/render/build/attachments.d.ts +35 -0
  205. package/render/build/attachments.js +94 -0
  206. package/render/build/models.d.ts +9 -0
  207. package/render/build/models.js +27 -0
  208. package/render/build/tiles.d.ts +48 -0
  209. package/render/build/tiles.js +194 -0
  210. package/render/gltf.d.ts +10 -0
  211. package/render/gltf.js +106 -0
  212. package/render/index.d.ts +12 -0
  213. package/render/index.js +12 -0
  214. package/render/layers.d.ts +21 -0
  215. package/render/layers.js +24 -0
  216. package/render/pictures.d.ts +46 -0
  217. package/render/pictures.js +80 -0
  218. package/render/pieceModels.d.ts +38 -0
  219. package/render/pieceModels.js +43 -0
  220. package/render/renderer.d.ts +165 -0
  221. package/render/renderer.js +647 -0
  222. package/render/tile.d.ts +114 -0
  223. package/render/tile.js +390 -0
  224. package/render/viewports.d.ts +25 -0
  225. package/render/viewports.js +26 -0
  226. package/roux/cmll.d.ts +35 -0
  227. package/roux/cmll.js +121 -0
  228. package/roux/cmllData.d.ts +12 -0
  229. package/roux/cmllData.js +49 -0
  230. package/roux/index.d.ts +52 -0
  231. package/roux/index.js +175 -0
  232. package/skin/attachments.d.ts +82 -0
  233. package/skin/attachments.js +110 -0
  234. package/skin/index.d.ts +3 -0
  235. package/skin/index.js +3 -0
  236. package/skin/shapes.d.ts +112 -0
  237. package/skin/shapes.js +147 -0
  238. package/skin/skin.d.ts +911 -0
  239. package/skin/skin.js +412 -0
  240. package/solve/client.d.ts +58 -0
  241. package/solve/client.js +56 -0
  242. package/solve/handle.d.ts +6 -0
  243. package/solve/handle.js +38 -0
  244. package/solve/index.d.ts +8 -0
  245. package/solve/index.js +7 -0
  246. package/solve/lse.d.ts +42 -0
  247. package/solve/lse.js +151 -0
  248. package/solve/pieces.d.ts +18 -0
  249. package/solve/pieces.js +24 -0
  250. package/solve/protocol.d.ts +70 -0
  251. package/solve/protocol.js +16 -0
  252. package/solve/scramble.d.ts +76 -0
  253. package/solve/scramble.js +187 -0
  254. package/solve/stage.d.ts +108 -0
  255. package/solve/stage.js +532 -0
  256. package/solve/twophase.d.ts +36 -0
  257. package/solve/twophase.js +229 -0
  258. package/solve/worker.d.ts +6 -0
  259. package/solve/worker.js +15 -0
  260. package/timeline/codec.d.ts +23 -0
  261. package/timeline/codec.js +84 -0
  262. package/timeline/index.d.ts +7 -0
  263. package/timeline/index.js +7 -0
  264. package/timeline/recording.d.ts +19 -0
  265. package/timeline/recording.js +20 -0
  266. package/timeline/replay.d.ts +68 -0
  267. package/timeline/replay.js +145 -0
  268. package/timeline/segments.d.ts +31 -0
  269. package/timeline/segments.js +39 -0
  270. package/timeline/share.d.ts +30 -0
  271. package/timeline/share.js +62 -0
  272. package/timeline/stats.d.ts +34 -0
  273. package/timeline/stats.js +56 -0
  274. package/timeline/timings.d.ts +30 -0
  275. package/timeline/timings.js +54 -0
  276. package/zz/index.d.ts +29 -0
  277. package/zz/index.js +68 -0
  278. package/zz/seeds.d.ts +9 -0
  279. package/zz/seeds.js +29 -0
package/core/state.js ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Cube state: 54 stickers, each carrying the index of its HOME facelet in the
3
+ * solved cube (so pieces keep their identity — masks and rendering follow
4
+ * them). A sticker's colour class is its home face: `colorOf(id) = id / 9`.
5
+ *
6
+ * Nothing here assumes a colour scheme or an orientation: "solved" means
7
+ * every face shows a single colour class, whichever it is. Real colours are
8
+ * the renderer's business (colour class → colour of a scheme).
9
+ */
10
+ import { CUBIES, CUBIE_OF_FACELET, FACELETS, FACELET_COUNT, FACES, centerOf } from "./geometry.js";
11
+ import { movePermutation } from "./moves.js";
12
+ import { parseAlg } from "./notation.js";
13
+ export function solvedState() {
14
+ return Uint8Array.from({ length: FACELET_COUNT }, (_, i) => i);
15
+ }
16
+ export const colorOf = (sticker) => (sticker / 9) | 0;
17
+ /** Colour class shown at facelet position `i`. */
18
+ export const colorAt = (state, i) => colorOf(state[i]);
19
+ /** Colour class of the centre currently on `face`. */
20
+ export const centerColor = (state, face) => colorAt(state, centerOf(face));
21
+ export function applyPermutation(state, perm) {
22
+ const out = new Uint8Array(FACELET_COUNT);
23
+ for (let i = 0; i < FACELET_COUNT; i++)
24
+ out[i] = state[perm[i]];
25
+ return out;
26
+ }
27
+ export function applyMove(state, move) {
28
+ return applyPermutation(state, movePermutation(move));
29
+ }
30
+ export function applyMoves(state, moves) {
31
+ let s = state;
32
+ for (const m of typeof moves === "string" ? parseAlg(moves) : moves)
33
+ s = applyMove(s, m);
34
+ return s;
35
+ }
36
+ export function statesEqual(a, b) {
37
+ for (let i = 0; i < FACELET_COUNT; i++)
38
+ if (a[i] !== b[i])
39
+ return false;
40
+ return true;
41
+ }
42
+ /** Every face shows one colour (any orientation, any scheme). */
43
+ export function isSolved(state) {
44
+ for (let f = 0; f < 6; f++) {
45
+ const c = colorAt(state, f * 9 + 4);
46
+ for (let i = 0; i < 9; i++)
47
+ if (colorAt(state, f * 9 + i) !== c)
48
+ return false;
49
+ }
50
+ return true;
51
+ }
52
+ // ─── Colour strings (Kociemba-style facelet strings) ───
53
+ /**
54
+ * 54 letters in U R F D L B facelet order, each naming the face whose home
55
+ * colour the sticker has — e.g. the solved cube is "UUUUUUUUURRR…BBB".
56
+ * This is what smart cubes and most solvers exchange.
57
+ */
58
+ export function toFaceletString(state) {
59
+ let s = "";
60
+ for (let i = 0; i < FACELET_COUNT; i++)
61
+ s += FACES[colorAt(state, i)];
62
+ return s;
63
+ }
64
+ /**
65
+ * Build a state from a facelet string. Stickers get their identity by matching
66
+ * each piece's colour set to its home piece; throws if the colours don't form a
67
+ * real cube (wrong counts or a piece that doesn't exist).
68
+ */
69
+ export function fromFaceletString(text) {
70
+ if (text.length !== FACELET_COUNT)
71
+ throw new Error(`Expected ${FACELET_COUNT} facelets, got ${text.length}`);
72
+ const colors = [...text].map((c) => {
73
+ const i = FACES.indexOf(c);
74
+ if (i < 0)
75
+ throw new Error(`Unknown facelet "${c}"`);
76
+ return i;
77
+ });
78
+ const homeBySet = new Map();
79
+ for (const c of CUBIES)
80
+ homeBySet.set(c.facelets.map((f) => colorOf(f)).sort().join(","), CUBIES.indexOf(c));
81
+ const state = new Uint8Array(FACELET_COUNT);
82
+ const used = new Set();
83
+ for (const cubie of CUBIES) {
84
+ const set = cubie.facelets.map((f) => colors[f]).sort().join(",");
85
+ const homeIdx = homeBySet.get(set);
86
+ if (homeIdx === undefined)
87
+ throw new Error(`No piece has colours ${set}`);
88
+ if (used.has(homeIdx))
89
+ throw new Error(`Piece with colours ${set} appears twice`);
90
+ used.add(homeIdx);
91
+ const home = CUBIES[homeIdx];
92
+ for (const f of cubie.facelets)
93
+ state[f] = home.facelets.find((h) => colorOf(h) === colors[f]);
94
+ }
95
+ return state;
96
+ }
97
+ // ─── Mutable wrapper for hot paths ───
98
+ export class CubeState {
99
+ s;
100
+ constructor(initial = solvedState()) {
101
+ this.s = new Uint8Array(initial);
102
+ }
103
+ get state() {
104
+ return this.s;
105
+ }
106
+ apply(moves) {
107
+ if (typeof moves === "string" || Array.isArray(moves))
108
+ this.s = applyMoves(this.s, moves);
109
+ else
110
+ this.s = applyMove(this.s, moves);
111
+ return this;
112
+ }
113
+ reset(state = solvedState()) {
114
+ this.s = new Uint8Array(state);
115
+ return this;
116
+ }
117
+ isSolved() {
118
+ return isSolved(this.s);
119
+ }
120
+ clone() {
121
+ return new CubeState(this.s);
122
+ }
123
+ }
124
+ // Re-exported for convenience.
125
+ export { CUBIE_OF_FACELET, FACELETS };
126
+ /**
127
+ * The state whose solution is the way from `from` to `to`: solving it gives
128
+ * moves M with applyMoves(from, M) = to. Lets any solver (and so any
129
+ * scramble generator) work from wherever the physical cube is now — a smart
130
+ * cube between attempts doesn't have to be solved first.
131
+ */
132
+ export function relativeState(from, to) {
133
+ const toInverse = new Uint8Array(FACELET_COUNT);
134
+ for (let i = 0; i < FACELET_COUNT; i++)
135
+ toInverse[to[i]] = i;
136
+ const out = new Uint8Array(FACELET_COUNT);
137
+ for (let j = 0; j < FACELET_COUNT; j++)
138
+ out[j] = toInverse[from[j]];
139
+ return out;
140
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A cube state in 15 URL-safe characters (11 bytes): version, then 73 bits —
3
+ * centre orientation (frame id, 5 bits), corner permutation (Lehmer rank, 16),
4
+ * corner twists (7 base-3 digits, 12), edge permutation (rank, 29 in two
5
+ * parts), edge flips (11). The last twist and flip follow from the others.
6
+ * For links, QR codes, storage keys. Decoding is strict: anything that isn't
7
+ * a state reachable by turning returns null.
8
+ */
9
+ import type { State } from "./state.js";
10
+ export declare const STATE_CODEC_VERSION = 1;
11
+ export declare function encodeState(state: State): string | null;
12
+ export declare function decodeState(text: string): State | null;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A cube state in 15 URL-safe characters (11 bytes): version, then 73 bits —
3
+ * centre orientation (frame id, 5 bits), corner permutation (Lehmer rank, 16),
4
+ * corner twists (7 base-3 digits, 12), edge permutation (rank, 29 in two
5
+ * parts), edge flips (11). The last twist and flip follow from the others.
6
+ * For links, QR codes, storage keys. Decoding is strict: anything that isn't
7
+ * a state reachable by turning returns null.
8
+ */
9
+ import { ByteReader, fromBase64Url, packFields, toBase64Url } from "./bytes.js";
10
+ import { fromCubies, isSolvable, permRank, permUnrank, toCubies } from "./cubies.js";
11
+ import { FRAMES } from "./frames.js";
12
+ export const STATE_CODEC_VERSION = 1;
13
+ const WIDTHS = [5, 16, 12, 15, 14, 11];
14
+ const EP_LOW = 1 << 15;
15
+ export function encodeState(state) {
16
+ const c = toCubies(state);
17
+ if (!c || !isSolvable(c))
18
+ return null;
19
+ const twist = c.co.slice(0, 7).reduce((acc, t) => acc * 3 + t, 0);
20
+ const flip = c.eo.slice(0, 11).reduce((acc, f) => acc * 2 + f, 0);
21
+ const ep = permRank(c.ep);
22
+ const out = [STATE_CODEC_VERSION];
23
+ const fields = [c.frame.id, permRank(c.cp), twist, ep % EP_LOW, Math.floor(ep / EP_LOW), flip];
24
+ packFields(out, fields, WIDTHS);
25
+ return toBase64Url(out);
26
+ }
27
+ export function decodeState(text) {
28
+ const bytes = fromBase64Url(text);
29
+ if (!bytes)
30
+ return null;
31
+ const r = new ByteReader(bytes);
32
+ if (r.byte() !== STATE_CODEC_VERSION)
33
+ return null;
34
+ const f = r.fields(WIDTHS);
35
+ if (!f || !r.done)
36
+ return null;
37
+ const [frameId, cpRank, twist, epLow, epHigh, flip] = f;
38
+ const epRank = epHigh * EP_LOW + epLow;
39
+ if (frameId >= 24 || cpRank >= 40320 || twist >= 2187 || epRank >= 479001600 || flip >= 2048)
40
+ return null;
41
+ const co = [];
42
+ for (let i = 0, t = twist; i < 7; i++, t = Math.floor(t / 3))
43
+ co.unshift(t % 3);
44
+ co.push((3 - (co.reduce((a, b) => a + b, 0) % 3)) % 3);
45
+ const eo = [];
46
+ for (let i = 0, t = flip; i < 11; i++, t = Math.floor(t / 2))
47
+ eo.unshift(t % 2);
48
+ eo.push(eo.reduce((a, b) => a + b, 0) % 2);
49
+ const c = { frame: FRAMES[frameId], cp: permUnrank(cpRank, 8), co, ep: permUnrank(epRank, 12), eo };
50
+ return isSolvable(c) ? fromCubies(c) : null;
51
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Live move log for a smart cube. Turns arrive one quarter at a time, and the
3
+ * log writes them the way a cuber would — counting everything that happened:
4
+ *
5
+ * R, R → R2 (a double flick is one move)
6
+ * R, R, R → R2 R (never folded back into R')
7
+ * R, R' → R R' (a slip and its fix: two moves, not zero)
8
+ * R, L' → M (opposite faces turned together the same way: a slice;
9
+ * U, D' → E in either order, only when they arrive together —
10
+ * F', B → S see `sliceWindowMs`)
11
+ * R, L', R, L' → M2
12
+ *
13
+ * Merges only look at the end of the log, so an entry never changes once
14
+ * something else follows it.
15
+ */
16
+ import { type Move } from "./moves.js";
17
+ export interface LoggedMove {
18
+ move: Move;
19
+ /** Time of the latest turn merged into it (ms, your clock). */
20
+ time: number;
21
+ }
22
+ export interface CollapserOptions {
23
+ /** Merge R, R into R2 only when at most this far apart. Default: always. */
24
+ repeatWindowMs?: number;
25
+ /** Merge opposite faces into a slice only when at most this far apart. Default 200 ms. */
26
+ sliceWindowMs?: number;
27
+ }
28
+ /** What a push did to the log: drop `removed` entries from its end, then append `added`. */
29
+ export interface LogChange {
30
+ removed: number;
31
+ added: LoggedMove[];
32
+ }
33
+ /** The slice two face turns make together (R + L' → M), or null. */
34
+ export declare function sliceOf(a: Move, b: Move): Move | null;
35
+ export declare class MoveCollapser {
36
+ private log;
37
+ private readonly repeatWindow;
38
+ private readonly sliceWindow;
39
+ constructor(options?: CollapserOptions);
40
+ get moves(): readonly LoggedMove[];
41
+ push(move: Move, time: number): LogChange;
42
+ clear(): void;
43
+ }
44
+ /** The same rules on a finished list (all turns taken as simultaneous-enough for slices unless times are given). */
45
+ export declare function collapseMoves(moves: readonly Move[], times?: readonly number[], options?: CollapserOptions): Move[];
package/core/stream.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Live move log for a smart cube. Turns arrive one quarter at a time, and the
3
+ * log writes them the way a cuber would — counting everything that happened:
4
+ *
5
+ * R, R → R2 (a double flick is one move)
6
+ * R, R, R → R2 R (never folded back into R')
7
+ * R, R' → R R' (a slip and its fix: two moves, not zero)
8
+ * R, L' → M (opposite faces turned together the same way: a slice;
9
+ * U, D' → E in either order, only when they arrive together —
10
+ * F', B → S see `sliceWindowMs`)
11
+ * R, L', R, L' → M2
12
+ *
13
+ * Merges only look at the end of the log, so an entry never changes once
14
+ * something else follows it.
15
+ */
16
+ import { decompose } from "./physical.js";
17
+ import { amountQuarters, toAmount } from "./moves.js";
18
+ const SLICES = ["M", "E", "S"].flatMap((family) => [1, -1, 2].map((amount) => ({ family, amount })));
19
+ const same = (a, b) => a.family === b.family && a.amount === b.amount;
20
+ /** The slice two face turns make together (R + L' → M), or null. */
21
+ export function sliceOf(a, b) {
22
+ for (const slice of SLICES) {
23
+ const [x, y] = decompose(slice).turns;
24
+ if ((same(a, x) && same(b, y)) || (same(a, y) && same(b, x)))
25
+ return slice;
26
+ }
27
+ return null;
28
+ }
29
+ /** Two identical quarter turns → the half turn; anything else → null. */
30
+ function doubled(a, b) {
31
+ if (!same(a, b) || a.amount === 2)
32
+ return null;
33
+ return { family: a.family, amount: toAmount(amountQuarters(a.amount) * 2) };
34
+ }
35
+ export class MoveCollapser {
36
+ log = [];
37
+ repeatWindow;
38
+ sliceWindow;
39
+ constructor(options = {}) {
40
+ this.repeatWindow = options.repeatWindowMs ?? Infinity;
41
+ this.sliceWindow = options.sliceWindowMs ?? 200;
42
+ }
43
+ get moves() {
44
+ return this.log.map(({ move, time }) => ({ move, time }));
45
+ }
46
+ push(move, time) {
47
+ const before = this.log.length;
48
+ this.log.push({ move, time });
49
+ let touched = 1; // entries at the end that may differ from what the caller last saw
50
+ for (;;) {
51
+ const n = this.log.length;
52
+ if (n < 2)
53
+ break;
54
+ const a = this.log[n - 2], b = this.log[n - 1];
55
+ const gap = b.time - a.time;
56
+ const merged = (gap <= this.repeatWindow && doubled(a.move, b.move)) || (gap <= this.sliceWindow && sliceOf(a.move, b.move));
57
+ if (!merged)
58
+ break;
59
+ this.log.splice(n - 2, 2, { move: merged, time: b.time });
60
+ touched = Math.max(touched, before - (n - 2) + 1);
61
+ }
62
+ const keep = this.log.length - Math.min(touched, this.log.length);
63
+ const removed = before - keep;
64
+ return { removed, added: this.log.slice(keep).map(({ move, time }) => ({ move, time })) };
65
+ }
66
+ clear() {
67
+ this.log = [];
68
+ }
69
+ }
70
+ /** The same rules on a finished list (all turns taken as simultaneous-enough for slices unless times are given). */
71
+ export function collapseMoves(moves, times, options) {
72
+ const c = new MoveCollapser(options);
73
+ moves.forEach((m, i) => c.push(m, times?.[i] ?? 0));
74
+ return c.moves.map((m) => m.move);
75
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * MethodTracker — follows a solve move by move and reports when each stage
3
+ * of a method is reached, in any orientation and with any colour scheme.
4
+ *
5
+ * Colour neutrality: the method is followed independently in all 24 frames
6
+ * (orientations) — each frame keeps its own progress. The result is the frame
7
+ * that got furthest (ties: the one that got there first). So a cross or block
8
+ * that happens to appear somewhere else — in the scramble, or mid-solve —
9
+ * can't hijack the analysis: the real one overtakes it.
10
+ */
11
+ import { type Frame } from "./frames.js";
12
+ import { FACES, type Face } from "./geometry.js";
13
+ import type { Method } from "./method.js";
14
+ import type { Move } from "./moves.js";
15
+ import { type State } from "./state.js";
16
+ export interface StageBoundary {
17
+ stage: string;
18
+ /** Moves applied when the stage was reached (0 = already done before the first move). */
19
+ moveIndex: number;
20
+ /** Timestamp passed with that move (ms), if any. */
21
+ time?: number;
22
+ /** e.g. the F2L slot as physical faces ("FR", "UB"…) or "left"/"edges". */
23
+ detail?: string;
24
+ /** The case the stage started from, when the method recognises one ("OLL 27", "T", "PLL skip"…). */
25
+ case?: string;
26
+ }
27
+ export declare class MethodTracker {
28
+ readonly method: Method;
29
+ private state;
30
+ private moveCount;
31
+ private progress;
32
+ constructor(method: Method, start?: State | string);
33
+ /** Apply one or more moves (with an optional timestamp); returns stages newly reached by the leading frame. */
34
+ push(move: Move | string, time?: number): StageBoundary[];
35
+ /**
36
+ * The frame that got furthest. Ties — and at the end of a solve every frame
37
+ * has "reached" every stage, since a solved cube passes them all — go to the
38
+ * frame that reached its stages earliest overall (smallest sum of move
39
+ * indices): the real cross/block was there all along, an accidental one
40
+ * elsewhere only completes everything on the last moves.
41
+ */
42
+ private get best();
43
+ /** Stage boundaries of the leading frame. */
44
+ get boundaries(): readonly StageBoundary[];
45
+ /**
46
+ * The next stage, whether the method is finished, the leading frame (null
47
+ * before the first stage) and the case the next stage started from, if
48
+ * recognised (e.g. which OLL came up after F2L).
49
+ */
50
+ get current(): {
51
+ next: string | null;
52
+ done: boolean;
53
+ frame: Frame | null;
54
+ case?: string;
55
+ };
56
+ /** Physical face the method's canonical D (bottom) settled on — e.g. the cross face for CFOP. */
57
+ get bottomFace(): Face | null;
58
+ private advance;
59
+ }
60
+ /** Convenience: stage boundaries of a whole solve. */
61
+ export declare function analyzeSolve(method: Method, scramble: string | readonly Move[], solution: string | readonly Move[], times?: readonly number[]): readonly StageBoundary[];
62
+ export { FACES };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * MethodTracker — follows a solve move by move and reports when each stage
3
+ * of a method is reached, in any orientation and with any colour scheme.
4
+ *
5
+ * Colour neutrality: the method is followed independently in all 24 frames
6
+ * (orientations) — each frame keeps its own progress. The result is the frame
7
+ * that got furthest (ties: the one that got there first). So a cross or block
8
+ * that happens to appear somewhere else — in the scramble, or mid-solve —
9
+ * can't hijack the analysis: the real one overtakes it.
10
+ */
11
+ import { FRAMES, view } from "./frames.js";
12
+ import { FACES } from "./geometry.js";
13
+ import { parseAlg } from "./notation.js";
14
+ import { applyMove, applyMoves, solvedState } from "./state.js";
15
+ const SLOT_NAME = /^[FB][RL]$/;
16
+ const FACE_ORDER = ["U", "D", "F", "B", "L", "R"];
17
+ /** Canonical slot name (e.g. "FR") → the physical faces it sits between in `frame`. */
18
+ function physicalSlot(detail, frame) {
19
+ if (!SLOT_NAME.test(detail))
20
+ return detail;
21
+ const faces = [frame.face[detail[0]], frame.face[detail[1]]];
22
+ return faces.sort((a, b) => FACE_ORDER.indexOf(a) - FACE_ORDER.indexOf(b)).join("");
23
+ }
24
+ export class MethodTracker {
25
+ method;
26
+ state;
27
+ moveCount = 0;
28
+ progress = FRAMES.map((frame) => ({ frame, stageIndex: 0, details: [], boundaries: [], cases: [] }));
29
+ constructor(method, start = solvedState()) {
30
+ this.method = method;
31
+ this.state = typeof start === "string" ? applyMoves(solvedState(), start) : new Uint8Array(start);
32
+ const first = method.stages[0];
33
+ if (first?.recognize)
34
+ for (const p of this.progress)
35
+ p.cases[0] = first.recognize(view(this.state, p.frame));
36
+ this.advance(undefined);
37
+ }
38
+ /** Apply one or more moves (with an optional timestamp); returns stages newly reached by the leading frame. */
39
+ push(move, time) {
40
+ const moves = typeof move === "string" ? parseAlg(move) : [move];
41
+ const before = this.boundaries.length;
42
+ for (const m of moves) {
43
+ this.state = applyMove(this.state, m);
44
+ this.moveCount++;
45
+ this.advance(time);
46
+ }
47
+ return this.boundaries.slice(before);
48
+ }
49
+ /**
50
+ * The frame that got furthest. Ties — and at the end of a solve every frame
51
+ * has "reached" every stage, since a solved cube passes them all — go to the
52
+ * frame that reached its stages earliest overall (smallest sum of move
53
+ * indices): the real cross/block was there all along, an accidental one
54
+ * elsewhere only completes everything on the last moves.
55
+ */
56
+ get best() {
57
+ const score = (p) => p.boundaries.reduce((sum, b) => sum + b.moveIndex, 0);
58
+ let best = this.progress[0];
59
+ for (const p of this.progress) {
60
+ if (p.stageIndex > best.stageIndex || (p.stageIndex === best.stageIndex && score(p) < score(best)))
61
+ best = p;
62
+ }
63
+ return best;
64
+ }
65
+ /** Stage boundaries of the leading frame. */
66
+ get boundaries() {
67
+ return this.best.boundaries;
68
+ }
69
+ /**
70
+ * The next stage, whether the method is finished, the leading frame (null
71
+ * before the first stage) and the case the next stage started from, if
72
+ * recognised (e.g. which OLL came up after F2L).
73
+ */
74
+ get current() {
75
+ const best = this.best;
76
+ const next = this.method.stages[best.stageIndex];
77
+ const kase = best.cases[best.stageIndex];
78
+ return { next: next?.id ?? null, done: !next, frame: best.stageIndex > 0 ? best.frame : null, ...(kase ? { case: kase } : {}) };
79
+ }
80
+ /** Physical face the method's canonical D (bottom) settled on — e.g. the cross face for CFOP. */
81
+ get bottomFace() {
82
+ return this.current.frame?.face.D ?? null;
83
+ }
84
+ advance(time) {
85
+ for (const p of this.progress) {
86
+ let seen = null;
87
+ for (;;) {
88
+ const stage = this.method.stages[p.stageIndex];
89
+ if (!stage)
90
+ break;
91
+ seen ??= view(this.state, p.frame);
92
+ if (!stage.done(seen))
93
+ break;
94
+ const raw = stage.detail?.(seen, p.details);
95
+ if (raw)
96
+ p.details.push(raw);
97
+ const kase = p.cases[p.stageIndex];
98
+ p.boundaries.push({
99
+ stage: stage.id,
100
+ moveIndex: this.moveCount,
101
+ ...(time !== undefined ? { time } : {}),
102
+ ...(raw ? { detail: physicalSlot(raw, p.frame) } : {}),
103
+ ...(kase ? { case: kase } : {}),
104
+ });
105
+ p.stageIndex++;
106
+ // The next stage starts here: recognise its case now (e.g. the OLL that came up after F2L).
107
+ const following = this.method.stages[p.stageIndex];
108
+ if (following?.recognize)
109
+ p.cases[p.stageIndex] = following.recognize(seen);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ /** Convenience: stage boundaries of a whole solve. */
115
+ export function analyzeSolve(method, scramble, solution, times) {
116
+ const tracker = new MethodTracker(method, applyMoves(solvedState(), scramble));
117
+ const moves = typeof solution === "string" ? parseAlg(solution) : solution;
118
+ moves.forEach((m, i) => tracker.push(m, times?.[i]));
119
+ return tracker.boundaries;
120
+ }
121
+ export { FACES };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * <cube-alg> — an algorithm as written, in sync with a <cube-player>: the
3
+ * move playing now is highlighted, played ones fade, click a move to jump to
4
+ * it. Comments, grouping and pauses stay as written (repeats and commutator
5
+ * inverses light up the move they copy).
6
+ *
7
+ * <cube-player id="p" alg="(R U R' U')3 // sexy × 3"></cube-player>
8
+ * <cube-alg for="p"></cube-alg>
9
+ *
10
+ * Text: its own `alg` attribute, else the player's `alg`, else the player's
11
+ * moves (a recorded solve). Styling: --cc-alg-font, --cc-alg-size,
12
+ * --cc-alg-done, --cc-alg-current, --cc-alg-current-bg, --cc-alg-comment;
13
+ * parts alg, token, token-done, token-current, token-todo, comment.
14
+ */
15
+ import type { CubePlayer } from "./player.js";
16
+ import { ElementBase } from "./base.js";
17
+ export declare class CubeAlg extends ElementBase {
18
+ static observedAttributes: string[];
19
+ private readonly root;
20
+ private player;
21
+ private doc;
22
+ private text;
23
+ /** Token index (by source start) of each move. */
24
+ private tokenOf;
25
+ private tokens;
26
+ private readonly onTime;
27
+ private readonly onLoad;
28
+ constructor();
29
+ connectedCallback(): void;
30
+ disconnectedCallback(): void;
31
+ attributeChangedCallback(): void;
32
+ /** Follow this player (instead of `for`). */
33
+ attach(player: CubePlayer): void;
34
+ private connect;
35
+ private disconnect;
36
+ private render;
37
+ /** Moves [0, applied) are done; move `applied` is next / playing. */
38
+ private highlight;
39
+ }
40
+ export declare function defineCubeAlg(tag?: string): void;
41
+ declare global {
42
+ interface HTMLElementTagNameMap {
43
+ "cube-alg": CubeAlg;
44
+ }
45
+ }