-
Notifications
You must be signed in to change notification settings - Fork 22
/
rubiks.tsx
221 lines (197 loc) · 5.95 KB
/
rubiks.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import * as THREE from 'three';
import * as React from 'react';
import { OrbitControls } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
/*
Idea: represent the group as
generated by the 6 permutations on 27 cubelets.
Each cubelet is referenced as a string "ijk"
and a permutation as a dictionary
*/
type Perm = { [key: string]: string }
function cycle(ns: any[]): Perm {
const o: Perm = {}
for (let i = 0; i < ns.length; i++) {
o[ns[i]] = ns[(i + 1) % ns.length]
}
return o
}
function apply(p: Perm, k: string) {
return p[k] ?? k
}
function compose(p1: Perm, p2: Perm) {
const o: Perm = { ...p2 }
for (const k1 in p1) {
o[k1] = apply(p2, p1[k1])
}
return o
}
function invert(p: Perm): Perm {
const o: Perm = {}
for (const k in p) {
o[p[k]] = k
}
return o
}
const colors = ['red', 'snow', 'darkorange', 'yellow', 'green', 'blue']
// action of 90° rotation on 3×3 grid
const R2 = compose(
cycle(["00", "20", "22", "02"]),
cycle(["01", "10", "21", "12"])
)
/** Converts a permutation on a 2D grid to a permutation
* on a 3D cube by adding an extra axis to the permutation values.
*
* ### Parameters
* - `p2d : Perm` is the 2D permutation
* - `newaxis : number` is the axis to introduce. That is, `0` is the X-axis, `1` is the Y-axis, and `2` is the Z-axis.
* - `axisvals : string[]` are the values to insert at `newaxis`. So for example `inject(R2, 1, ["0","1","2"])` corresponds to rotating
* the entire Rubik's cube around the Y-axis.
* `inject(R2, 1, ["0"])` rotates just the top layer around the Y-axis.
*
* ### Example:
* ```ts
* inject(cycle(["00", "20", "22", "02"]), 1, [1])
* ≡ cycle(["010", "210", "212", "012"])
*
* inject(cycle(["00", "20", "22", "02"]), 0, [1])
* ≡ cycle(["100", "120", "122", "102"])
* ```
*/
function inject(p2d: Perm, newaxis: number, axisvals: string[]) {
function ins(x: string, v: string) {
const xs = x.split("")
xs.splice(newaxis, 0, v)
return xs.join("")
}
const o: Perm = {}
for (const k2d in p2d) {
const v2d = p2d[k2d]
for (const v of axisvals) {
o[ins(k2d, v)] = ins(v2d, v)
}
}
return o
}
type genstr =
| "U" | "D" | "L" | "R" | "F" | "B"
| "U⁻¹" | "D⁻¹" | "L⁻¹" | "R⁻¹" | "F⁻¹" | "B⁻¹"
const generators: { [k in genstr]: Perm } = {
U: inject(R2, 0, ["2"]),
D: inject(R2, 0, ["0"]),
L: inject(R2, 1, ["2"]),
R: inject(R2, 1, ["0"]),
F: inject(R2, 2, ["2"]),
B: inject(R2, 2, ["0"]),
} as any
for (const k of Object.getOwnPropertyNames(generators)) {
// @ts-ignore
generators[`${k}⁻¹`] = invert(generators[k])
}
function generatorToRotation(generator: string, cubelet: string, time = 1.0): THREE.Matrix4 {
if (generator.includes("⁻¹")) {
return generatorToRotation(generator.split("⁻¹")[0], cubelet, time).invert()
}
const θ = Math.PI * 0.5 * time
if (generator == "U" && cubelet[0] == "2") {
return new THREE.Matrix4().makeRotationX(θ)
}
if (generator == "D" && cubelet[0] == "0") {
return new THREE.Matrix4().makeRotationX(θ)
}
if (generator == "L" && cubelet[1] == "2") {
return new THREE.Matrix4().makeRotationY(- θ)
}
if (generator == "R" && cubelet[1] == "0") {
return new THREE.Matrix4().makeRotationY(- θ)
}
if (generator == "F" && cubelet[2] == "2") {
return new THREE.Matrix4().makeRotationZ(θ)
}
if (generator == "B" && cubelet[2] == "0") {
return new THREE.Matrix4().makeRotationZ(θ)
}
console.warn(`Invalid generator ${generator}. Skipping.`)
return new THREE.Matrix4()
}
function clamp(number: number, min = 0, max = 1) {
return Math.max(min, Math.min(number, max));
}
function elementToRotation(seq: genstr[], cubelet: string, time = 1.0): THREE.Matrix4 {
const pos: [number, number, number] = cubelet.split("").map(x => (Number(x) - 1) * (1.0 + 0.1)) as any
const trans = new THREE.Matrix4().makeTranslation(...pos)
const m = new THREE.Matrix4()
let p = {}
for (let i = 0; i < seq.length; i++) {
if (i > time * seq.length) {
break
}
const g : genstr = seq[i]
m.premultiply(generatorToRotation(g, apply(p, cubelet), clamp((time * seq.length) - i)))
p = compose(p, generators[g] ?? {})
}
m.multiply(trans)
return m
}
function* prod(...iters: any[]): Generator<any[]> {
if (iters.length === 0) {
yield []
return
}
let [xs, ...rest] = iters // [fixme] need to Tee the iters.
for (let x of xs) {
for (let ys of prod(...rest)) {
yield [x, ...ys]
}
}
}
const cubelets = [...prod([0, 1, 2], [0, 1, 2], [0, 1, 2])].map(x => x.join(""))
interface CubeletProps {
time: number;
seq: genstr[];
cid: string;
}
function Cubelet(props: CubeletProps) {
const me = React.useRef<THREE.Mesh>()
React.useEffect(() => {
const m = elementToRotation(props.seq, props.cid, props.time ?? 1.0)
if (me.current) {
me.current.setRotationFromMatrix(m)
me.current.position.setFromMatrixPosition(m)
}
}, [props.cid, props.time, props.seq])
return (
// @ts-ignore
<mesh ref={me}>
<boxGeometry args={[1, 1, 1]} />
{colors.map((col, idx) => (
<meshPhongMaterial key={idx} attach={`material-${idx}`} color={col} />
))}
</mesh>
)
}
interface CubeProps {
time: number;
seq: genstr[]
}
function Cube(props: CubeProps) {
return <group>
{cubelets.map(cubelet => <Cubelet key={cubelet} cid={cubelet} time={props.time} seq={props.seq} />)}
</group>
}
export default function (props: any) {
const seq = props.seq ?? []
const [t, setT] = React.useState(100)
return <div style={{ height: 300 }}>
<input type="range" min="0" max="100" value={t} onChange={e => setT(e.target.value as any)} />
<div>Sequence: {JSON.stringify(seq)}</div>
<Canvas >
<pointLight position={[150, 150, 150]} intensity={0.55} />
<ambientLight color={0xffffff} />
<group rotation-x={Math.PI * 0.25} rotation-y={Math.PI * 0.25}>
<Cube seq={seq} time={t / 100} />
</group>
<OrbitControls />
</Canvas>
</div>
}