2 ### -*- coding: utf-8 -*-
4 ### Rubik cube model and rendering
6 ### (c) 2018 Mark Wooding
9 ###----- Licensing notice ---------------------------------------------------
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.
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.
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.
25 ###--------------------------------------------------------------------------
26 ### External dependencies.
28 import math
as M
; TAU
= 2*M
.pi
35 ###--------------------------------------------------------------------------
36 ### The main cube model.
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']
45 ## Colours of the faces.
46 COLOUR
= ['#', '+', '%', '/', '=', '-', 'ยท']
47 RGB
= [(1, 0, 0), (1, 0.75, 0),
51 CMAP
= { 'r': U
, 'o': D
, 'y': L
, 'w': R
, 'b': F
, 'g': B
, '?': UNK
}
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)]]
68 def convert_side_coords(w
, orig
, face
, x
, y
):
70 Convert relative coordinates (X, Y) on a side face into raw coordinates.
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.
78 k
, ix
, iy
= GEOM
[orig
][face
]
80 if ix
< 0 or jx
< 0: ox
= w
- 1
82 if iy
< 0 or jy
< 0: oy
= w
- 1
84 return k
, ox
+ x
*ix
+ y
*jx
, oy
+ x
*iy
+ y
*jy
86 def slice_orbits(w
, face
, dp
):
88 Return the effect of a slice turn on the side stickers.
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.
96 oo
.append([convert_side_coords(w
, face
, k
, x
, dp
)
97 for k
in [U
, R
, D
, L
]])
100 def face_orbits(w
, face
, inv
):
102 Return the effect of a face turn on the face's stickers.
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
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
112 for x
in xrange((w
+ 1)//2, w
):
113 for y
in xrange(w
//2, x
+ 1):
117 o
.append((face
, u
, v
))
118 u
, v
= ta
*u
+ tb
*v
+ dx
, tc
*u
+ td
*v
+ dy
122 def orbits(w
, face
, dp
):
124 Return the effect of a slice turn on the cube's stickers.
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.
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)
135 def maybe_parens(parens
, s
):
136 """If S, then return the string `(S)'; otherwise just return S."""
137 if parens
: return '(' + s
+ ')'
140 class BaseMove (object):
142 Base class for move objects.
144 Subclasses must implement the following methods.
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
151 """Return the move repeated M times."""
152 if m
== 0: return NULLMOVE
153 else: return RepeatedMove(me
, m
)
155 """Return a list of constituent moves."""
158 """Return a string representation of the move."""
161 class NullMove (BaseMove
):
162 """The null move, which does nothing at all."""
164 """Apply the move to the puzzle PUZ."""
167 """Return the inverse of the move."""
170 """Return the move repeated M times."""
173 """Return a list of constituent moves."""
176 """Return a string representation, in a priority-LV context."""
178 NULLMOVE
= NullMove()
180 class AtomicMove (BaseMove
):
181 """A turn of a single slice."""
184 for k
in xrange(NFACE
):
185 MAP
[FACE
[k
]] = (k
, 0)
186 MAP
[FACE
[k
].lower()] = (k
, 1)
188 def __init__(me
, face
, dp
= 0, n
= 1):
190 Construct an atomic move.
192 Returns a move representing N clockwise turns of the DPth slice, as
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
)
203 """Return the inverse of the move."""
204 return AtomicMove(me
.face
, me
.dp
, -me
.n
)
206 """Return the move repeated M times."""
208 if not mm
: return NULLMOVE
209 else: return AtomicMove(me
.face
, me
.dp
, mm
)
211 """Return a string representation, in a priority-LV context."""
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
)
221 class SpinMove (BaseMove
):
222 """A spin of the entire cube."""
223 MAP
= { 'X': R
, 'Y': U
, 'Z': F
}
225 for k
, v
in MAP
.iteritems(): UNMAP
[v
] = k
227 def __init__(me
, face
, n
= 1):
228 """Construct a spin move.
230 Returns a move representing N clockwise spins the entire cube DPth slice,
236 """Apply the move to the puzzle PUZ."""
237 puz
.turn_multi(me
.face
, puz
.w
, me
.n
)
239 """Return the inverse of the move."""
240 return SpinMove(me
.face
, -me
.n
)
242 """Return the move repeated M times."""
244 if not mm
: return NULLMOVE
245 else: return SpinMove(me
.face
, mm
)
247 """Return a string representation, in a priority-LV context."""
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
= "'"
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
])
261 """Apply the move to the puzzle PUZ."""
262 for mv
in me
.moves
: mv
.apply(puz
)
264 """Return the inverse of the move."""
265 return MoveSequence(list(reversed([mv
.invert() for mv
in me
.moves
])))
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
]))
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."""
277 """Apply the move to the puzzle PUZ."""
278 for i
in xrange(me
.m
): me
.move
.apply(puz
)
280 """Return the inverse of the move."""
281 return RepeatedMove(me
.move
.invert(), me
.m
)
283 """Return the move repeated M times."""
284 if m
== 0: return NULLMOVE
285 else: return RepeatedMove(me
.move
, m
*me
.m
)
287 """Return a string representation, in a priority-LV context."""
288 return maybe_parens(lv
< 3, '%s%d' %
(me
.move
._str(2), me
.m
))
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
295 def parse_number(s
, i
):
297 Parse an integer from S, starting at index I.
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.
306 while i
< len(s
) and s
[i
].isdigit():
307 n
= 10*n
+ ord(s
[i
]) - ord('0')
309 i
= skip_spaces(s
, i
)
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)
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)
334 i
= skip_spaces(s
, i
+ 1)
335 mv
= AtomicMove(k
, dp
, 1)
339 def parse_move(s
, i
):
340 i
, mv
= parse_primary(s
, i
)
341 if mv
is None: return i
, None
343 if i
< len(s
) and s
[i
] == "'":
344 i
= skip_spaces(s
, i
+ 1)
346 elif i
< len(s
) and s
[i
].isdigit():
347 i
, m
= parse_number(s
, i
)
349 elif i
+ 1 < len(s
) and s
[i
] == '^' and s
[i
+ 1].isdigit():
350 i
, m
= parse_number(s
, i
+ 1)
356 def parse_sequence(s
, i
):
359 i
, mv
= parse_move(s
, i
)
362 if len(seq
) == 0: raise SyntaxError('unrecognized move')
363 elif len(seq
) == 1: return i
, seq
[0]
364 else: return i
, MoveSequence(seq
)
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()])
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()])
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')
389 def xltmatrix(dx
, dy
, dz
):
390 return NP
.matrix([[1, 0, 0, dx
],
393 [0, 0, 0, 1]], NP
.float64
)
395 def rotmatrix(theta
, axis
):
396 R
= NP
.matrix(NP
.identity(4, NP
.float64
))
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
405 def scalematrix(sx
, sy
, sz
):
406 return NP
.matrix([[sx
, 0, 0, 0],
409 [0, 0, 0, 1]], NP
.float64
)
411 class Transform (object):
412 def __init__(me
, theta_x
, theta_y
, 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
423 return NP
.array([x
, y
, z
], NP
.float64
)
426 mag
= M
.sqrt(NP
.dot(v
, v
))
429 def shorten(p
, q
, delta
):
430 u
= unit_vector(q
- p
)
431 return p
+ delta
*u
, q
- delta
*u
436 class BasePainter (object):
437 def __init__(me
, xf
, cr
):
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
)
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
)
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
)
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
)
466 def rounded_polygon(me
, r
, pts
):
470 if o0
is None: o0
= 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
)
481 def polygon(me
, pts
):
486 class Painter (BasePainter
):
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
504 x0
, y0
, x1
, y1
= me
._cr
.stroke_extents()
506 me
._update_bbox(x0
, y0
, x1
, y1
)
509 x0
, y0
, x1
, y1
= me
._cr
.fill_extents()
511 me
._update_bbox(x0
, y0
, x1
, y1
)
514 class Puzzle (object):
518 me
.m
= NP
.ndarray((NFACE
, w
, w
), NP
.uint8
)
520 for k
in xrange(NFACE
):
521 for y
in reversed(xrange(w
)):
525 me
.orbits
= [[orbits(w
, k
, dp
) for dp
in xrange(w
)]
526 for k
in xrange(NFACE
)]
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
]
532 k
, x
, y
= o
[(i
+ n
)%4]
536 def turn_multi(me
, face
, dp
, n
):
537 for i
in xrange(dp
): me
.turn(face
, i
, n
)
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
543 for y
in xrange(me
.w
):
545 elif i
>= len(s
) or s
[i
] != ',': raise SyntaxError('missing ,')
547 for x
in xrange(me
.w
):
548 if i
>= len(s
): raise SyntaxError('unexpected eof')
550 me
.m
[k
, ox
+ x
*ix
+ y
*jx
, oy
+ x
*iy
+ y
*jy
] = c
553 if i
< len(s
) and not s
[i
].isspace():
554 raise SyntaxError('face spec too long')
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')
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
)])
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
]])
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
)])
587 def render_face(me
, paint
, k
, ox
, oy
, oz
, ix
, iy
, iz
, jx
, jy
, jz
):
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()
607 def render(me
, paint
, 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)
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()
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()
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)
638 for a
in SYS
.argv
[1:]:
640 elif a
[0] == '-' or a
[0] == '+':
645 if sense
: st
= Puzzle(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()
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)
671 surf
= CR
.PSSurface(p
, w
+ 2, h
+ 2)
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
)
681 if e
== '.png': surf
.write_to_png(p
)