+#! /usr/bin/python
+### -*- coding: utf-8 -*-
+###
+### Rubik cube model and rendering
+###
+### (c) 2018 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This program is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### This program is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+### GNU General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with this program; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+###--------------------------------------------------------------------------
+### External dependencies.
+
+import math as M; TAU = 2*M.pi
+import os as OS
+import sys as SYS
+
+import cairo as CR
+import numpy as NP
+
+###--------------------------------------------------------------------------
+### The main cube model.
+
+## Names for the faces and corresponding colours.
+U, D, L, R, F, B, UNK = 0, 1, 2, 3, 4, 5, 6
+FACE = ['U', 'D', 'L', 'R', 'F', 'B']
+
+## Number of faces.
+NFACE = 6
+
+## Colours of the faces.
+COLOUR = ['#', '+', '%', '/', '=', '-', 'ยท']
+RGB = [(1, 0, 0), (1, 0.75, 0),
+ (1, 1, 0), (1, 1, 1),
+ (0, 0, 1), (0, 1, 0),
+ (0.4, 0.4, 0.4)]
+CMAP = { 'r': U, 'o': D, 'y': L, 'w': R, 'b': F, 'g': B, '?': UNK }
+
+## Geometry table.
+##
+## For 0 <= k < 6 and 0 <= d < 4, GEOM[k][d] holds a triple (k', ix, iy) with
+## the following meaning. If you start at face k, move in the direction d
+## (up, down, left, or right), and treat the edge that you crossed as the x
+## axis of a right-handed coordinate system, increasing clockwise, then you
+## are on face k', and (ix, iy) is a unit vector in the coordinate system of
+## the state array pointing in the positive x direction.
+GEOM = [[(B, -1, 0), (F, -1, 0), (L, -1, 0), (R, -1, 0)],
+ [(F, +1, 0), (B, +1, 0), (L, +1, 0), (R, +1, 0)],
+ [(U, 0, -1), (D, 0, -1), (B, 0, +1), (F, 0, -1)],
+ [(U, 0, +1), (D, 0, +1), (F, 0, +1), (B, 0, -1)],
+ [(U, +1, 0), (D, -1, 0), (L, 0, +1), (R, 0, -1)],
+ [(U, -1, 0), (D, +1, 0), (R, 0, +1), (L, 0, -1)]]
+
+def convert_side_coords(w, orig, face, x, y):
+ """
+ Convert relative coordinates (X, Y) on a side face into raw coordinates.
+
+ Suppose we start on face ORIG and move to the adjacent FACE (one of `U',
+ `D', `L', `R'), treating the edge we crossed as the x-axis of a right-hand
+ coordinate system, increasing clockwise; return a triple (K, U, V), such
+ that m[K][U][V] identifies the slot in the model corresponding to the given
+ coordinates. The argument W is the side length of the cube.
+ """
+ k, ix, iy = GEOM[orig][face]
+ jx, jy = -iy, ix
+ if ix < 0 or jx < 0: ox = w - 1
+ else: ox = 0
+ if iy < 0 or jy < 0: oy = w - 1
+ else: oy = 0
+ return k, ox + x*ix + y*jx, oy + x*iy + y*jy
+
+def slice_orbits(w, face, dp):
+ """
+ Return the effect of a slice turn on the side stickers.
+
+ Returns a list of lists of (K, X, Y) model coordinates describing the cycle
+ decomposition of the permutation induced on the side stickers of a
+ clockwise turn of the DPth slice of a size-W cube as viewed from FACE.
+ """
+ oo = []
+ for x in xrange(w):
+ oo.append([convert_side_coords(w, face, k, x, dp)
+ for k in [U, R, D, L]])
+ return oo
+
+def face_orbits(w, face, inv):
+ """
+ Return the effect of a face turn on the face's stickers.
+
+ Returns a list of lists of (K, X, Y) model coordinates describing the cycle
+ decomposition of the permutation induced on the face stickers of a
+ clockwise (or anticlockwise, if INV is true) turn of a FACE of a size-W
+ cube.
+ """
+ if inv: ta, tb, tc, td, dx, dy = 0, -1, 1, 0, w - 1, 0
+ else: ta, tb, tc, td, dx, dy = 0, 1, -1, 0, 0, w - 1
+ oo = []
+ for x in xrange((w + 1)//2, w):
+ for y in xrange(w//2, x + 1):
+ o = []
+ u, v = x, y
+ for i in xrange(4):
+ o.append((face, u, v))
+ u, v = ta*u + tb*v + dx, tc*u + td*v + dy
+ oo.append(o)
+ return oo
+
+def orbits(w, face, dp):
+ """
+ Return the effect of a slice turn on the cube's stickers.
+
+ Returns a list of lists of (K, X, Y) model coordinates describing the cycle
+ decompositon of the permutation induced on the cube's stickers of a
+ clockwise turn of the DPth slice of a size-W cube, as viewed from FACE.
+ """
+ oo = slice_orbits(w, face, dp)
+ if dp == 0: oo += face_orbits(w, face, False)
+ elif dp == w - 1: oo += face_orbits(w, face ^ 1, True)
+ return oo
+
+def maybe_parens(parens, s):
+ """If S, then return the string `(S)'; otherwise just return S."""
+ if parens: return '(' + s + ')'
+ else: return s
+
+class BaseMove (object):
+ """
+ Base class for move objects.
+
+ Subclasses must implement the following methods.
+
+ * apply(PUZ): apply the move to a puzzle PUZ
+ * invert(): return the inverse of the move
+ * _str(lv): return a string representation, in a priority-LV context
+ """
+ def repeat(me, m):
+ """Return the move repeated M times."""
+ if m == 0: return NULLMOVE
+ else: return RepeatedMove(me, m)
+ def flatten(me):
+ """Return a list of constituent moves."""
+ return [me]
+ def __str__(me):
+ """Return a string representation of the move."""
+ return me._str(9)
+
+class NullMove (BaseMove):
+ """The null move, which does nothing at all."""
+ def apply(me, puz):
+ """Apply the move to the puzzle PUZ."""
+ pass
+ def invert(me):
+ """Return the inverse of the move."""
+ return me
+ def repeat(me, m):
+ """Return the move repeated M times."""
+ return me
+ def flatten(me):
+ """Return a list of constituent moves."""
+ return []
+ def _str(me, lv):
+ """Return a string representation, in a priority-LV context."""
+ return '#<null>'
+NULLMOVE = NullMove()
+
+class AtomicMove (BaseMove):
+ """A turn of a single slice."""
+
+ MAP = { }
+ for k in xrange(NFACE):
+ MAP[FACE[k]] = (k, 0)
+ MAP[FACE[k].lower()] = (k, 1)
+
+ def __init__(me, face, dp = 0, n = 1):
+ """
+ Construct an atomic move.
+
+ Returns a move representing N clockwise turns of the DPth slice, as
+ viewed from FACE.
+ """
+ me.face = face
+ me.dp = dp
+ me.n = n
+ def apply(me, puz):
+ """Apply the move to the puzzle PUZ."""
+ if me.dp >= 0: puz.turn(me.face, me.dp, me.n)
+ else: puz.turn_multi(me.face, -me.dp, me.n)
+ def invert(me):
+ """Return the inverse of the move."""
+ return AtomicMove(me.face, me.dp, -me.n)
+ def repeat(me, m):
+ """Return the move repeated M times."""
+ mm = (me.n*m)%4
+ if not mm: return NULLMOVE
+ else: return AtomicMove(me.face, me.dp, mm)
+ def _str(me, lv):
+ """Return a string representation, in a priority-LV context."""
+ f = FACE[me.face]
+ if me.n%4 == 0: d = '0'
+ elif me.n%4 == 1: d = ''
+ elif me.n%4 == 2: d = '2'
+ elif me.n%4 == 3: d = "'"
+ if me.dp == 0: return f + d
+ elif me.dp == 1: return f.lower() + d
+ elif me.dp < 0: return '%s%s_%d' % (f, d, -me.dp)
+
+class SpinMove (BaseMove):
+ """A spin of the entire cube."""
+ MAP = { 'X': R, 'Y': U, 'Z': F }
+ UNMAP = { }
+ for k, v in MAP.iteritems(): UNMAP[v] = k
+
+ def __init__(me, face, n = 1):
+ """Construct a spin move.
+
+ Returns a move representing N clockwise spins the entire cube DPth slice,
+ as viewed from FACE.
+ """
+ me.face = face
+ me.n = n
+ def apply(me, puz):
+ """Apply the move to the puzzle PUZ."""
+ puz.turn_multi(me.face, puz.w, me.n)
+ def invert(me):
+ """Return the inverse of the move."""
+ return SpinMove(me.face, -me.n)
+ def repeat(me, m):
+ """Return the move repeated M times."""
+ mm = (me.n*m)%4
+ if not mm: return NULLMOVE
+ else: return SpinMove(me.face, mm)
+ def _str(me, lv):
+ """Return a string representation, in a priority-LV context."""
+ f = me.MAP[me.face]
+ if me.n%4 == 0: d = '0'
+ elif me.n%4 == 1: d = ''
+ elif me.n%4 == 2: d = '2'
+ elif me.n%4 == 3: d = "'"
+ return f + d
+
+class MoveSequence (BaseMove):
+ """A sequence of moves, applied in left-to-right order."""
+ def __init__(me, moves):
+ """Construct a sequence of moves from the list MOVES."""
+ me.moves = reduce(lambda x, y: x + y, [mv.flatten() for mv in moves])
+ def apply(me, puz):
+ """Apply the move to the puzzle PUZ."""
+ for mv in me.moves: mv.apply(puz)
+ def invert(me):
+ """Return the inverse of the move."""
+ return MoveSequence(list(reversed([mv.invert() for mv in me.moves])))
+ def _str(me, lv):
+ """Return a string representation, in a priority-LV context."""
+ return maybe_parens(lv < 5, ' '.join([mv._str(5) for mv in me.moves]))
+
+class RepeatedMove (BaseMove):
+ """A move applied multiple times."""
+ def __init__(me, move, m):
+ """Construct the move which consists of MOVE repeated M times."""
+ me.move = move
+ me.m = m
+ def apply(me, puz):
+ """Apply the move to the puzzle PUZ."""
+ for i in xrange(me.m): me.move.apply(puz)
+ def invert(me):
+ """Return the inverse of the move."""
+ return RepeatedMove(me.move.invert(), me.m)
+ def repeat(me, m):
+ """Return the move repeated M times."""
+ if m == 0: return NULLMOVE
+ else: return RepeatedMove(me.move, m*me.m)
+ def _str(me, lv):
+ """Return a string representation, in a priority-LV context."""
+ return maybe_parens(lv < 3, '%s%d' % (me.move._str(2), me.m))
+
+def skip_spaces(s, i):
+ """Return the smallest index J >= I such that S[J] is not whitespace."""
+ while i < len(s) and s[i].isspace(): i += 1
+ return i
+
+def parse_number(s, i):
+ """
+ Parse an integer from S, starting at index I.
+
+ The substring S[I:] consists of zero or more digits, optionally followed by
+ a nondigit character and any other characters. Convert the digit sequence
+ into an integer N and return the pair (J, N), where J is the index of the
+ first non-whitespace character following the digit sequence, or the length
+ of S if there is no such character.
+ """
+ n = 0
+ while i < len(s) and s[i].isdigit():
+ n = 10*n + ord(s[i]) - ord('0')
+ i += 1
+ i = skip_spaces(s, i)
+ return i, n
+
+def parse_parens(s, i, delim):
+ i = skip_spaces(s, i)
+ i, mv = parse_conjugate(s, i)
+ if s[i] != delim: raise SyntaxError('missing %s' % delim)
+ i = skip_spaces(s, i + 1)
+ return i, mv
+
+def parse_primary(s, i):
+ if i >= len(s): return i, None
+ elif s[i] == '(': return parse_parens(s, i + 1, ')')
+ elif s[i] == '[': return parse_parens(s, i + 1, ']')
+ elif s[i] in SpinMove.MAP:
+ k = SpinMove.MAP[s[i]]
+ i = skip_spaces(s, i + 1)
+ return i, SpinMove(k, 1)
+ elif s[i] in AtomicMove.MAP:
+ k, dp = AtomicMove.MAP[s[i]]
+ if dp == 0 and i + 2 < len(s) and s[i + 1] == '_' and s[i + 2].isdigit():
+ i, n = parse_number(s, i + 2)
+ if n == 1: dp = 0
+ else: dp = -n
+ else:
+ i = skip_spaces(s, i + 1)
+ mv = AtomicMove(k, dp, 1)
+ return i, mv
+ else: return i, None
+
+def parse_move(s, i):
+ i, mv = parse_primary(s, i)
+ if mv is None: return i, None
+ while True:
+ if i < len(s) and s[i] == "'":
+ i = skip_spaces(s, i + 1)
+ mv = mv.invert()
+ elif i < len(s) and s[i].isdigit():
+ i, m = parse_number(s, i)
+ mv = mv.repeat(m)
+ elif i + 1 < len(s) and s[i] == '^' and s[i + 1].isdigit():
+ i, m = parse_number(s, i + 1)
+ mv = mv.repeat(m)
+ else:
+ break
+ return i, mv
+
+def parse_sequence(s, i):
+ seq = []
+ while True:
+ i, mv = parse_move(s, i)
+ if mv is None: break
+ seq.append(mv)
+ if len(seq) == 0: raise SyntaxError('unrecognized move')
+ elif len(seq) == 1: return i, seq[0]
+ else: return i, MoveSequence(seq)
+
+def parse_commutator(s, i):
+ i, mv = parse_sequence(s, i)
+ if i < len(s) and s[i] == ';':
+ i = skip_spaces(s, i + 1)
+ i, mv1 = parse_sequence(s, i)
+ mv = MoveSequence([mv, mv1, mv.invert(), mv1.invert()])
+ return i, mv
+
+def parse_conjugate(s, i):
+ i, mv = parse_commutator(s, i)
+ if i < len(s) and (s[i] == '.' or s[i] == '*'):
+ i = skip_spaces(s, i + 1)
+ i, mv1 = parse_commutator(s, i)
+ mv = MoveSequence([mv, mv1, mv.invert()])
+ return i, mv
+
+def parse_moves(s, i = 0):
+ i = skip_spaces(s, i)
+ i, mv = parse_conjugate(s, i)
+ i = skip_spaces(s, i)
+ if i < len(s): raise SyntaxError('junk at eol')
+ return mv
+
+def xltmatrix(dx, dy, dz):
+ return NP.matrix([[1, 0, 0, dx],
+ [0, 1, 0, dy],
+ [0, 0, 1, dz],
+ [0, 0, 0, 1]], NP.float64)
+
+def rotmatrix(theta, axis):
+ R = NP.matrix(NP.identity(4, NP.float64))
+ i, j = 0, 1
+ if i >= axis: i += 1
+ if j >= axis: j += 1
+ c, s = M.cos(theta), M.sin(theta)
+ R[i, i] = R[j, j] = c
+ R[i, j] = s; R[j, i] = -s
+ return R
+
+def scalematrix(sx, sy, sz):
+ return NP.matrix([[sx, 0, 0, 0],
+ [0, sy, 0, 0],
+ [0, 0, sz, 0],
+ [0, 0, 0, 1]], NP.float64)
+
+class Transform (object):
+ def __init__(me, theta_x, theta_y, delta_z):
+ me.theta_x = theta_x
+ me.theta_y = theta_y
+ me.delta_z = delta_z
+ me._T = xltmatrix(0, 0, delta_z)*\
+ rotmatrix(theta_x, 0)*rotmatrix(theta_y, 1)
+ def project(me, x, y, z):
+ u, v, w, _ = (me._T*NP.array([x, y, z, 1])[:, NP.newaxis]).A1
+ return me.delta_z*u/w, me.delta_z*v/w
+
+def vector(x, y, z):
+ return NP.array([x, y, z], NP.float64)
+
+def unit_vector(v):
+ mag = M.sqrt(NP.dot(v, v))
+ return v/mag
+
+def shorten(p, q, delta):
+ u = unit_vector(q - p)
+ return p + delta*u, q - delta*u
+
+def along(f, p, q):
+ return p + f*(q - p)
+
+class BasePainter (object):
+ def __init__(me, xf, cr):
+ me._cr = cr
+ me._xf = xf
+ dx, _ = me._cr.device_to_user_distance(0.4, 0)
+ me._cr.set_line_width(dx)
+ def set_colour(me, r, g, b):
+ me._cr.set_source_rgb(r, g, b)
+ return me
+ def path(me, pts):
+ firstp = True
+ for x, y, z in pts:
+ u, v = me._xf.project(x, y, z)
+ if firstp: me._cr.move_to(u, v)
+ else: me._cr.line_to(u, v)
+ firstp = False
+ return me
+ def _rounded_path_corner(me, r, q1, p1, p2):
+ q2, q3 = shorten(p1, p2, r)
+ c1, c2 = along(0.7, q1, p1), along(0.7, q2, p1)
+ x0, y0 = me._xf.project(*c1)
+ x1, y1 = me._xf.project(*c2)
+ x2, y2 = me._xf.project(*q2)
+ me._cr.curve_to(x0, y0, x1, y1, x2, y2)
+ return p2, q3
+ def _rounded_path_step(me, r, q1, p1, p2):
+ p2, q3 = me._rounded_path_corner(r, q1, p1, p2)
+ x3, y3 = me._xf.project(*q3)
+ me._cr.line_to(x3, y3)
+ return p2, q3
+ def rounded_polygon(me, r, pts):
+ o0 = o1 = None
+ for x, y, z in pts:
+ p2 = vector(x, y, z)
+ if o0 is None: o0 = p2
+ elif o1 is None:
+ o1 = p1 = p2
+ q0, q1 = shorten(o0, p1, r)
+ x0, y0 = me._xf.project(*q1)
+ me._cr.move_to(x0, y0)
+ else: p1, q1 = me._rounded_path_step(r, q1, p1, p2)
+ p1, q1 = me._rounded_path_step(r, q1, p1, o0)
+ p1, q1 = me._rounded_path_corner(r, q1, p1, o1)
+ me._cr.close_path()
+ return me
+ def polygon(me, pts):
+ me.path(pts)
+ me._cr.close_path()
+ return me
+
+class Painter (BasePainter):
+ def stroke(me):
+ me._cr.stroke()
+ return me
+ def fill(me):
+ me._cr.fill()
+ return me
+
+class MeasuringPainter (BasePainter):
+ def __init__(me, *args, **kw):
+ super(MeasuringPainter, me).__init__(*args, **kw)
+ me.x0 = me.y0 = me.x1 = me.y1 = None
+ def _update_bbox(me, x0, y0, x1, y1):
+ if me.x0 is None or me.x0 > x0: me.x0 = x0
+ if me.y0 is None or me.y0 > y0: me.y0 = y0
+ if me.x1 is None or me.x1 < x1: me.x1 = x1
+ if me.y1 is None or me.y1 < y1: me.y1 = y1
+ def stroke(me):
+ x0, y0, x1, y1 = me._cr.stroke_extents()
+ me._cr.new_path()
+ me._update_bbox(x0, y0, x1, y1)
+ return me
+ def fill(me):
+ x0, y0, x1, y1 = me._cr.fill_extents()
+ me._cr.new_path()
+ me._update_bbox(x0, y0, x1, y1)
+ return me
+
+class Puzzle (object):
+
+ def __init__(me, w):
+ me.w = w
+ me.m = NP.ndarray((NFACE, w, w), NP.uint8)
+
+ for k in xrange(NFACE):
+ for y in reversed(xrange(w)):
+ for x in xrange(w):
+ me.m[k, x, y] = k
+
+ me.orbits = [[orbits(w, k, dp) for dp in xrange(w)]
+ for k in xrange(NFACE)]
+
+ def turn(me, face, dp, n):
+ for o in me.orbits[face][dp]:
+ c = [me.m[k, x, y] for k, x, y in o]
+ for i in xrange(4):
+ k, x, y = o[(i + n)%4]
+ me.m[k, x, y] = c[i]
+ return me
+
+ def turn_multi(me, face, dp, n):
+ for i in xrange(dp): me.turn(face, i, n)
+
+ def fill_face(me, s, i, k, ox, oy, ix, iy, jx, jy):
+ i = skip_spaces(s, i)
+ if i >= len(s): return i
+ firstp = True
+ for y in xrange(me.w):
+ if firstp: pass
+ elif i >= len(s) or s[i] != ',': raise SyntaxError('missing ,')
+ else: i += 1
+ for x in xrange(me.w):
+ if i >= len(s): raise SyntaxError('unexpected eof')
+ c = CMAP[s[i]]
+ me.m[k, ox + x*ix + y*jx, oy + x*iy + y*jy] = c
+ i += 1
+ firstp = False
+ if i < len(s) and not s[i].isspace():
+ raise SyntaxError('face spec too long')
+ return i
+
+ def fill(me, s):
+ i = 0
+ me.m[:, :, :] = UNK
+ i = me.fill_face(s, i, U, 0, 2, +1, 0, 0, -1)
+ i = me.fill_face(s, i, F, 0, 2, +1, 0, 0, -1)
+ i = me.fill_face(s, i, R, 0, 2, +1, 0, 0, -1)
+ i = me.fill_face(s, i, B, 2, 2, -1, 0, 0, -1)
+ i = me.fill_face(s, i, L, 2, 2, -1, 0, 0, -1)
+ i = me.fill_face(s, i, D, 0, 0, +1, 0, 0, +1)
+ i = skip_spaces(s, i)
+ if i < len(s): raise SyntaxError('trailing junk')
+ return me
+
+ def show(me):
+ for y in reversed(xrange(me.w)):
+ print (2*me.w + 2)*' ' + \
+ ' '.join([COLOUR[me.m[U, x, y]]
+ for x in xrange(me.w)])
+ print
+ for y in reversed(xrange(me.w)):
+ print ' '.join([' '.join([COLOUR[me.m[k, x, y]]
+ for x in xrange(me.w)])
+ for k in [L, F, R, B]])
+ print
+ for y in reversed(xrange(me.w)):
+ print (2*me.w + 2)*' ' + \
+ ' '.join([COLOUR[me.m[D, x, y]]
+ for x in xrange(me.w)])
+ print
+
+ def render_face(me, paint, k, ox, oy, oz, ix, iy, iz, jx, jy, jz):
+ w = 2.0/me.w
+ g = w/16.0
+ for x in xrange(me.w):
+ for y in xrange(me.w):
+ paint.set_colour(*RGB[me.m[k, x, y]])
+ paint.rounded_polygon(0.04,
+ [(ox + (x*w + g)*ix + (y*w + g)*jx,
+ oy + (x*w + g)*iy + (y*w + g)*jy,
+ oz + (x*w + g)*iz + (y*w + g)*jz),
+ (ox + ((x + 1)*w - g)*ix + (y*w + g)*jx,
+ oy + ((x + 1)*w - g)*iy + (y*w + g)*jy,
+ oz + ((x + 1)*w - g)*iz + (y*w + g)*jz),
+ (ox + ((x + 1)*w - g)*ix + ((y + 1)*w - g)*jx,
+ oy + ((x + 1)*w - g)*iy + ((y + 1)*w - g)*jy,
+ oz + ((x + 1)*w - g)*iz + ((y + 1)*w - g)*jz),
+ (ox + (x*w + g)*ix + ((y + 1)*w - g)*jx,
+ oy + (x*w + g)*iy + ((y + 1)*w - g)*jy,
+ oz + (x*w + g)*iz + ((y + 1)*w - g)*jz)]).fill()
+
+ def render(me, paint, reflectp):
+
+ if reflectp:
+ paint.set_colour(0, 0, 0)
+ paint.rounded_polygon(0.04, [(-1, +1, +4), (+1, +1, +4),
+ (+1, -1, +4), (-1, -1, +4)]).fill()
+ paint.rounded_polygon(0.04, [(-1, -4, +1), (+1, -4, +1),
+ (+1, -4, -1), (-1, -4, -1)]).fill()
+ paint.rounded_polygon(0.04, [(-4, -1, +1), (-4, +1, +1),
+ (-4, +1, -1), (-4, -1, -1)]).fill()
+ me.render_face(paint, D, -1, -4, +1, +1, 0, 0, 0, 0, -1)
+ me.render_face(paint, B, +1, -1, +4, -1, 0, 0, 0, +1, 0)
+ me.render_face(paint, L, -4, -1, +1, 0, 0, -1, 0, +1, 0)
+
+ paint.set_colour(0, 0, 0)
+ paint.rounded_polygon(0.04, [(-1, +1, -1), (-1, +1, +1),
+ (+1, +1, +1), (+1, -1, +1),
+ (+1, -1, -1), (-1, -1, -1)]).fill()
+
+ ##paint.set_colour(0.3, 0.3, 0.3)
+ ##paint.path([(-1, +1, -1), (+1, +1, -1), (+1, +1, +1)]).stroke()
+ ##paint.path([(+1, +1, -1), (+1, -1, -1)]).stroke()
+
+ me.render_face(paint, U, -1, +1, -1, +1, 0, 0, 0, 0, +1)
+ me.render_face(paint, F, -1, -1, -1, +1, 0, 0, 0, +1, 0)
+ me.render_face(paint, R, +1, -1, -1, 0, 0, +1, 0, +1, 0)
+
+st = Puzzle(3)
+invertp = False
+reflectp = True
+scale = 32
+for a in SYS.argv[1:]:
+ if not a: pass
+ elif a[0] == '-' or a[0] == '+':
+ w = a[1:]
+ sense = a[0] == '-'
+ if w.isdigit():
+ n = int(w)
+ if sense: st = Puzzle(n)
+ else: scale = n
+ elif w == 'invert': invertp = sense
+ elif w == 'reflect': reflectp = sense
+ else: raise SyntaxError('unknown option')
+ elif a[0] == '=': st.fill(a[1:])
+ elif a[0] == ':': parse_moves(a[1:]).apply(st)
+ elif a == '?': st.show()
+ else:
+ p = a
+ _, e = OS.path.splitext(p)
+ surf = CR.ImageSurface(CR.FORMAT_ARGB32, 100, 100)
+ xf = Transform(TAU/10, TAU/10, 8)
+ cr = CR.Context(surf)
+ if invertp: cr.scale(scale, scale)
+ else: cr.scale(scale, -scale)
+ m = MeasuringPainter(xf, cr)
+ st.render(m, reflectp)
+ x0, y0 = cr.user_to_device(m.x0, m.y0)
+ x1, y1 = cr.user_to_device(m.x1, m.y1)
+ if x0 > x1: x0, x1 = x1, x0
+ if y0 > y1: y0, y1 = y1, y0
+ w, h = map(lambda x: int(M.ceil(x)), [x1 - x0, y1 - y0])
+ if e == '.pdf': surf = CR.PDFSurface(p, w + 2, h + 2)
+ elif e == '.png': surf = CR.ImageSurface(CR.FORMAT_ARGB32, w + 2, h + 2)
+ elif e == '.ps':
+ surf = CR.PSSurface(p, w + 2, h + 2)
+ surf.set_eps(True)
+ else: raise SyntaxError('unrecognized extension')
+ cr = CR.Context(surf)
+ cr.translate(1 - x0, 1 - y0)
+ if invertp: cr.scale(scale, scale)
+ else: cr.scale(scale, -scale)
+ paint = Painter(xf, cr)
+ st.render(paint, reflectp)
+ surf.show_page()
+ if e == '.png': surf.write_to_png(p)
+ surf.finish()
+ del cr, surf