Commit | Line | Data |
---|---|---|
295b2492 MW |
1 | #! /usr/bin/python |
2 | ### -*- coding: utf-8 -*- | |
3 | ### | |
4 | ### Rubik cube model and rendering | |
5 | ### | |
6 | ### (c) 2018 Mark Wooding | |
7 | ### | |
8 | ||
9 | ###----- Licensing notice --------------------------------------------------- | |
10 | ### | |
11 | ### This program is free software; you can redistribute it and/or modify | |
12 | ### it under the terms of the GNU General Public License as published by | |
13 | ### the Free Software Foundation; either version 2 of the License, or | |
14 | ### (at your option) any later version. | |
15 | ### | |
16 | ### This program is distributed in the hope that it will be useful, | |
17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 | ### GNU General Public License for more details. | |
20 | ### | |
21 | ### You should have received a copy of the GNU General Public License | |
22 | ### along with this program; if not, write to the Free Software Foundation, | |
23 | ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
24 | ||
25 | ###-------------------------------------------------------------------------- | |
26 | ### External dependencies. | |
27 | ||
28 | import math as M; TAU = 2*M.pi | |
29 | import os as OS | |
30 | import sys as SYS | |
31 | ||
32 | import cairo as CR | |
33 | import numpy as NP | |
34 | ||
35 | ###-------------------------------------------------------------------------- | |
36 | ### The main cube model. | |
37 | ||
38 | ## Names for the faces and corresponding colours. | |
39 | U, D, L, R, F, B, UNK = 0, 1, 2, 3, 4, 5, 6 | |
40 | FACE = ['U', 'D', 'L', 'R', 'F', 'B'] | |
41 | ||
42 | ## Number of faces. | |
43 | NFACE = 6 | |
44 | ||
45 | ## Colours of the faces. | |
46 | COLOUR = ['#', '+', '%', '/', '=', '-', 'ยท'] | |
47 | RGB = [(1, 0, 0), (1, 0.75, 0), | |
48 | (1, 1, 0), (1, 1, 1), | |
49 | (0, 0, 1), (0, 1, 0), | |
50 | (0.4, 0.4, 0.4)] | |
51 | CMAP = { 'r': U, 'o': D, 'y': L, 'w': R, 'b': F, 'g': B, '?': UNK } | |
52 | ||
53 | ## Geometry table. | |
54 | ## | |
55 | ## For 0 <= k < 6 and 0 <= d < 4, GEOM[k][d] holds a triple (k', ix, iy) with | |
56 | ## the following meaning. If you start at face k, move in the direction d | |
57 | ## (up, down, left, or right), and treat the edge that you crossed as the x | |
58 | ## axis of a right-handed coordinate system, increasing clockwise, then you | |
59 | ## are on face k', and (ix, iy) is a unit vector in the coordinate system of | |
60 | ## the state array pointing in the positive x direction. | |
61 | GEOM = [[(B, -1, 0), (F, -1, 0), (L, -1, 0), (R, -1, 0)], | |
62 | [(F, +1, 0), (B, +1, 0), (L, +1, 0), (R, +1, 0)], | |
63 | [(U, 0, -1), (D, 0, -1), (B, 0, +1), (F, 0, -1)], | |
64 | [(U, 0, +1), (D, 0, +1), (F, 0, +1), (B, 0, -1)], | |
65 | [(U, +1, 0), (D, -1, 0), (L, 0, +1), (R, 0, -1)], | |
66 | [(U, -1, 0), (D, +1, 0), (R, 0, +1), (L, 0, -1)]] | |
67 | ||
68 | def convert_side_coords(w, orig, face, x, y): | |
69 | """ | |
70 | Convert relative coordinates (X, Y) on a side face into raw coordinates. | |
71 | ||
72 | Suppose we start on face ORIG and move to the adjacent FACE (one of `U', | |
73 | `D', `L', `R'), treating the edge we crossed as the x-axis of a right-hand | |
74 | coordinate system, increasing clockwise; return a triple (K, U, V), such | |
75 | that m[K][U][V] identifies the slot in the model corresponding to the given | |
76 | coordinates. The argument W is the side length of the cube. | |
77 | """ | |
78 | k, ix, iy = GEOM[orig][face] | |
79 | jx, jy = -iy, ix | |
80 | if ix < 0 or jx < 0: ox = w - 1 | |
81 | else: ox = 0 | |
82 | if iy < 0 or jy < 0: oy = w - 1 | |
83 | else: oy = 0 | |
84 | return k, ox + x*ix + y*jx, oy + x*iy + y*jy | |
85 | ||
86 | def slice_orbits(w, face, dp): | |
87 | """ | |
88 | Return the effect of a slice turn on the side stickers. | |
89 | ||
90 | Returns a list of lists of (K, X, Y) model coordinates describing the cycle | |
91 | decomposition of the permutation induced on the side stickers of a | |
92 | clockwise turn of the DPth slice of a size-W cube as viewed from FACE. | |
93 | """ | |
94 | oo = [] | |
95 | for x in xrange(w): | |
96 | oo.append([convert_side_coords(w, face, k, x, dp) | |
97 | for k in [U, R, D, L]]) | |
98 | return oo | |
99 | ||
100 | def face_orbits(w, face, inv): | |
101 | """ | |
102 | Return the effect of a face turn on the face's stickers. | |
103 | ||
104 | Returns a list of lists of (K, X, Y) model coordinates describing the cycle | |
105 | decomposition of the permutation induced on the face stickers of a | |
106 | clockwise (or anticlockwise, if INV is true) turn of a FACE of a size-W | |
107 | cube. | |
108 | """ | |
109 | if inv: ta, tb, tc, td, dx, dy = 0, -1, 1, 0, w - 1, 0 | |
110 | else: ta, tb, tc, td, dx, dy = 0, 1, -1, 0, 0, w - 1 | |
111 | oo = [] | |
112 | for x in xrange((w + 1)//2, w): | |
113 | for y in xrange(w//2, x + 1): | |
114 | o = [] | |
115 | u, v = x, y | |
116 | for i in xrange(4): | |
117 | o.append((face, u, v)) | |
118 | u, v = ta*u + tb*v + dx, tc*u + td*v + dy | |
119 | oo.append(o) | |
120 | return oo | |
121 | ||
122 | def orbits(w, face, dp): | |
123 | """ | |
124 | Return the effect of a slice turn on the cube's stickers. | |
125 | ||
126 | Returns a list of lists of (K, X, Y) model coordinates describing the cycle | |
127 | decompositon of the permutation induced on the cube's stickers of a | |
128 | clockwise turn of the DPth slice of a size-W cube, as viewed from FACE. | |
129 | """ | |
130 | oo = slice_orbits(w, face, dp) | |
131 | if dp == 0: oo += face_orbits(w, face, False) | |
132 | elif dp == w - 1: oo += face_orbits(w, face ^ 1, True) | |
133 | return oo | |
134 | ||
135 | def maybe_parens(parens, s): | |
136 | """If S, then return the string `(S)'; otherwise just return S.""" | |
137 | if parens: return '(' + s + ')' | |
138 | else: return s | |
139 | ||
140 | class BaseMove (object): | |
141 | """ | |
142 | Base class for move objects. | |
143 | ||
144 | Subclasses must implement the following methods. | |
145 | ||
146 | * apply(PUZ): apply the move to a puzzle PUZ | |
147 | * invert(): return the inverse of the move | |
148 | * _str(lv): return a string representation, in a priority-LV context | |
149 | """ | |
150 | def repeat(me, m): | |
151 | """Return the move repeated M times.""" | |
152 | if m == 0: return NULLMOVE | |
153 | else: return RepeatedMove(me, m) | |
154 | def flatten(me): | |
155 | """Return a list of constituent moves.""" | |
156 | return [me] | |
157 | def __str__(me): | |
158 | """Return a string representation of the move.""" | |
159 | return me._str(9) | |
160 | ||
161 | class NullMove (BaseMove): | |
162 | """The null move, which does nothing at all.""" | |
163 | def apply(me, puz): | |
164 | """Apply the move to the puzzle PUZ.""" | |
165 | pass | |
166 | def invert(me): | |
167 | """Return the inverse of the move.""" | |
168 | return me | |
169 | def repeat(me, m): | |
170 | """Return the move repeated M times.""" | |
171 | return me | |
172 | def flatten(me): | |
173 | """Return a list of constituent moves.""" | |
174 | return [] | |
175 | def _str(me, lv): | |
176 | """Return a string representation, in a priority-LV context.""" | |
177 | return '#<null>' | |
178 | NULLMOVE = NullMove() | |
179 | ||
180 | class AtomicMove (BaseMove): | |
181 | """A turn of a single slice.""" | |
182 | ||
183 | MAP = { } | |
184 | for k in xrange(NFACE): | |
185 | MAP[FACE[k]] = (k, 0) | |
186 | MAP[FACE[k].lower()] = (k, 1) | |
187 | ||
188 | def __init__(me, face, dp = 0, n = 1): | |
189 | """ | |
190 | Construct an atomic move. | |
191 | ||
192 | Returns a move representing N clockwise turns of the DPth slice, as | |
193 | viewed from FACE. | |
194 | """ | |
195 | me.face = face | |
196 | me.dp = dp | |
197 | me.n = n | |
198 | def apply(me, puz): | |
199 | """Apply the move to the puzzle PUZ.""" | |
200 | if me.dp >= 0: puz.turn(me.face, me.dp, me.n) | |
201 | else: puz.turn_multi(me.face, -me.dp, me.n) | |
202 | def invert(me): | |
203 | """Return the inverse of the move.""" | |
204 | return AtomicMove(me.face, me.dp, -me.n) | |
205 | def repeat(me, m): | |
206 | """Return the move repeated M times.""" | |
207 | mm = (me.n*m)%4 | |
208 | if not mm: return NULLMOVE | |
209 | else: return AtomicMove(me.face, me.dp, mm) | |
210 | def _str(me, lv): | |
211 | """Return a string representation, in a priority-LV context.""" | |
212 | f = FACE[me.face] | |
213 | if me.n%4 == 0: d = '0' | |
214 | elif me.n%4 == 1: d = '' | |
215 | elif me.n%4 == 2: d = '2' | |
216 | elif me.n%4 == 3: d = "'" | |
217 | if me.dp == 0: return f + d | |
218 | elif me.dp == 1: return f.lower() + d | |
219 | elif me.dp < 0: return '%s%s_%d' % (f, d, -me.dp) | |
220 | ||
221 | class SpinMove (BaseMove): | |
222 | """A spin of the entire cube.""" | |
223 | MAP = { 'X': R, 'Y': U, 'Z': F } | |
224 | UNMAP = { } | |
225 | for k, v in MAP.iteritems(): UNMAP[v] = k | |
226 | ||
227 | def __init__(me, face, n = 1): | |
228 | """Construct a spin move. | |
229 | ||
230 | Returns a move representing N clockwise spins the entire cube DPth slice, | |
231 | as viewed from FACE. | |
232 | """ | |
233 | me.face = face | |
234 | me.n = n | |
235 | def apply(me, puz): | |
236 | """Apply the move to the puzzle PUZ.""" | |
237 | puz.turn_multi(me.face, puz.w, me.n) | |
238 | def invert(me): | |
239 | """Return the inverse of the move.""" | |
240 | return SpinMove(me.face, -me.n) | |
241 | def repeat(me, m): | |
242 | """Return the move repeated M times.""" | |
243 | mm = (me.n*m)%4 | |
244 | if not mm: return NULLMOVE | |
245 | else: return SpinMove(me.face, mm) | |
246 | def _str(me, lv): | |
247 | """Return a string representation, in a priority-LV context.""" | |
248 | f = me.MAP[me.face] | |
249 | if me.n%4 == 0: d = '0' | |
250 | elif me.n%4 == 1: d = '' | |
251 | elif me.n%4 == 2: d = '2' | |
252 | elif me.n%4 == 3: d = "'" | |
253 | return f + d | |
254 | ||
255 | class MoveSequence (BaseMove): | |
256 | """A sequence of moves, applied in left-to-right order.""" | |
257 | def __init__(me, moves): | |
258 | """Construct a sequence of moves from the list MOVES.""" | |
259 | me.moves = reduce(lambda x, y: x + y, [mv.flatten() for mv in moves]) | |
260 | def apply(me, puz): | |
261 | """Apply the move to the puzzle PUZ.""" | |
262 | for mv in me.moves: mv.apply(puz) | |
263 | def invert(me): | |
264 | """Return the inverse of the move.""" | |
265 | return MoveSequence(list(reversed([mv.invert() for mv in me.moves]))) | |
266 | def _str(me, lv): | |
267 | """Return a string representation, in a priority-LV context.""" | |
268 | return maybe_parens(lv < 5, ' '.join([mv._str(5) for mv in me.moves])) | |
269 | ||
270 | class RepeatedMove (BaseMove): | |
271 | """A move applied multiple times.""" | |
272 | def __init__(me, move, m): | |
273 | """Construct the move which consists of MOVE repeated M times.""" | |
274 | me.move = move | |
275 | me.m = m | |
276 | def apply(me, puz): | |
277 | """Apply the move to the puzzle PUZ.""" | |
278 | for i in xrange(me.m): me.move.apply(puz) | |
279 | def invert(me): | |
280 | """Return the inverse of the move.""" | |
281 | return RepeatedMove(me.move.invert(), me.m) | |
282 | def repeat(me, m): | |
283 | """Return the move repeated M times.""" | |
284 | if m == 0: return NULLMOVE | |
285 | else: return RepeatedMove(me.move, m*me.m) | |
286 | def _str(me, lv): | |
287 | """Return a string representation, in a priority-LV context.""" | |
288 | return maybe_parens(lv < 3, '%s%d' % (me.move._str(2), me.m)) | |
289 | ||
290 | def skip_spaces(s, i): | |
291 | """Return the smallest index J >= I such that S[J] is not whitespace.""" | |
292 | while i < len(s) and s[i].isspace(): i += 1 | |
293 | return i | |
294 | ||
295 | def parse_number(s, i): | |
296 | """ | |
297 | Parse an integer from S, starting at index I. | |
298 | ||
299 | The substring S[I:] consists of zero or more digits, optionally followed by | |
300 | a nondigit character and any other characters. Convert the digit sequence | |
301 | into an integer N and return the pair (J, N), where J is the index of the | |
302 | first non-whitespace character following the digit sequence, or the length | |
303 | of S if there is no such character. | |
304 | """ | |
305 | n = 0 | |
306 | while i < len(s) and s[i].isdigit(): | |
307 | n = 10*n + ord(s[i]) - ord('0') | |
308 | i += 1 | |
309 | i = skip_spaces(s, i) | |
310 | return i, n | |
311 | ||
312 | def parse_parens(s, i, delim): | |
313 | i = skip_spaces(s, i) | |
314 | i, mv = parse_conjugate(s, i) | |
315 | if s[i] != delim: raise SyntaxError('missing %s' % delim) | |
316 | i = skip_spaces(s, i + 1) | |
317 | return i, mv | |
318 | ||
319 | def parse_primary(s, i): | |
320 | if i >= len(s): return i, None | |
321 | elif s[i] == '(': return parse_parens(s, i + 1, ')') | |
322 | elif s[i] == '[': return parse_parens(s, i + 1, ']') | |
323 | elif s[i] in SpinMove.MAP: | |
324 | k = SpinMove.MAP[s[i]] | |
325 | i = skip_spaces(s, i + 1) | |
326 | return i, SpinMove(k, 1) | |
327 | elif s[i] in AtomicMove.MAP: | |
328 | k, dp = AtomicMove.MAP[s[i]] | |
329 | if dp == 0 and i + 2 < len(s) and s[i + 1] == '_' and s[i + 2].isdigit(): | |
330 | i, n = parse_number(s, i + 2) | |
331 | if n == 1: dp = 0 | |
332 | else: dp = -n | |
333 | else: | |
334 | i = skip_spaces(s, i + 1) | |
335 | mv = AtomicMove(k, dp, 1) | |
336 | return i, mv | |
337 | else: return i, None | |
338 | ||
339 | def parse_move(s, i): | |
340 | i, mv = parse_primary(s, i) | |
341 | if mv is None: return i, None | |
342 | while True: | |
343 | if i < len(s) and s[i] == "'": | |
344 | i = skip_spaces(s, i + 1) | |
345 | mv = mv.invert() | |
346 | elif i < len(s) and s[i].isdigit(): | |
347 | i, m = parse_number(s, i) | |
348 | mv = mv.repeat(m) | |
349 | elif i + 1 < len(s) and s[i] == '^' and s[i + 1].isdigit(): | |
350 | i, m = parse_number(s, i + 1) | |
351 | mv = mv.repeat(m) | |
352 | else: | |
353 | break | |
354 | return i, mv | |
355 | ||
356 | def parse_sequence(s, i): | |
357 | seq = [] | |
358 | while True: | |
359 | i, mv = parse_move(s, i) | |
360 | if mv is None: break | |
361 | seq.append(mv) | |
362 | if len(seq) == 0: raise SyntaxError('unrecognized move') | |
363 | elif len(seq) == 1: return i, seq[0] | |
364 | else: return i, MoveSequence(seq) | |
365 | ||
366 | def parse_commutator(s, i): | |
367 | i, mv = parse_sequence(s, i) | |
368 | if i < len(s) and s[i] == ';': | |
369 | i = skip_spaces(s, i + 1) | |
370 | i, mv1 = parse_sequence(s, i) | |
371 | mv = MoveSequence([mv, mv1, mv.invert(), mv1.invert()]) | |
372 | return i, mv | |
373 | ||
374 | def parse_conjugate(s, i): | |
375 | i, mv = parse_commutator(s, i) | |
376 | if i < len(s) and (s[i] == '.' or s[i] == '*'): | |
377 | i = skip_spaces(s, i + 1) | |
378 | i, mv1 = parse_commutator(s, i) | |
379 | mv = MoveSequence([mv, mv1, mv.invert()]) | |
380 | return i, mv | |
381 | ||
382 | def parse_moves(s, i = 0): | |
383 | i = skip_spaces(s, i) | |
384 | i, mv = parse_conjugate(s, i) | |
385 | i = skip_spaces(s, i) | |
386 | if i < len(s): raise SyntaxError('junk at eol') | |
387 | return mv | |
388 | ||
389 | def xltmatrix(dx, dy, dz): | |
390 | return NP.matrix([[1, 0, 0, dx], | |
391 | [0, 1, 0, dy], | |
392 | [0, 0, 1, dz], | |
393 | [0, 0, 0, 1]], NP.float64) | |
394 | ||
395 | def rotmatrix(theta, axis): | |
396 | R = NP.matrix(NP.identity(4, NP.float64)) | |
397 | i, j = 0, 1 | |
398 | if i >= axis: i += 1 | |
399 | if j >= axis: j += 1 | |
400 | c, s = M.cos(theta), M.sin(theta) | |
401 | R[i, i] = R[j, j] = c | |
402 | R[i, j] = s; R[j, i] = -s | |
403 | return R | |
404 | ||
405 | def scalematrix(sx, sy, sz): | |
406 | return NP.matrix([[sx, 0, 0, 0], | |
407 | [0, sy, 0, 0], | |
408 | [0, 0, sz, 0], | |
409 | [0, 0, 0, 1]], NP.float64) | |
410 | ||
411 | class Transform (object): | |
412 | def __init__(me, theta_x, theta_y, delta_z): | |
413 | me.theta_x = theta_x | |
414 | me.theta_y = theta_y | |
415 | me.delta_z = delta_z | |
416 | me._T = xltmatrix(0, 0, delta_z)*\ | |
417 | rotmatrix(theta_x, 0)*rotmatrix(theta_y, 1) | |
418 | def project(me, x, y, z): | |
419 | u, v, w, _ = (me._T*NP.array([x, y, z, 1])[:, NP.newaxis]).A1 | |
420 | return me.delta_z*u/w, me.delta_z*v/w | |
421 | ||
422 | def vector(x, y, z): | |
423 | return NP.array([x, y, z], NP.float64) | |
424 | ||
425 | def unit_vector(v): | |
426 | mag = M.sqrt(NP.dot(v, v)) | |
427 | return v/mag | |
428 | ||
429 | def shorten(p, q, delta): | |
430 | u = unit_vector(q - p) | |
431 | return p + delta*u, q - delta*u | |
432 | ||
433 | def along(f, p, q): | |
434 | return p + f*(q - p) | |
435 | ||
436 | class BasePainter (object): | |
437 | def __init__(me, xf, cr): | |
438 | me._cr = cr | |
439 | me._xf = xf | |
440 | dx, _ = me._cr.device_to_user_distance(0.4, 0) | |
441 | me._cr.set_line_width(dx) | |
442 | def set_colour(me, r, g, b): | |
443 | me._cr.set_source_rgb(r, g, b) | |
444 | return me | |
445 | def path(me, pts): | |
446 | firstp = True | |
447 | for x, y, z in pts: | |
448 | u, v = me._xf.project(x, y, z) | |
449 | if firstp: me._cr.move_to(u, v) | |
450 | else: me._cr.line_to(u, v) | |
451 | firstp = False | |
452 | return me | |
453 | def _rounded_path_corner(me, r, q1, p1, p2): | |
454 | q2, q3 = shorten(p1, p2, r) | |
455 | c1, c2 = along(0.7, q1, p1), along(0.7, q2, p1) | |
456 | x0, y0 = me._xf.project(*c1) | |
457 | x1, y1 = me._xf.project(*c2) | |
458 | x2, y2 = me._xf.project(*q2) | |
459 | me._cr.curve_to(x0, y0, x1, y1, x2, y2) | |
460 | return p2, q3 | |
461 | def _rounded_path_step(me, r, q1, p1, p2): | |
462 | p2, q3 = me._rounded_path_corner(r, q1, p1, p2) | |
463 | x3, y3 = me._xf.project(*q3) | |
464 | me._cr.line_to(x3, y3) | |
465 | return p2, q3 | |
466 | def rounded_polygon(me, r, pts): | |
467 | o0 = o1 = None | |
468 | for x, y, z in pts: | |
469 | p2 = vector(x, y, z) | |
470 | if o0 is None: o0 = p2 | |
471 | elif o1 is None: | |
472 | o1 = p1 = p2 | |
473 | q0, q1 = shorten(o0, p1, r) | |
474 | x0, y0 = me._xf.project(*q1) | |
475 | me._cr.move_to(x0, y0) | |
476 | else: p1, q1 = me._rounded_path_step(r, q1, p1, p2) | |
477 | p1, q1 = me._rounded_path_step(r, q1, p1, o0) | |
478 | p1, q1 = me._rounded_path_corner(r, q1, p1, o1) | |
479 | me._cr.close_path() | |
480 | return me | |
481 | def polygon(me, pts): | |
482 | me.path(pts) | |
483 | me._cr.close_path() | |
484 | return me | |
485 | ||
486 | class Painter (BasePainter): | |
487 | def stroke(me): | |
488 | me._cr.stroke() | |
489 | return me | |
490 | def fill(me): | |
491 | me._cr.fill() | |
492 | return me | |
493 | ||
494 | class MeasuringPainter (BasePainter): | |
495 | def __init__(me, *args, **kw): | |
496 | super(MeasuringPainter, me).__init__(*args, **kw) | |
497 | me.x0 = me.y0 = me.x1 = me.y1 = None | |
498 | def _update_bbox(me, x0, y0, x1, y1): | |
499 | if me.x0 is None or me.x0 > x0: me.x0 = x0 | |
500 | if me.y0 is None or me.y0 > y0: me.y0 = y0 | |
501 | if me.x1 is None or me.x1 < x1: me.x1 = x1 | |
502 | if me.y1 is None or me.y1 < y1: me.y1 = y1 | |
503 | def stroke(me): | |
504 | x0, y0, x1, y1 = me._cr.stroke_extents() | |
505 | me._cr.new_path() | |
506 | me._update_bbox(x0, y0, x1, y1) | |
507 | return me | |
508 | def fill(me): | |
509 | x0, y0, x1, y1 = me._cr.fill_extents() | |
510 | me._cr.new_path() | |
511 | me._update_bbox(x0, y0, x1, y1) | |
512 | return me | |
513 | ||
514 | class Puzzle (object): | |
515 | ||
516 | def __init__(me, w): | |
517 | me.w = w | |
518 | me.m = NP.ndarray((NFACE, w, w), NP.uint8) | |
519 | ||
520 | for k in xrange(NFACE): | |
521 | for y in reversed(xrange(w)): | |
522 | for x in xrange(w): | |
523 | me.m[k, x, y] = k | |
524 | ||
525 | me.orbits = [[orbits(w, k, dp) for dp in xrange(w)] | |
526 | for k in xrange(NFACE)] | |
527 | ||
528 | def turn(me, face, dp, n): | |
529 | for o in me.orbits[face][dp]: | |
530 | c = [me.m[k, x, y] for k, x, y in o] | |
531 | for i in xrange(4): | |
532 | k, x, y = o[(i + n)%4] | |
533 | me.m[k, x, y] = c[i] | |
534 | return me | |
535 | ||
536 | def turn_multi(me, face, dp, n): | |
537 | for i in xrange(dp): me.turn(face, i, n) | |
538 | ||
539 | def fill_face(me, s, i, k, ox, oy, ix, iy, jx, jy): | |
540 | i = skip_spaces(s, i) | |
541 | if i >= len(s): return i | |
542 | firstp = True | |
543 | for y in xrange(me.w): | |
544 | if firstp: pass | |
545 | elif i >= len(s) or s[i] != ',': raise SyntaxError('missing ,') | |
546 | else: i += 1 | |
547 | for x in xrange(me.w): | |
548 | if i >= len(s): raise SyntaxError('unexpected eof') | |
549 | c = CMAP[s[i]] | |
550 | me.m[k, ox + x*ix + y*jx, oy + x*iy + y*jy] = c | |
551 | i += 1 | |
552 | firstp = False | |
553 | if i < len(s) and not s[i].isspace(): | |
554 | raise SyntaxError('face spec too long') | |
555 | return i | |
556 | ||
557 | def fill(me, s): | |
558 | i = 0 | |
559 | me.m[:, :, :] = UNK | |
560 | i = me.fill_face(s, i, U, 0, 2, +1, 0, 0, -1) | |
561 | i = me.fill_face(s, i, F, 0, 2, +1, 0, 0, -1) | |
562 | i = me.fill_face(s, i, R, 0, 2, +1, 0, 0, -1) | |
563 | i = me.fill_face(s, i, B, 2, 2, -1, 0, 0, -1) | |
564 | i = me.fill_face(s, i, L, 2, 2, -1, 0, 0, -1) | |
565 | i = me.fill_face(s, i, D, 0, 0, +1, 0, 0, +1) | |
566 | i = skip_spaces(s, i) | |
567 | if i < len(s): raise SyntaxError('trailing junk') | |
568 | return me | |
569 | ||
570 | def show(me): | |
571 | for y in reversed(xrange(me.w)): | |
572 | print (2*me.w + 2)*' ' + \ | |
573 | ' '.join([COLOUR[me.m[U, x, y]] | |
574 | for x in xrange(me.w)]) | |
575 | ||
576 | for y in reversed(xrange(me.w)): | |
577 | print ' '.join([' '.join([COLOUR[me.m[k, x, y]] | |
578 | for x in xrange(me.w)]) | |
579 | for k in [L, F, R, B]]) | |
580 | ||
581 | for y in reversed(xrange(me.w)): | |
582 | print (2*me.w + 2)*' ' + \ | |
583 | ' '.join([COLOUR[me.m[D, x, y]] | |
584 | for x in xrange(me.w)]) | |
585 | ||
586 | ||
587 | def render_face(me, paint, k, ox, oy, oz, ix, iy, iz, jx, jy, jz): | |
588 | w = 2.0/me.w | |
589 | g = w/16.0 | |
590 | for x in xrange(me.w): | |
591 | for y in xrange(me.w): | |
592 | paint.set_colour(*RGB[me.m[k, x, y]]) | |
593 | paint.rounded_polygon(0.04, | |
594 | [(ox + (x*w + g)*ix + (y*w + g)*jx, | |
595 | oy + (x*w + g)*iy + (y*w + g)*jy, | |
596 | oz + (x*w + g)*iz + (y*w + g)*jz), | |
597 | (ox + ((x + 1)*w - g)*ix + (y*w + g)*jx, | |
598 | oy + ((x + 1)*w - g)*iy + (y*w + g)*jy, | |
599 | oz + ((x + 1)*w - g)*iz + (y*w + g)*jz), | |
600 | (ox + ((x + 1)*w - g)*ix + ((y + 1)*w - g)*jx, | |
601 | oy + ((x + 1)*w - g)*iy + ((y + 1)*w - g)*jy, | |
602 | oz + ((x + 1)*w - g)*iz + ((y + 1)*w - g)*jz), | |
603 | (ox + (x*w + g)*ix + ((y + 1)*w - g)*jx, | |
604 | oy + (x*w + g)*iy + ((y + 1)*w - g)*jy, | |
605 | oz + (x*w + g)*iz + ((y + 1)*w - g)*jz)]).fill() | |
606 | ||
607 | def render(me, paint, reflectp): | |
608 | ||
609 | if reflectp: | |
610 | paint.set_colour(0, 0, 0) | |
611 | paint.rounded_polygon(0.04, [(-1, +1, +4), (+1, +1, +4), | |
612 | (+1, -1, +4), (-1, -1, +4)]).fill() | |
613 | paint.rounded_polygon(0.04, [(-1, -4, +1), (+1, -4, +1), | |
614 | (+1, -4, -1), (-1, -4, -1)]).fill() | |
615 | paint.rounded_polygon(0.04, [(-4, -1, +1), (-4, +1, +1), | |
616 | (-4, +1, -1), (-4, -1, -1)]).fill() | |
617 | me.render_face(paint, D, -1, -4, +1, +1, 0, 0, 0, 0, -1) | |
618 | me.render_face(paint, B, +1, -1, +4, -1, 0, 0, 0, +1, 0) | |
619 | me.render_face(paint, L, -4, -1, +1, 0, 0, -1, 0, +1, 0) | |
620 | ||
621 | paint.set_colour(0, 0, 0) | |
622 | paint.rounded_polygon(0.04, [(-1, +1, -1), (-1, +1, +1), | |
623 | (+1, +1, +1), (+1, -1, +1), | |
624 | (+1, -1, -1), (-1, -1, -1)]).fill() | |
625 | ||
626 | ##paint.set_colour(0.3, 0.3, 0.3) | |
627 | ##paint.path([(-1, +1, -1), (+1, +1, -1), (+1, +1, +1)]).stroke() | |
628 | ##paint.path([(+1, +1, -1), (+1, -1, -1)]).stroke() | |
629 | ||
630 | me.render_face(paint, U, -1, +1, -1, +1, 0, 0, 0, 0, +1) | |
631 | me.render_face(paint, F, -1, -1, -1, +1, 0, 0, 0, +1, 0) | |
632 | me.render_face(paint, R, +1, -1, -1, 0, 0, +1, 0, +1, 0) | |
633 | ||
634 | st = Puzzle(3) | |
635 | invertp = False | |
636 | reflectp = True | |
637 | scale = 32 | |
638 | for a in SYS.argv[1:]: | |
639 | if not a: pass | |
640 | elif a[0] == '-' or a[0] == '+': | |
641 | w = a[1:] | |
642 | sense = a[0] == '-' | |
643 | if w.isdigit(): | |
644 | n = int(w) | |
645 | if sense: st = Puzzle(n) | |
646 | else: scale = n | |
647 | elif w == 'invert': invertp = sense | |
648 | elif w == 'reflect': reflectp = sense | |
649 | else: raise SyntaxError('unknown option') | |
650 | elif a[0] == '=': st.fill(a[1:]) | |
651 | elif a[0] == ':': parse_moves(a[1:]).apply(st) | |
652 | elif a == '?': st.show() | |
653 | else: | |
654 | p = a | |
655 | _, e = OS.path.splitext(p) | |
656 | surf = CR.ImageSurface(CR.FORMAT_ARGB32, 100, 100) | |
657 | xf = Transform(TAU/10, TAU/10, 8) | |
658 | cr = CR.Context(surf) | |
659 | if invertp: cr.scale(scale, scale) | |
660 | else: cr.scale(scale, -scale) | |
661 | m = MeasuringPainter(xf, cr) | |
662 | st.render(m, reflectp) | |
663 | x0, y0 = cr.user_to_device(m.x0, m.y0) | |
664 | x1, y1 = cr.user_to_device(m.x1, m.y1) | |
665 | if x0 > x1: x0, x1 = x1, x0 | |
666 | if y0 > y1: y0, y1 = y1, y0 | |
667 | w, h = map(lambda x: int(M.ceil(x)), [x1 - x0, y1 - y0]) | |
668 | if e == '.pdf': surf = CR.PDFSurface(p, w + 2, h + 2) | |
669 | elif e == '.png': surf = CR.ImageSurface(CR.FORMAT_ARGB32, w + 2, h + 2) | |
670 | elif e == '.ps': | |
671 | surf = CR.PSSurface(p, w + 2, h + 2) | |
672 | surf.set_eps(True) | |
673 | else: raise SyntaxError('unrecognized extension') | |
674 | cr = CR.Context(surf) | |
675 | cr.translate(1 - x0, 1 - y0) | |
676 | if invertp: cr.scale(scale, scale) | |
677 | else: cr.scale(scale, -scale) | |
678 | paint = Painter(xf, cr) | |
679 | st.render(paint, reflectp) | |
680 | surf.show_page() | |
681 | if e == '.png': surf.write_to_png(p) | |
682 | surf.finish() | |
683 | del cr, surf |