@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/roux/cmll.js ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * CMLL recognition — the last-layer corners with both blocks built, colour
3
+ * neutral and scheme-free. Read relative to the BLOCKS (the M slice may be
4
+ * off): L / R colours from the side centres, front / back / bottom from the
5
+ * first block's own stickers, the top colour is the one left. Corners only
6
+ * (the edges are LSE), compared up to the angle (pre-AUF) and a final U turn.
7
+ *
8
+ * recognizeCmll(state) // { id: "Sune Left Bar", group: "Sune", preAuf: "U", alg } | null (skip) — cube held blocks L/R, bottom D
9
+ */
10
+ import { CORNER_FACELETS, FRAMES, applyMoves, colorAt, fromCubies, invert, parseAlg, solvedState, view } from "../core/index.js";
11
+ import { CMLL_CASES } from "./cmllData.js";
12
+ export { CMLL_CASES } from "./cmllData.js";
13
+ /** Last-layer corners in turning order: URF UFL ULB UBR (U sticker first, then clockwise). */
14
+ const U_CORNERS = [0, 1, 2, 3].map((i) => CORNER_FACELETS[i]);
15
+ const AUFS = ["", "U", "U2", "U'"];
16
+ // Reference stickers: side centres L and R; the first block's DL edge (bottom), FL edge (front), BL edge (back).
17
+ const L_CENTRE = 40, R_CENTRE = 13, DL_BOTTOM = 30, FL_FRONT = 21, BL_BACK = 50;
18
+ function corners(s) {
19
+ const L = colorAt(s, L_CENTRE), R = colorAt(s, R_CENTRE);
20
+ const front = colorAt(s, FL_FRONT), back = colorAt(s, BL_BACK), bottom = colorAt(s, DL_BOTTOM);
21
+ const top = [0, 1, 2, 3, 4, 5].find((c) => ![L, R, front, back, bottom].includes(c));
22
+ if (top === undefined)
23
+ return null;
24
+ // Home corners by their two side colours.
25
+ const HOME = [
26
+ [R, front],
27
+ [front, L],
28
+ [L, back],
29
+ [back, R],
30
+ ];
31
+ const cp = [], co = [];
32
+ for (const fs of U_CORNERS) {
33
+ const cols = fs.map((f) => colorAt(s, f));
34
+ const o = cols.indexOf(top);
35
+ if (o < 0)
36
+ return null;
37
+ const sides = cols.filter((_, i) => i !== o);
38
+ cp.push(HOME.findIndex((h) => h.every((c) => sides.includes(c))));
39
+ co.push(o);
40
+ }
41
+ return cp.includes(-1) ? null : { cp, co };
42
+ }
43
+ const shift = (a, k) => a.map((_, i) => a[(i + k) % a.length]);
44
+ const text = (c, k, b) => `${shift(c.cp, k).map((v) => (v + b) % 4).join("")}|${shift(c.co, k).join("")}`;
45
+ /** Up to a final U turn. */
46
+ const raw = (c) => [0, 1, 2, 3].map((b) => text(c, 0, b)).sort()[0];
47
+ /** …and up to the angle. */
48
+ const key = (c) => [0, 1, 2, 3].flatMap((k) => [0, 1, 2, 3].map((b) => text(c, k, b))).sort()[0];
49
+ let TABLE = null;
50
+ function table() {
51
+ if (TABLE)
52
+ return TABLE;
53
+ TABLE = new Map();
54
+ for (const kase of CMLL_CASES) {
55
+ const c = corners(applyMoves(solvedState(), invert(parseAlg(kase.alg))));
56
+ const k = key(c);
57
+ if (TABLE.has(k))
58
+ throw new Error(`${kase.id} looks like ${TABLE.get(k).kase.id}`);
59
+ TABLE.set(k, { kase, raw: raw(c) });
60
+ }
61
+ return TABLE;
62
+ }
63
+ const SOLVED = key(corners(solvedState()));
64
+ /**
65
+ * The CMLL case of a cube held with the blocks on L / R and their bottom
66
+ * on D (both blocks solved) — null when the corners are already solved (up
67
+ * to AUF) or it isn't a CMLL state.
68
+ */
69
+ export function recognizeCmll(s) {
70
+ const c = corners(s);
71
+ if (!c)
72
+ return null;
73
+ const k = key(c);
74
+ if (k === SOLVED)
75
+ return null;
76
+ const e = table().get(k);
77
+ if (!e)
78
+ return null;
79
+ const preAuf = AUFS.find((a) => {
80
+ const t = corners(a ? applyMoves(s, a) : s);
81
+ return t !== null && raw(t) === e.raw;
82
+ }) ?? "";
83
+ return { id: e.kase.id, group: e.kase.group, preAuf, alg: e.kase.alg };
84
+ }
85
+ /** CMLL of a cube with both Roux blocks built in any orientation (colour neutral): the frame, or null. */
86
+ export function recognizeCmllAnywhere(state, blocksBuilt) {
87
+ for (const frame of FRAMES) {
88
+ const s = view(state, frame);
89
+ if (!blocksBuilt(s))
90
+ continue;
91
+ return { match: recognizeCmll(s) ?? "skip", frame };
92
+ }
93
+ return null;
94
+ }
95
+ /**
96
+ * A cube to practise one CMLL case on: both blocks solved, the corners in
97
+ * the case (from a random angle), the last six edges random. Make a
98
+ * scramble to it with @cubecore/solve `scrambleTo(state, { from })`.
99
+ */
100
+ export function cmllCaseState(id, random = Math.random) {
101
+ const kase = CMLL_CASES.find((c) => c.id === id);
102
+ if (!kase)
103
+ throw new Error(`Unknown CMLL case: ${id}`);
104
+ const c = corners(applyMoves(solvedState(), invert(parseAlg(kase.alg))));
105
+ // Last six edges (UR UF UL UB DF DB = 0 1 2 3 5 7): any order and flips the corners' parity allows.
106
+ const lse = [0, 1, 2, 3, 5, 7];
107
+ const parity = (p) => p.reduce((acc, v, i) => acc + p.slice(i + 1).filter((w) => w < v).length, 0) % 2;
108
+ let order;
109
+ do
110
+ order = [...lse].sort(() => random() - 0.5);
111
+ while (parity(order) !== parity(c.cp));
112
+ const ep = Array.from({ length: 12 }, (_, i) => i);
113
+ lse.forEach((pos, i) => (ep[pos] = order[i]));
114
+ const eo = new Array(12).fill(0);
115
+ let flips = 0;
116
+ for (const pos of lse.slice(1))
117
+ flips += eo[pos] = random() < 0.5 ? 1 : 0;
118
+ eo[lse[0]] = flips % 2;
119
+ const state = fromCubies({ frame: FRAMES[0], cp: [...c.cp, 4, 5, 6, 7], co: [...c.co, 0, 0, 0, 0], ep, eo });
120
+ return applyMoves(state, AUFS[Math.floor(random() * 4)] || []);
121
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * CMLL cases as data — names and groups as on speedcubedb.com (O, H, Pi,
3
+ * U, T, Sune, Anti Sune, L; 42 cases). `alg` is one standard algorithm per
4
+ * case; recognition is built from it, so any algorithm for the case works.
5
+ */
6
+ export interface CmllCase {
7
+ /** "Sune Left Bar", "H Columns"… */
8
+ id: string;
9
+ group: string;
10
+ alg: string;
11
+ }
12
+ export declare const CMLL_CASES: readonly CmllCase[];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * CMLL cases as data — names and groups as on speedcubedb.com (O, H, Pi,
3
+ * U, T, Sune, Anti Sune, L; 42 cases). `alg` is one standard algorithm per
4
+ * case; recognition is built from it, so any algorithm for the case works.
5
+ */
6
+ export const CMLL_CASES = [
7
+ { id: "O Adjacent", group: "Solved", alg: "R U R' F' R U R' U' R' F R2 U' R'" },
8
+ { id: "O Diagonal", group: "Solved", alg: "F R U' R' U' R U R' F' R U R' U' R' F R F'" },
9
+ { id: "H Columns", group: "H", alg: "U R U R' U R U' R' U R U2 R'" },
10
+ { id: "H Rows", group: "H", alg: "F R U R' U' R U R' U' R U R' U' F'" },
11
+ { id: "H Column", group: "H", alg: "R' F2 D R2 U R2 D' F2 R" },
12
+ { id: "H Row", group: "H", alg: "U2 r U' r2 D' r U' r' D r2 U r'" },
13
+ { id: "Pi Right Bar", group: "Pi", alg: "F R U R' U' R U R' U' F'" },
14
+ { id: "Pi Down Slash", group: "Pi", alg: "U F R' F' R U2 R U' R' U R U2 R'" },
15
+ { id: "Pi X", group: "Pi", alg: "R' F2 D R2 U' R2 D' F2 R" },
16
+ { id: "Pi Up Slash", group: "Pi", alg: "R U2 R' U' R U R' U2 R' F R F'" },
17
+ { id: "Pi Columns", group: "Pi", alg: "U' r U' r2 D' r U r' D r2 U r'" },
18
+ { id: "Pi Left Bar", group: "Pi", alg: "U' R' U' R' F R F' R U' R' U2 R" },
19
+ { id: "U Up Slash", group: "U", alg: "U2 R2 D R' U2 R D' R' U2 R'" },
20
+ { id: "U Down Slash", group: "U", alg: "R2 D' R U2 R' D R U2 R" },
21
+ { id: "U Bottom Row", group: "U", alg: "R' U' R U' R' U2 R2 U R' U R U2 R'" },
22
+ { id: "U Rows", group: "U", alg: "U' F R2 D R' U R D' R2 U' F'" },
23
+ { id: "U X", group: "U", alg: "U2 r U' r' U r' D' r U' r' D r" },
24
+ { id: "U Upper Row", group: "U", alg: "U' F R U R' U' F'" },
25
+ { id: "T Left Bar", group: "T", alg: "U' R U R' U' R' F R F'" },
26
+ { id: "T Right Bar", group: "T", alg: "U L' U' L U L F' L' F" },
27
+ { id: "T Rows", group: "T", alg: "R U2 R' U' R U' R2 U2 R U R' U R" },
28
+ { id: "T Bottom Row", group: "T", alg: "r' U r U2 R2 F R F' R" },
29
+ { id: "T Top Row", group: "T", alg: "r' D' r U r' D r U' r U r'" },
30
+ { id: "T Columns", group: "T", alg: "U2 r U' r2 D' r U2 r' D r2 U r'" },
31
+ { id: "Sune Left Bar", group: "Sune", alg: "U R U R' U R U2 R'" },
32
+ { id: "Sune X", group: "Sune", alg: "U L' U2 L U2 r U' r' F" },
33
+ { id: "Sune Up Slash", group: "Sune", alg: "U F R' F' R U2 R U2 R'" },
34
+ { id: "Sune Columns", group: "Sune", alg: "U R U R' U' R' F R F' R U R' U R U2 R'" },
35
+ { id: "Sune Right Bar", group: "Sune", alg: "U' R U R' U R' F R F' R U2 R'" },
36
+ { id: "Sune Down Slash", group: "Sune", alg: "U r U' r' F R' F' R" },
37
+ { id: "Anti Sune Right Bar", group: "Anti Sune", alg: "U R' U' R U' R' U2 R" },
38
+ { id: "Anti Sune Columns", group: "Anti Sune", alg: "U2 R U R2 F' r F R U' r2 F r" },
39
+ { id: "Anti Sune Down Slash", group: "Anti Sune", alg: "U' F' L F L' U2 L' U2 L" },
40
+ { id: "Anti Sune X", group: "Anti Sune", alg: "U' R U2 R' U2 R' F R F'" },
41
+ { id: "Anti Sune Up Slash", group: "Anti Sune", alg: "U' R' F R F' r U r'" },
42
+ { id: "Anti Sune Left Bar", group: "Anti Sune", alg: "U R U2 R' F R' F' R U' R U' R'" },
43
+ { id: "L Best", group: "L", alg: "U' F' r U r' U' r' F r" },
44
+ { id: "L Good", group: "L", alg: "U2 F R' F' R U R U' R'" },
45
+ { id: "L Pure", group: "L", alg: "R U R' U R U' R' U R U' R' U R U2 R'" },
46
+ { id: "L Front Commutator", group: "L", alg: "U2 R U2 R D R' U2 R D' R2" },
47
+ { id: "L Diagonal", group: "L", alg: "U2 R U2 R2 F R F' R U2 R'" },
48
+ { id: "L Back Commutator", group: "L", alg: "U R' U2 R' D' R U2 R' D R2" },
49
+ ];
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Roux — first block, second block, CMLL, EO, UL/UR, L4E — checks, method and
3
+ * masks. Canonical: first block on L, second on R, both on the bottom layer.
4
+ *
5
+ * M slices move the U/F/D/B centres, so blocks are checked by comparing
6
+ * pieces to each other (and to the L/R centres, which M never moves) — still
7
+ * no absolute colours, so any scheme and any orientation works.
8
+ */
9
+ import { type Frame, type Mask, type MaskRule, type Method, type LseStageDef, type StageDef, type State } from "../core/index.js";
10
+ export * from "./cmll.js";
11
+ export declare const firstBlock: (s: State) => boolean;
12
+ export declare function secondBlock(s: State): boolean;
13
+ /** CMLL: both blocks, and the top corners solved relative to them (up to AUF). */
14
+ export declare const cmll: (s: State) => boolean;
15
+ /**
16
+ * LSE edge orientation: every last-six edge has its top/bottom-coloured
17
+ * sticker on U or D — as the centres say: with the M slice a quarter off
18
+ * (the top / bottom centres on F / B), the edges in the M slice count by
19
+ * F / B instead (one M puts them right, flips and all).
20
+ */
21
+ export declare function lseEo(s: State): boolean;
22
+ /** LSE 4b: UL and UR edges in place, with the corners aligned. */
23
+ export declare function ulUr(s: State): boolean;
24
+ export declare const ROUX: Method;
25
+ export type RouxMask = "fb" | "blocks" | "cmll" | "lse" | "eo" | "ulur";
26
+ export declare const ROUX_MASKS: Record<RouxMask, MaskRule>;
27
+ export declare const rouxMask: (name: RouxMask, frame?: Frame) => Mask;
28
+ export type RouxSide = "front" | "back";
29
+ /**
30
+ * Roux trainer stages (canonical: first block on L-bottom), as in
31
+ * roux-trainers / act: first block and first square may be built with any
32
+ * bottom colour on the L side (`neutral: "x"`, the best of the four);
33
+ * FBDR keeps one first square solved, second square keeps the first block.
34
+ * EOLR is a last-six-edges stage — see @cubecore/solve `LSE_STAGES`.
35
+ */
36
+ export declare const ROUX_TRAINERS: {
37
+ readonly fb: () => StageDef;
38
+ /** First square: DL + FL + DLF (front) or DL + BL + DBL (back). */
39
+ readonly fs: (side?: RouxSide) => StageDef;
40
+ /** The first block and the DR edge, from a solved first square (`keep`: which one). */
41
+ readonly fbdr: (keep?: RouxSide) => StageDef;
42
+ /** Second square: DR + FR + DFR (front) or DR + BR + DRB (back), first block solved. */
43
+ /**
44
+ * EOLR: the six edges oriented and UL / UR brought to where one
45
+ * `[U] M2 [AUF]` (or `M' [U] M2 [AUF]` with the centres off) finishes
46
+ * them — the 16 goal states roux-trainers and act use. M / U only.
47
+ */
48
+ readonly eolr: () => LseStageDef;
49
+ /** The whole of LSE (M / U), from CMLL solved to a solved cube. */
50
+ readonly lse: () => LseStageDef;
51
+ readonly ss: (side?: RouxSide) => StageDef;
52
+ };
package/roux/index.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Roux — first block, second block, CMLL, EO, UL/UR, L4E — checks, method and
3
+ * masks. Canonical: first block on L, second on R, both on the bottom layer.
4
+ *
5
+ * M slices move the U/F/D/B centres, so blocks are checked by comparing
6
+ * pieces to each other (and to the L/R centres, which M never moves) — still
7
+ * no absolute colours, so any scheme and any orientation works.
8
+ */
9
+ import { FACELETS, IDENTITY_FRAME, PIECE, buildMask, checks as C, colorAt, } from "../core/index.js";
10
+ import { recognizeCmll } from "./cmll.js";
11
+ export * from "./cmll.js";
12
+ const { cubieAt, faceletOn, centerIdx, opposite, upToAuf } = C;
13
+ const FB = { dl: cubieAt(-1, -1, 0), fl: cubieAt(-1, 0, 1), bl: cubieAt(-1, 0, -1), dfl: cubieAt(-1, -1, 1), dbl: cubieAt(-1, -1, -1) };
14
+ const SB = { dr: cubieAt(1, -1, 0), fr: cubieAt(1, 0, 1), br: cubieAt(1, 0, -1), dfr: cubieAt(1, -1, 1), dbr: cubieAt(1, -1, -1) };
15
+ function uniformOn(s, cubies, face) {
16
+ const colors = cubies.map((c) => faceletOn(c, face)).filter((f) => f !== undefined).map((f) => colorAt(s, f));
17
+ return colors.every((c) => c === colors[0]) ? colors[0] : null;
18
+ }
19
+ /**
20
+ * A 1×2×3 Roux block on the `side` (L or R) of the bottom layer: its side
21
+ * stickers match that side's centre, and its bottom / front / back stickers
22
+ * are each one colour (the centres there may be off by M, so they aren't used).
23
+ * Returns the block's colours, or null if it isn't built.
24
+ */
25
+ function rouxBlock(s, side) {
26
+ const b = side === "L" ? FB : SB;
27
+ const pieces = Object.values(b);
28
+ const sideColor = colorAt(s, centerIdx(side));
29
+ if (!pieces.every((c) => colorAt(s, faceletOn(c, side)) === sideColor))
30
+ return null;
31
+ const bottom = uniformOn(s, pieces, "D");
32
+ const front = uniformOn(s, pieces, "F");
33
+ const back = uniformOn(s, pieces, "B");
34
+ if (bottom === null || front === null || back === null)
35
+ return null;
36
+ // The three colours must form a real block (e.g. bottom ≠ front's opposite) — true for real pieces
37
+ // once every sticker agrees, but guard against a colour appearing twice.
38
+ if (new Set([bottom, front, back, sideColor]).size !== 4)
39
+ return null;
40
+ return { bottom, front, back };
41
+ }
42
+ export const firstBlock = (s) => rouxBlock(s, "L") !== null;
43
+ export function secondBlock(s) {
44
+ const fb = rouxBlock(s, "L");
45
+ const sb = rouxBlock(s, "R");
46
+ return fb !== null && sb !== null && fb.bottom === sb.bottom && fb.front === sb.front && fb.back === sb.back;
47
+ }
48
+ const CMLL_CORNERS = { ufl: cubieAt(-1, 1, 1), ubl: cubieAt(-1, 1, -1), ufr: cubieAt(1, 1, 1), ubr: cubieAt(1, 1, -1) };
49
+ function cmllAligned(s) {
50
+ const fb = rouxBlock(s, "L");
51
+ if (!fb)
52
+ return false;
53
+ const top = opposite(fb.bottom);
54
+ const { ufl, ubl, ufr, ubr } = CMLL_CORNERS;
55
+ const col = (c, face) => colorAt(s, faceletOn(c, face));
56
+ return ([ufl, ubl, ufr, ubr].every((c) => col(c, "U") === top) &&
57
+ col(ufl, "L") === colorAt(s, centerIdx("L")) &&
58
+ col(ubl, "L") === colorAt(s, centerIdx("L")) &&
59
+ col(ufr, "R") === colorAt(s, centerIdx("R")) &&
60
+ col(ubr, "R") === colorAt(s, centerIdx("R")) &&
61
+ col(ufl, "F") === fb.front &&
62
+ col(ufr, "F") === fb.front &&
63
+ col(ubl, "B") === fb.back &&
64
+ col(ubr, "B") === fb.back);
65
+ }
66
+ /** CMLL: both blocks, and the top corners solved relative to them (up to AUF). */
67
+ export const cmll = (s) => secondBlock(s) && upToAuf(s, cmllAligned);
68
+ const LSE_EDGES = [cubieAt(0, 1, 1), cubieAt(0, 1, -1), cubieAt(-1, 1, 0), cubieAt(1, 1, 0), cubieAt(0, -1, 1), cubieAt(0, -1, -1)];
69
+ /**
70
+ * LSE edge orientation: every last-six edge has its top/bottom-coloured
71
+ * sticker on U or D — as the centres say: with the M slice a quarter off
72
+ * (the top / bottom centres on F / B), the edges in the M slice count by
73
+ * F / B instead (one M puts them right, flips and all).
74
+ */
75
+ export function lseEo(s) {
76
+ if (!cmll(s))
77
+ return false;
78
+ const bottom = rouxBlock(s, "L").bottom;
79
+ const ud = [bottom, opposite(bottom)];
80
+ const offQuarter = !ud.includes(colorAt(s, centerIdx("U")));
81
+ return LSE_EDGES.every((e) => {
82
+ const f = e.facelets.find((x) => ud.includes(colorAt(s, x)));
83
+ if (f === undefined)
84
+ return false;
85
+ const face = FACELETS[f].face;
86
+ const inM = e.pos[0] === 0;
87
+ return inM && offQuarter ? face === "F" || face === "B" : face === "U" || face === "D";
88
+ });
89
+ }
90
+ /** LSE 4b: UL and UR edges in place, with the corners aligned. */
91
+ export function ulUr(s) {
92
+ if (!lseEo(s))
93
+ return false;
94
+ return upToAuf(s, (t) => {
95
+ if (!cmllAligned(t))
96
+ return false;
97
+ const top = opposite(rouxBlock(t, "L").bottom);
98
+ const ul = cubieAt(-1, 1, 0);
99
+ const ur = cubieAt(1, 1, 0);
100
+ return (colorAt(t, faceletOn(ul, "U")) === top &&
101
+ colorAt(t, faceletOn(ur, "U")) === top &&
102
+ colorAt(t, faceletOn(ul, "L")) === colorAt(t, centerIdx("L")) &&
103
+ colorAt(t, faceletOn(ur, "R")) === colorAt(t, centerIdx("R")));
104
+ });
105
+ }
106
+ export const ROUX = {
107
+ id: "roux",
108
+ name: "Roux",
109
+ stages: [
110
+ { id: "fb", label: "First block", done: firstBlock },
111
+ { id: "sb", label: "Second block", done: secondBlock },
112
+ // CMLL recognises the case it starts from ("Sune Left Bar", "CMLL skip") — StageBoundary.case.
113
+ { id: "cmll", label: "CMLL", done: cmll, recognize: (s) => (secondBlock(s) ? (recognizeCmll(s)?.id ?? "CMLL skip") : undefined) },
114
+ { id: "eo", label: "EO", done: lseEo },
115
+ { id: "ulur", label: "UL/UR", done: ulUr },
116
+ { id: "l4e", label: "L4E", done: C.isSolved },
117
+ ],
118
+ };
119
+ const y = (c) => c.pos[1];
120
+ export const ROUX_MASKS = {
121
+ fb: (f, c) => (c.pos[0] === -1 && y(c) <= 0 ? "regular" : c.kind === "center" ? "dim" : "ignored"),
122
+ blocks: (f, c) => (c.pos[0] !== 0 && y(c) <= 0 ? "regular" : c.kind === "center" ? "dim" : "ignored"),
123
+ cmll: (f, c) => (c.kind === "corner" && y(c) === 1 ? "regular" : c.pos[0] !== 0 && y(c) <= 0 ? "dim" : "ignored"),
124
+ lse: (f, c) => (c.pos[0] === 0 || (c.kind === "edge" && y(c) === 1) ? "regular" : "dim"),
125
+ // LSE step by step: EO shows the six edges as orientation material; UL/UR puts those two in colour.
126
+ eo: (f, c) => (c.kind === "center" ? "regular" : lse6(c) ? "oriented" : "dim"),
127
+ ulur: (f, c) => (c.kind === "center" ? "regular" : c.kind === "edge" && y(c) === 1 && c.pos[0] !== 0 ? "regular" : lse6(c) ? "ignored" : "dim"),
128
+ };
129
+ /** The last six edges: the M slice plus UL and UR. */
130
+ function lse6(c) {
131
+ return c.kind === "edge" && (c.pos[0] === 0 || y(c) === 1);
132
+ }
133
+ export const rouxMask = (name, frame = IDENTITY_FRAME) => buildMask(ROUX_MASKS[name], frame);
134
+ const FB_PIECES = [PIECE.DL, PIECE.FL, PIECE.BL, PIECE.DLF, PIECE.DBL];
135
+ const FIRST_SQUARE = { front: [PIECE.DL, PIECE.FL, PIECE.DLF], back: [PIECE.DL, PIECE.BL, PIECE.DBL] };
136
+ const SECOND_SQUARE = { front: [PIECE.FR, PIECE.DFR], back: [PIECE.BR, PIECE.DRB] };
137
+ /**
138
+ * Roux trainer stages (canonical: first block on L-bottom), as in
139
+ * roux-trainers / act: first block and first square may be built with any
140
+ * bottom colour on the L side (`neutral: "x"`, the best of the four);
141
+ * FBDR keeps one first square solved, second square keeps the first block.
142
+ * EOLR is a last-six-edges stage — see @cubecore/solve `LSE_STAGES`.
143
+ */
144
+ export const ROUX_TRAINERS = {
145
+ fb: () => ({ name: "roux-fb", pieces: FB_PIECES, groups: [[0, 1, 2, 3, 4]], neutral: "x" }),
146
+ /** First square: DL + FL + DLF (front) or DL + BL + DBL (back). */
147
+ fs: (side = "front") => ({ name: `roux-fs-${side}`, pieces: FIRST_SQUARE[side], groups: [[0, 1, 2]], neutral: "x" }),
148
+ /** The first block and the DR edge, from a solved first square (`keep`: which one). */
149
+ fbdr: (keep = "front") => ({
150
+ name: `roux-fbdr-${keep}`,
151
+ pieces: [...FB_PIECES, PIECE.DR],
152
+ groups: [[0, 1, 2, 3, 4], [0, 1, 3, 5], [0, 2, 4, 5]],
153
+ keep: keep === "front" ? [0, 1, 3] : [0, 2, 4],
154
+ }),
155
+ /** Second square: DR + FR + DFR (front) or DR + BR + DRB (back), first block solved. */
156
+ /**
157
+ * EOLR: the six edges oriented and UL / UR brought to where one
158
+ * `[U] M2 [AUF]` (or `M' [U] M2 [AUF]` with the centres off) finishes
159
+ * them — the 16 goal states roux-trainers and act use. M / U only.
160
+ */
161
+ eolr: () => ({
162
+ name: "roux-eolr",
163
+ kind: "lse",
164
+ features: "eolr",
165
+ goals: ["U' M2", "U M2", "M' U M2", "M' U' M2"].flatMap((head) => ["", " U", " U'", " U2"].map((auf) => head + auf)),
166
+ }),
167
+ /** The whole of LSE (M / U), from CMLL solved to a solved cube. */
168
+ lse: () => ({ name: "roux-lse", kind: "lse", features: "lse", goals: [""] }),
169
+ ss: (side = "front") => ({
170
+ name: `roux-ss-${side}`,
171
+ pieces: [...FB_PIECES, PIECE.DR, ...SECOND_SQUARE[side]],
172
+ groups: [[0, 1, 2, 3, 4], [5, 6, 7, 0, 3]],
173
+ keep: [0, 1, 2, 3, 4],
174
+ }),
175
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Things attached to stickers: decals (any PNG / SVG image) and features
3
+ * (extra geometry — holes, charging slots, anything an app defines).
4
+ *
5
+ * Both are chosen with a `StickerSelector` and belong to the STICKER, not to
6
+ * a place on the cube: a charging port on the yellow centre stays on the
7
+ * yellow centre whatever you turn, and turns with it (see core
8
+ * `stickerTurn`). Coordinates are tile-local: x right / y up in the tile's
9
+ * plane as it sat at home, in fractions of the tile's HALF side (so ±1 is
10
+ * the tile's edge), z out of the tile.
11
+ *
12
+ * Skins stay plain data: a skin names a feature `type` and passes `params`;
13
+ * the code that draws it is registered once with `defineFeature` — built-ins
14
+ * below, apps add their own the same way.
15
+ */
16
+ import { type Face } from "../core/index.js";
17
+ import type * as THREE from "three";
18
+ import type { PieceKind } from "./shapes.js";
19
+ /**
20
+ * Which stickers something applies to. Every given condition must hold; an
21
+ * empty selector matches all 54. `faces` are HOME faces, i.e. colour classes:
22
+ * with the western scheme "D" = the yellow stickers.
23
+ */
24
+ export interface StickerSelector {
25
+ faces?: readonly Face[];
26
+ kinds?: readonly PieceKind[];
27
+ /** Explicit home facelet indices (e.g. 4 = the U centre). */
28
+ stickers?: readonly number[];
29
+ }
30
+ export declare function kindOfSticker(sticker: number): PieceKind;
31
+ export declare function selects(selector: StickerSelector, sticker: number): boolean;
32
+ export declare const selectedStickers: (selector: StickerSelector) => number[];
33
+ export interface Decal {
34
+ select: StickerSelector;
35
+ /** URL, data: URL or an SVG string. */
36
+ image: string;
37
+ /** Width and height as a fraction of the tile side. */
38
+ size: number;
39
+ /** Centre offset, tile-local (fractions of the half side). Default [0, 0]. */
40
+ offset?: readonly [number, number];
41
+ /** Extra counter-clockwise quarter turns. Default 0. */
42
+ rotate?: number;
43
+ /** "multiply" lets an image drawn on white sit on any colour. */
44
+ blend?: "normal" | "multiply";
45
+ /** Draw only while the sticker is shown in full colour (not dimmed / greyed by a mask). Default true. */
46
+ onlyRegular?: boolean;
47
+ }
48
+ /**
49
+ * An SVG string as a data: URL; URLs and data: URLs pass through. An SVG
50
+ * without width/height gets 512×512: browsers load such an image with no
51
+ * intrinsic size, which leaves a WebGL texture empty.
52
+ */
53
+ export declare function imageUrl(image: string): string;
54
+ export interface FeatureUse {
55
+ select: StickerSelector;
56
+ /** A type registered with `defineFeature` ("holes", "slot", or your own). */
57
+ type: string;
58
+ params?: Record<string, unknown>;
59
+ }
60
+ export interface FeatureContext {
61
+ /** Tile side in cubie units (tile-local ±1 = ± side / 2). */
62
+ side: number;
63
+ /** Height of the tile top above the cubie face. */
64
+ thickness: number;
65
+ params: Record<string, unknown>;
66
+ }
67
+ export interface FeatureDef {
68
+ type: string;
69
+ /**
70
+ * three.js objects in tile-local 3D units: x/y in the tile plane (tile
71
+ * centre = 0, cubie units — multiply fractions by side / 2), z = 0 on the
72
+ * tile top. `three` is the renderer's three.js module.
73
+ */
74
+ build3d?(ctx: FeatureContext & {
75
+ three: typeof THREE;
76
+ }): THREE.Object3D;
77
+ /** SVG markup in the tile's box: x, y ∈ 0..1, y down, (0.5, 0.5) = tile centre. For @cubecore/image. */
78
+ svg?(ctx: FeatureContext): string;
79
+ }
80
+ /** Register (or replace) a feature type. */
81
+ export declare function defineFeature(def: FeatureDef): FeatureDef;
82
+ export declare const featureDef: (type: string) => FeatureDef | undefined;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Things attached to stickers: decals (any PNG / SVG image) and features
3
+ * (extra geometry — holes, charging slots, anything an app defines).
4
+ *
5
+ * Both are chosen with a `StickerSelector` and belong to the STICKER, not to
6
+ * a place on the cube: a charging port on the yellow centre stays on the
7
+ * yellow centre whatever you turn, and turns with it (see core
8
+ * `stickerTurn`). Coordinates are tile-local: x right / y up in the tile's
9
+ * plane as it sat at home, in fractions of the tile's HALF side (so ±1 is
10
+ * the tile's edge), z out of the tile.
11
+ *
12
+ * Skins stay plain data: a skin names a feature `type` and passes `params`;
13
+ * the code that draws it is registered once with `defineFeature` — built-ins
14
+ * below, apps add their own the same way.
15
+ */
16
+ import { FACES } from "../core/index.js";
17
+ export function kindOfSticker(sticker) {
18
+ const i = sticker % 9;
19
+ return i === 4 ? "center" : i % 2 === 0 ? "corner" : "edge";
20
+ }
21
+ export function selects(selector, sticker) {
22
+ if (selector.faces && !selector.faces.includes(FACES[Math.floor(sticker / 9)]))
23
+ return false;
24
+ if (selector.kinds && !selector.kinds.includes(kindOfSticker(sticker)))
25
+ return false;
26
+ if (selector.stickers && !selector.stickers.includes(sticker))
27
+ return false;
28
+ return true;
29
+ }
30
+ export const selectedStickers = (selector) => Array.from({ length: 54 }, (_, i) => i).filter((i) => selects(selector, i));
31
+ /**
32
+ * An SVG string as a data: URL; URLs and data: URLs pass through. An SVG
33
+ * without width/height gets 512×512: browsers load such an image with no
34
+ * intrinsic size, which leaves a WebGL texture empty.
35
+ */
36
+ export function imageUrl(image) {
37
+ const svg = image.trimStart();
38
+ if (!svg.startsWith("<svg"))
39
+ return image;
40
+ const open = svg.slice(0, svg.indexOf(">") + 1);
41
+ const sized = /\swidth=/.test(open) ? svg : svg.replace("<svg", '<svg width="512" height="512"');
42
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(sized)}`;
43
+ }
44
+ const REGISTRY = new Map();
45
+ /** Register (or replace) a feature type. */
46
+ export function defineFeature(def) {
47
+ REGISTRY.set(def.type, def);
48
+ return def;
49
+ }
50
+ export const featureDef = (type) => REGISTRY.get(type);
51
+ const num = (v, d) => (typeof v === "number" ? v : d);
52
+ const pts = (v, d) => (Array.isArray(v) ? v : d);
53
+ /** Where the shade of a hole / slot is drawn: translucent black darkens any tile colour. */
54
+ function shade(three, opacity) {
55
+ return new three.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity, depthWrite: false });
56
+ }
57
+ /**
58
+ * Round holes. params: `radius` (fraction of the tile side), `at` (tile-local
59
+ * centres, fractions of the half side), `opacity` (0.38).
60
+ */
61
+ defineFeature({
62
+ type: "holes",
63
+ build3d({ three, side, params }) {
64
+ const g = new three.Group();
65
+ const disc = new three.CircleGeometry(num(params.radius, 0.045) * side, 20);
66
+ const mat = shade(three, num(params.opacity, 0.38));
67
+ for (const [x, y] of pts(params.at, [[0.64, 0.64], [-0.64, 0.64], [-0.64, -0.64], [0.64, -0.64]])) {
68
+ const m = new three.Mesh(disc, mat);
69
+ m.position.set((x * side) / 2, (y * side) / 2, 0.001);
70
+ g.add(m);
71
+ }
72
+ return g;
73
+ },
74
+ svg({ params }) {
75
+ const r = num(params.radius, 0.045), o = num(params.opacity, 0.38);
76
+ return pts(params.at, [[0.64, 0.64], [-0.64, 0.64], [-0.64, -0.64], [0.64, -0.64]])
77
+ .map(([x, y]) => `<circle cx="${0.5 + x / 2}" cy="${0.5 - y / 2}" r="${r}" fill="#000" fill-opacity="${o}"/>`)
78
+ .join("");
79
+ },
80
+ });
81
+ /**
82
+ * A rounded slot, e.g. a charging port. params: `width`, `height`, `radius`
83
+ * (fractions of the tile side), `at` (tile-local centre), `opacity` (0.55).
84
+ */
85
+ defineFeature({
86
+ type: "slot",
87
+ build3d({ three, side, params }) {
88
+ const w = num(params.width, 0.3) * side, h = num(params.height, 0.1) * side;
89
+ const r = Math.min(num(params.radius, 0.05) * side, w / 2, h / 2);
90
+ const s = new three.Shape();
91
+ s.moveTo(-w / 2 + r, -h / 2);
92
+ s.lineTo(w / 2 - r, -h / 2);
93
+ s.absarc(w / 2 - r, -h / 2 + r, r, -Math.PI / 2, 0, false);
94
+ s.lineTo(w / 2, h / 2 - r);
95
+ s.absarc(w / 2 - r, h / 2 - r, r, 0, Math.PI / 2, false);
96
+ s.lineTo(-w / 2 + r, h / 2);
97
+ s.absarc(-w / 2 + r, h / 2 - r, r, Math.PI / 2, Math.PI, false);
98
+ s.lineTo(-w / 2, -h / 2 + r);
99
+ s.absarc(-w / 2 + r, -h / 2 + r, r, Math.PI, Math.PI * 1.5, false);
100
+ const m = new three.Mesh(new three.ShapeGeometry(s, 6), shade(three, num(params.opacity, 0.55)));
101
+ const [x, y] = params.at ?? [0, 0];
102
+ m.position.set((x * side) / 2, (y * side) / 2, 0.001);
103
+ return m;
104
+ },
105
+ svg({ params }) {
106
+ const w = num(params.width, 0.3), h = num(params.height, 0.1), r = Math.min(num(params.radius, 0.05), w / 2, h / 2);
107
+ const [x, y] = params.at ?? [0, 0];
108
+ return `<rect x="${0.5 + x / 2 - w / 2}" y="${0.5 - y / 2 - h / 2}" width="${w}" height="${h}" rx="${r}" fill="#000" fill-opacity="${num(params.opacity, 0.55)}"/>`;
109
+ },
110
+ });
@@ -0,0 +1,3 @@
1
+ export * from "./skin.js";
2
+ export * from "./shapes.js";
3
+ export * from "./attachments.js";
package/skin/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./skin.js";
2
+ export * from "./shapes.js";
3
+ export * from "./attachments.js";