| 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 | print |
| 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 | print |
| 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 | print |
| 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 |