~ubuntu-branches/ubuntu/raring/pybik/raring

« back to all changes in this revision

Viewing changes to pybiklib/model.py

  • Committer: Package Import Robot
  • Author(s): B. Clausius
  • Date: 2013-02-03 17:35:32 UTC
  • mfrom: (1.1.2)
  • Revision ID: package-import@ubuntu.com-20130203173532-a71ulf5b07fcul37
Tags: 1.0.1-1
* New upstream release
  + Improved user interface.
  + Added Towers and Bricks (non cubic puzzles).
  + Added an option to show the back faces.
  + The cube can be manipulated with the keyboard.
  + Animation is faster and rendering more beautiful.
  + Added more pretty patterns.
  + Added a new solver.
  + Added new translations.
* More generic watch file based on the proposal by Bart Martens
* Updated debhelper dependency and compat to 9
* Updated Standards-Version to 3.9.4, no changes needed
* debian/copyright:
  + Updated Format URL for final copyright format 1.0
  + Added paragraphs for image files
* Updated Build-Depends: new: python-numpy, python-qt4, help2man
* Updated Depends for transitions:
  + GTK2/GConf -> Qt4 (PySide or PyQt4)
  + GtkGlExt -> QtOpenGL (PySide or PyQt4)
* Suggests python-opengl (unusual usage) and gconf2 (config transition)
* Splittet into an arch dependent and an arch independent package
  (increased size and build time)
* Enabled parallel build for the architecture independent part
* Install autogenerated README file without install paragraph
* Replace the license file (displayed in the about box) by a link
  to usr/share/common-licenses/GPL-3 and add lintian override

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# -*- coding: utf-8 -*-
 
3
 
 
4
#  Copyright © 2012-2013  B. Clausius <barcc@gmx.de>
 
5
#
 
6
#  This program is free software: you can redistribute it and/or modify
 
7
#  it under the terms of the GNU General Public License as published by
 
8
#  the Free Software Foundation, either version 3 of the License, or
 
9
#  (at your option) any later version.
 
10
#
 
11
#  This program is distributed in the hope that it will be useful,
 
12
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
#  GNU General Public License for more details.
 
15
#
 
16
#  You should have received a copy of the GNU General Public License
 
17
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
18
 
 
19
from __future__ import print_function, division, unicode_literals
 
20
 
 
21
import os
 
22
from math import sin, cos, pi, sqrt
 
23
import re
 
24
import cPickle as pickle
 
25
 
 
26
import numpy as np
 
27
 
 
28
from .debug import DEBUG, DEBUG_NOLABEL, DEBUG_NOBEVEL, DEBUG_INNER
 
29
from .config import MODELS_DIR
 
30
 
 
31
N_ = lambda t: t
 
32
try:
 
33
    _
 
34
except NameError:
 
35
    _ = N_
 
36
 
 
37
use_modeldata = True
 
38
modeldata_cache = {}
 
39
 
 
40
 
 
41
class Face (object):
 
42
    def __init__(self, symbol, vindices=None, vertices=None):
 
43
        self.symbol = symbol
 
44
        self.vindices = vindices
 
45
        self.vertices = vertices
 
46
        self.normal = None
 
47
        self.scaled = None # Face with indices for beveled_vertices
 
48
        self.faces = None # adjacent faces
 
49
        
 
50
    def __repr__(self):
 
51
        return 'Face({}, {})'.format(self.symbol, self.vindices)
 
52
        
 
53
    def edges(self):
 
54
        vertices = self.vertices if self.vindices is None else self.vindices
 
55
        v1 = vertices[0]
 
56
        for v2 in vertices[1:]:
 
57
            yield v1, v2
 
58
            v1 = v2
 
59
        yield v1, vertices[0]
 
60
        
 
61
    def center(self, vertices=None):
 
62
        if vertices is None:
 
63
            vertices = self.vertices
 
64
        if self.vindices is not None:
 
65
            vertices = [vertices[vi] for vi in self.vindices]
 
66
        return [sum(_i)/len(_i) for _i in zip(*vertices)]
 
67
        
 
68
    def create_scaled(self, vertices, factor, new_vertices):
 
69
        bf = []
 
70
        for vi in self.vindices:
 
71
            # Scale orthogonal to the center
 
72
            bv = [(v-c)*factor+c for v, c in zip(vertices[vi], self.center(vertices))]
 
73
            bf.append(len(new_vertices))
 
74
            new_vertices.append(bv)
 
75
        self.scaled = Face(self.symbol, bf)
 
76
        
 
77
    def translated(self, vertices, offset, factor=1):
 
78
        tf = []
 
79
        if vertices is None:
 
80
            for v in self.vertices:
 
81
                tv = [xv+xo*factor for xv, xo in zip(v, offset)]
 
82
                tf.append(tv)
 
83
        else:
 
84
            for vi in self.vindices:
 
85
                tv = [xv+xo*factor for xv, xo in zip(vertices[vi], offset)]
 
86
                tf.append(tv)
 
87
        return Face(self.symbol, vertices=tf)
 
88
        
 
89
    def roll(self, n):
 
90
        if self.vindices is not None:
 
91
            vlen = len(self.vindices)
 
92
            self.vindices = [self.vindices[(i-n)%vlen] for i in range(vlen)]
 
93
        elif self.vertices is not None:
 
94
            vlen = len(self.vertices)
 
95
            self.vertices = [self.vertices[(i-n)%vlen] for i in range(vlen)]
 
96
            
 
97
    def init_adjacent_faces(self, faces):
 
98
        self.faces = []
 
99
        for edge in self.edges():
 
100
            edge = tuple(reversed(edge))
 
101
            for f in faces:
 
102
                for e in f.edges():
 
103
                    if e == edge:
 
104
                        break
 
105
                else:
 
106
                    continue
 
107
                self.faces.append(f.symbol)
 
108
                break
 
109
            else:
 
110
                assert False, 'No adjacent face for edge: ' + str((self, edge, faces))
 
111
        assert len(self.faces) == len(self.vindices)
 
112
        
 
113
    def flip(self):
 
114
        self.vertices.reverse()
 
115
        
 
116
        
 
117
class Block (object):
 
118
    def __init__(self, model, index, data=None):
 
119
        if data is not None:
 
120
            self.__dict__.update(data)
 
121
            return
 
122
        self.index = index
 
123
        self.indices = (index % model.sizes[0],
 
124
                        index // model.sizes[0] % model.sizes[1],
 
125
                        index // model.sizes[0] // model.sizes[1])
 
126
        self.coords = [float(2*idx - model.sizes[i] + 1) for i, idx in enumerate(self.indices)]
 
127
        self.coords.append(1.)
 
128
        self.symbol_to_move = {}
 
129
        self.visible_faces = []
 
130
        self.visible_glfaces = []
 
131
        for symbol in model.faces:
 
132
            mdir = symbol not in model.symbols
 
133
            maxis = model.symbols.index(model.invers_symbols[symbol] if mdir else symbol)
 
134
            mslice = self.axis_to_slice(maxis)
 
135
            self.symbol_to_move[symbol] = (maxis, mslice, mdir)
 
136
            if mslice == (model.sizes[maxis]-1 if mdir else 0):
 
137
                self.visible_faces.append(symbol)
 
138
        self.inplace_rotations = []
 
139
        
 
140
    def init_inplace_rotations(self, model):
 
141
        for (blocknum, rotsym), blocknum2 in model.rotated_position.items():
 
142
            if blocknum == blocknum2 == self.index and rotsym:
 
143
                self.inplace_rotations.append(rotsym)
 
144
        self.inplace_rotations = sorted(self.inplace_rotations, key=len)[:2]
 
145
        
 
146
    def __repr__(self):
 
147
        return 'Block({}, {}, {}, {})'.format(self.index, self.indices, self.coords, self.visible_faces)
 
148
        
 
149
    def axis_to_slice(self, maxis):
 
150
        return self.indices[maxis]
 
151
        
 
152
        
 
153
class Geometry (object):
 
154
    def __init__(self, vertices=None, faces=None, bevel_factor=None):
 
155
        self.vertices = vertices
 
156
        self.faces = faces
 
157
        self.beveled_vertices = [] # list of vertex positions
 
158
        
 
159
        for f in self.faces.values():
 
160
            f.normal = f.center(self.vertices)
 
161
            f.create_scaled(self.vertices, bevel_factor, self.beveled_vertices)
 
162
            f.init_adjacent_faces(self.faces.values())
 
163
            
 
164
    def create_permutation(self, matrix):
 
165
        m = np.matrix(matrix)
 
166
        indices = {}
 
167
        for i, v in enumerate(self.vertices):
 
168
            rv = (m * np.matrix(v+[0]).T).T.tolist()[0][:-1]
 
169
            ri = self.vertices.index(rv)
 
170
            indices[i] = ri
 
171
        symbols = {}
 
172
        for sym, f in self.faces.items():
 
173
            rvindices = [indices[i] for i in f.vindices]
 
174
            for rsym, rf in self.faces.items():
 
175
                if set(rvindices) == set(rf.vindices):
 
176
                    symbols[rsym.lower()] = sym.lower()
 
177
        return symbols
 
178
        
 
179
        
 
180
class EmptyModel (object):
 
181
    type = 'Empty'
 
182
    mformat = u'Empty'
 
183
    symbols = b''
 
184
    symbolsI = b''
 
185
    faces = b''
 
186
    
 
187
    sizes = [0, 0, 0]
 
188
    size = None
 
189
    bounding_sphere_radius = 1.
 
190
    blocks = []
 
191
    
 
192
    def __init__(self, *unused_args):
 
193
        # The arrays should never be empty. For empty numpy-arrays it is
 
194
        # not possible to get the C-address in module gldraw (compiled mode).
 
195
        # The fields can only become empty when certain debugging options are active.
 
196
        glvertices = np.array([0], dtype='f')
 
197
        glnormals = np.array([0], dtype='f')
 
198
        glcolors = np.array([0], dtype=np.ubyte)
 
199
        gltexpostiled = np.array([0], dtype='f')
 
200
        gltexposmosaic = np.array([0], dtype='f')
 
201
        self.gldata = [glvertices, glnormals, glcolors, gltexpostiled, gltexposmosaic]
 
202
        
 
203
    def gl_data(self):
 
204
        return tuple(self.gldata) + ([], 0, 0)
 
205
                
 
206
    def gl_pick_data(self, unused_selection_mode):
 
207
        # pylint: disable=W0201
 
208
        self.gl_pick_vertices = np.array([0], dtype='f')
 
209
        self.gl_pick_colors = np.array([0], dtype=np.ubyte)
 
210
        return self.gl_pick_vertices, self.gl_pick_colors
 
211
        
 
212
        
 
213
class BrickModel (object):
 
214
    type = N_('Brick')
 
215
    mformat = _(u'{0}×{1}×{2}-Brick')
 
216
    symmetry = 180., 180., 180.
 
217
    
 
218
    axes = [(-1,0,0), (0,-1,0), (0,0,-1)]   # pylint: disable=C0324
 
219
    symbols = b'LDB'
 
220
    symbolsI = b'RUF'
 
221
    faces = b'UDLRFB'
 
222
    epsilon = 0.00001
 
223
    
 
224
    #### geometry of the cube ####
 
225
    #  vertex      standard
 
226
    #  indices    orientation    face symbols
 
227
    # 2------3                     +---+
 
228
    # |\     |\      y             | U |
 
229
    # | 6------7     |         +---+---+---+---+
 
230
    # | |    | |     o--x      | L | F | R | B |
 
231
    # 0-|----1 |      \        +---+---+---+---+
 
232
    #  \|     \|       z           | D |
 
233
    #   4------5                   +---+
 
234
    
 
235
    geom = Geometry(
 
236
                # vertex-positions, used for unbeveled faces and picking
 
237
                vertices=[[-1,-1,-1], [1,-1,-1], [-1, 1,-1], [1, 1,-1],
 
238
                          [-1,-1, 1], [1,-1, 1], [-1, 1, 1], [1, 1, 1]],
 
239
                # vertex-indices for unbeveled faces, used for picking
 
240
                faces={b'U': Face(b'U', [2,6,7,3]), b'D': Face(b'D', [4,0,1,5]),
 
241
                       b'L': Face(b'L', [2,0,4,6]), b'R': Face(b'R', [7,5,1,3]),
 
242
                       b'F': Face(b'F', [6,4,5,7]), b'B': Face(b'B', [3,1,0,2])},
 
243
                bevel_factor=0.9
 
244
            )
 
245
    texpos_tiled = [(0, 0),  (0, 1),  (1, 1),  (1, 0)]
 
246
    face_axes = {b'U': (0, 2), b'D': (0, 2),
 
247
                 b'L': (2, 1), b'R': (2, 1),
 
248
                 b'F': (0, 1), b'B': (0, 1)}
 
249
    
 
250
    def __init__(self, size, mirror_distance):
 
251
        self.sizes = self.norm_sizes(size) # tuple of lenght 3
 
252
        assert len(self.sizes) == 3
 
253
        for _size in self.sizes:
 
254
            assert 1 <= _size <= 10
 
255
        self.size = size # derived classes can assign a different value
 
256
        self.mirror_distance = mirror_distance and mirror_distance * max(self.sizes)
 
257
        
 
258
        #TODO: The bounding_sphere_radius is optimised for the far clipping plane,
 
259
        #      for the near clipping plane the radius without the mirror_distance
 
260
        #      would be sufficient.
 
261
        s = max(self.sizes)
 
262
        sm = s + (self.mirror_distance or 0)
 
263
        self.bounding_sphere_radius = sqrt(2*s*s + sm*sm)
 
264
        
 
265
        self.invers_symbols = {}
 
266
        for sym, isym in zip(self.symbols, self.symbolsI):
 
267
            self.invers_symbols[sym] = isym
 
268
            self.invers_symbols[isym] = sym
 
269
        self.axesI = [tuple(-x for x in a) for a in self.axes]
 
270
        
 
271
        self.normal_rotation_symbols = {}
 
272
        self.rotation_matrices = {}
 
273
        self._init_rotations()
 
274
        self.face_permutations = self._create_permutations()
 
275
        
 
276
        if use_modeldata:
 
277
            global modeldata_cache
 
278
            try:
 
279
                modeldata = modeldata_cache[(self.type, self.sizes)]
 
280
            except KeyError:
 
281
                datafilename = self.get_datafilename(self.sizes)
 
282
                datafilename = os.path.join(MODELS_DIR, datafilename)
 
283
                with open(datafilename, 'rb') as datafile:
 
284
                    modeldata = pickle.load(datafile)
 
285
                modeldata_cache = modeldata
 
286
                modeldata = modeldata_cache[(self.type, self.sizes)]
 
287
            self.blocks = [Block(None, None, data=data) for data in modeldata[b'blocks']]
 
288
            self.rotated_position = modeldata[b'rotated_position']
 
289
            self.gl_never_visible_face = self._gl_never_visible_face_cached
 
290
        else:
 
291
            nblocks = self.sizes[0] * self.sizes[1] * self.sizes[2]
 
292
            self.blocks = [Block(self, i) for i in range(nblocks)]
 
293
            self.rotated_position = self._create_rotated_position()
 
294
            for block in self.blocks:
 
295
                block.init_inplace_rotations(self)
 
296
            self.gl_never_visible_face = self._gl_never_visible_face_create_cache
 
297
        
 
298
        self.pick_vector = {}
 
299
        for maxis, axis in enumerate(self.axes):
 
300
            for symbol, face in self.geom.faces.items():
 
301
                self.pick_vector[maxis, False, symbol] = np.cross(axis, face.normal).tolist()
 
302
                self.pick_vector[maxis, True, symbol] = np.cross(face.normal, axis).tolist()
 
303
                
 
304
        self.pick_data = [()]  # list of (maxis, mslice, mdir, face, center, block, symbol)
 
305
        
 
306
        # List of numpy-arrays
 
307
        # the gldraw module uses C-pointers to the data, so it is essential
 
308
        # that the arrays do not get lost
 
309
        self.gldata = []
 
310
        
 
311
    @classmethod
 
312
    def norm_sizes(cls, sizes):
 
313
        return sizes
 
314
        
 
315
    @classmethod
 
316
    def displaystring(cls, size):
 
317
        return cls.mformat.format(*size)
 
318
        
 
319
    def __unicode__(self):
 
320
        return self.displaystring(self.sizes)
 
321
        
 
322
    @classmethod
 
323
    def _create_rotation(cls, axis, angle):
 
324
        angle = angle / 180. * pi
 
325
        sa = sin(angle)
 
326
        ca = cos(angle)
 
327
        e_ca = 1 - ca
 
328
        n1 = axis[0]
 
329
        n2 = axis[1]
 
330
        n3 = axis[2]
 
331
        m = np.matrix([
 
332
            [n1*n1*e_ca + ca,    n1*n2*e_ca - n3*sa, n1*n3*e_ca + n2*sa, 0.],
 
333
            [n2*n1*e_ca + n3*sa, n2*n2*e_ca + ca,    n2*n3*e_ca - n1*sa, 0.],
 
334
            [n3*n1*e_ca - n2*sa, n3*n2*e_ca + n1*sa, n3*n3*e_ca + ca,    0.],
 
335
            [0.,    0.,     0.,     1.],
 
336
        ])
 
337
        #XXX: try to keep the matrix clean
 
338
        mx, my = m.shape
 
339
        for y in range(my):
 
340
            for x in range(mx):
 
341
                if abs(m.A[y][x]) < cls.epsilon:
 
342
                    m.A[y][x] = 0.
 
343
        return m
 
344
        
 
345
    @classmethod
 
346
    def _matrix_equal(cls, m1, m2):
 
347
        assert m1.shape == m2.shape, (m1, m2)
 
348
        mx, my = m1.shape
 
349
        for y in range(my):
 
350
            for x in range(mx):
 
351
                if abs(m1.A[y][x] - m2.A[y][x]) > cls.epsilon:
 
352
                    return False
 
353
        return True
 
354
        
 
355
    def _init_rotations(self):
 
356
        prim = [(sym, self._create_rotation(axis, angle))
 
357
                    for axis, sym, angle in zip(self.axes + self.axesI,
 
358
                                         self.symbols + self.symbolsI,
 
359
                                         self.symmetry + self.symmetry)]
 
360
        self.normal_rotation_symbols[b''] = b''
 
361
        transform = [(b'', np.matrix(np.identity(4)))]
 
362
        for sp, p in prim:
 
363
            transform.append((sp, p))
 
364
        for sm, m in transform:
 
365
            for sp, p in prim:
 
366
                n = m * p
 
367
                sn = sm + sp
 
368
                for st, t in transform:
 
369
                    if self._matrix_equal(t, n):
 
370
                        self.normal_rotation_symbols[sn] = st
 
371
                        break
 
372
                else:
 
373
                    self.normal_rotation_symbols[sn] = sn
 
374
                    transform.append((sn, n))
 
375
        for sm, m in transform:
 
376
            self.rotation_matrices[sm] = m.A.tolist()
 
377
        
 
378
    def _create_permutations(self):
 
379
        face_permutations = {}
 
380
        for msym, matrix in self.rotation_matrices.items():
 
381
            face_permutations[msym] = self.geom.create_permutation(matrix)
 
382
        return face_permutations
 
383
        
 
384
    def _create_rotated_position(self):
 
385
        rotated_position = {}
 
386
        for b, block in enumerate(self.blocks):
 
387
            for sym, rotation in self.rotation_matrices.items():
 
388
                coords = (np.matrix([block.coords]) * rotation).A.tolist()[0]
 
389
                for p, pos in enumerate(self.blocks):
 
390
                    if pos.coords == coords:
 
391
                        rotated_position[b, sym] = p
 
392
                        break
 
393
                else:
 
394
                    assert False, 'not a permutation'
 
395
        return rotated_position
 
396
        
 
397
    @classmethod
 
398
    def get_datafilename(cls, sizes):
 
399
        x, y, unused_z = sizes
 
400
        if x <= 2:
 
401
            return b'mdata_01-02'
 
402
        else:
 
403
            return b'mdata_{:02}_{}'.format(x, 0 if x<=5 else y%2 if x<=8 else y%3)
 
404
        
 
405
    def get_savedata(self):
 
406
        blocks = [block.__dict__ for block in self.blocks]
 
407
        return {b'blocks': blocks, b'rotated_position': self.rotated_position}
 
408
            
 
409
    def norm_symbol(self, sym):
 
410
        try:
 
411
            return self.normal_rotation_symbols[sym]
 
412
        except KeyError:
 
413
            new_sym = ''
 
414
            for c in sym:
 
415
                try:
 
416
                    new_sym = self.normal_rotation_symbols[new_sym+c]
 
417
                except KeyError:
 
418
                    raise ValueError('invalid symbol:', sym)
 
419
            return new_sym
 
420
            
 
421
    def block_indices_to_index(self, indices):
 
422
        indices = tuple(indices)
 
423
        for b, block in enumerate(self.blocks):
 
424
            if block.indices == indices:
 
425
                return b
 
426
        raise ValueError('Invalid block indices:', indices)
 
427
        
 
428
    def rotation_symbolic_to_matrix(self, block, sym):
 
429
        m = self.rotation_matrices[sym][:]
 
430
        m[3] = self.blocks[block].coords
 
431
        return m
 
432
        
 
433
    def block_symbolic_to_block_index(self, symblock):
 
434
        indices = [1] * len(self.axes)
 
435
        for match in re.finditer(r'(.)(\d*)', symblock):
 
436
            blockface, blockslice = match.groups()
 
437
            blockface = blockface.upper()
 
438
            blockslicenum = int(blockslice)-1 if blockslice else 0
 
439
            if blockface in self.symbolsI:
 
440
                axis = self.symbolsI.index(blockface)
 
441
                blockslicenum = self.sizes[axis]-1 - blockslicenum
 
442
                blockface = self.invers_symbols[blockface]
 
443
            else:
 
444
                axis = self.symbols.index(blockface)
 
445
            indices[axis] = blockslicenum
 
446
        return self.block_indices_to_index(indices)
 
447
        
 
448
    def block_index_to_block_symbolic(self, blockpos, rotsym):
 
449
        def axisidx_to_sym(axis, idx):
 
450
            if idx <= self.sizes[axis] // 2:
 
451
                sym = self.symbols[axis]
 
452
            else:
 
453
                idx = self.sizes[axis]-1 - idx
 
454
                sym = self.symbolsI[axis]
 
455
            sym = sym.lower()
 
456
            if idx == 0:
 
457
                # skip idx for corners
 
458
                return sym, self.face_symbolic_to_face_color(sym, rotsym)
 
459
            elif idx == 1 and self.sizes[axis] == 3:
 
460
                # for size == 3 there is only one edge
 
461
                return '', ''
 
462
            else:
 
463
                # symbol with index to distinguish edge, but no color because the face is not visible
 
464
                return sym + str(idx+1), '?'
 
465
        x, y, z = self.blocks[blockpos].indices
 
466
        symx, colorsymx = axisidx_to_sym(0, x)
 
467
        symy, colorsymy = axisidx_to_sym(1, y)
 
468
        symz, colorsymz = axisidx_to_sym(2, z)
 
469
        return symx + symy + symz, colorsymx + colorsymy + colorsymz
 
470
        
 
471
    def face_symbolic_to_face_color(self, face, rot):
 
472
        for k, v in self.face_permutations[rot].items():
 
473
            if v == face:
 
474
                return k
 
475
        else:
 
476
            assert False, (face, rot)
 
477
        
 
478
    def rotate_symbolic(self, axis, rdir, block, sym):
 
479
        rsym = (self.symbols if not rdir else self.symbolsI)[axis]
 
480
        block = self.rotated_position[block, rsym]
 
481
        sym = self.norm_symbol(sym + rsym)
 
482
        return block, sym
 
483
        
 
484
    def rotate_move(self, complete_move, move):
 
485
        caxis, unused_cslice, cdir = complete_move
 
486
        maxis, mslice, mdir = move
 
487
        caxissym = (self.symbols if cdir else self.symbolsI)[caxis]
 
488
        maxissym = (self.symbols if not mdir else self.symbolsI)[maxis]
 
489
        raxissym = self.face_permutations[caxissym][maxissym.lower()].upper()
 
490
        rdir = raxissym not in self.symbols
 
491
        raxis = self.symbols.index(self.invers_symbols[raxissym] if rdir else raxissym)
 
492
        if mdir != rdir:
 
493
            mslice = self.sizes[raxis] - 1 - mslice
 
494
        return raxis, mslice, rdir
 
495
        
 
496
    @staticmethod
 
497
    def get_selected_move(block, face, edgeno):
 
498
        '''block: a block in the rotation slice
 
499
           face -> edgeno is the direction of slice rotation
 
500
        '''
 
501
        edgeno = (edgeno - 1) % 4
 
502
        rotation_symbol = face.faces[edgeno]
 
503
        return block.symbol_to_move[rotation_symbol]
 
504
        
 
505
    def compare_move_to_pick(self, maxis, unused_mslice, mdir, face, faceedge):
 
506
        axis = self.axes[maxis]
 
507
        if mdir:
 
508
            vpick = np.cross(axis, face.normal).tolist()
 
509
        else:
 
510
            vpick = np.cross(face.normal, axis).tolist()
 
511
        #TODO: rather test whether face.center+pick_vector intersects with the edge
 
512
        return vpick == faceedge.normal
 
513
        
 
514
    @staticmethod
 
515
    def get_selected_move_center(block, face):
 
516
        maxis, mslice, mdir = block.symbol_to_move[face.symbol]
 
517
        return maxis, mslice, not mdir
 
518
        
 
519
    def _gl_never_visible_face_cached(self, block, face):   # pylint: disable=R0201
 
520
        return face not in block.visible_glfaces
 
521
        
 
522
    def _gl_never_visible_face_create_cache(self, block, face):
 
523
        if not self._gl_never_visible_face_calculated(block, face):
 
524
            block.visible_glfaces.append(face)
 
525
        # create the cache and discard the real gl face
 
526
        return True
 
527
        
 
528
    def _gl_never_visible_face_calculated(self, block, face):
 
529
        if max(self.sizes) == 2 and self.sizes.count(2) >= 2:
 
530
            #FIXME: the algorithm to detect invisible faces is wrong,
 
531
            # deactivate the test for the cubes that are most affected.
 
532
            return False
 
533
        vertices = self.geom.beveled_vertices
 
534
        c = block.coords
 
535
        sqr0 = self.sizes[0] * self.sizes[0]
 
536
        sqr1 = self.sizes[1] * self.sizes[1]
 
537
        sqr2 = self.sizes[2] * self.sizes[2]
 
538
        for vi in face:
 
539
            v = [vk+ck for vk, ck in zip(vertices[vi], c)]
 
540
            if (v[0]*v[0] + v[1]*v[1] > min(sqr0, sqr1) or
 
541
                v[0]*v[0] + v[2]*v[2] > min(sqr0, sqr2) or
 
542
                v[1]*v[1] + v[2]*v[2] > min(sqr1, sqr2)):
 
543
                return False
 
544
        return True
 
545
        
 
546
    def texpos_tiled_to_mosaic(self, block, face, texpos_tiled):
 
547
        axisx, axisy = self.face_axes[face.symbol]
 
548
        sizex, sizey = self.sizes[axisx], self.sizes[axisy]
 
549
        tptx, tpty = texpos_tiled
 
550
        
 
551
        vertices = [self.geom.vertices[vi] for vi in face.vindices]
 
552
        subx = [(c1-c0)/2 for c0, c1 in zip(vertices[0], vertices[-1])]
 
553
        suby = [(c1-c0)/2 for c0, c1 in zip(vertices[0], vertices[1])]
 
554
        coords = block.coords[:3]
 
555
        tpmx = sum(tc*bc for tc, bc in zip(subx, coords))
 
556
        tpmy = sum(tc*bc for tc, bc in zip(suby, coords))
 
557
        
 
558
        texx = ((sizex-1 + tpmx) / 2. + tptx) / sizex
 
559
        texy = ((sizey-1 + tpmy) / 2. + tpty) / sizey
 
560
        return texx, texy
 
561
        
 
562
    def gl_label_quads(self, block, visible):
 
563
        if DEBUG:
 
564
            if DEBUG_INNER:
 
565
                for i in range(3):
 
566
                    if block.indices[i] in [0, self.sizes[i]-1]:
 
567
                        return
 
568
            if DEBUG_NOLABEL and visible:
 
569
                return
 
570
            if DEBUG_NOBEVEL and not visible:
 
571
                return
 
572
        for faceno, symbol in enumerate(self.faces):
 
573
            face = self.geom.faces[symbol]
 
574
            if (symbol in block.visible_faces) != visible:
 
575
                continue
 
576
            f = face.scaled
 
577
            if self.gl_never_visible_face(block, f.vindices):
 
578
                continue
 
579
            for i, vi in enumerate(f.vindices):
 
580
                v = self.geom.beveled_vertices[vi]
 
581
                texpos_tiled = self.texpos_tiled[i]
 
582
                texpos_mosaic = self.texpos_tiled_to_mosaic(block, face, texpos_tiled)
 
583
                yield (v, face.normal, faceno, texpos_tiled, texpos_mosaic)
 
584
            if self.mirror_distance is not None and visible:
 
585
                f = f.translated(self.geom.beveled_vertices, face.normal, self.mirror_distance)
 
586
                f.flip()
 
587
                for i, v in enumerate(f.vertices):
 
588
                    tptx, tpty = self.texpos_tiled[i]
 
589
                    texpos_tiled = 1. - tptx, tpty
 
590
                    texpos_mosaic = self.texpos_tiled_to_mosaic(block, face, texpos_tiled)
 
591
                    yield (v, face.normal, faceno, texpos_tiled, texpos_mosaic)
 
592
                    
 
593
    def gl_beveled_quads(self, block):
 
594
        '''For every edge create a face'''
 
595
        if DEBUG:
 
596
            if DEBUG_INNER:
 
597
                for i in range(3):
 
598
                    if block.indices[i] in [0, self.sizes[i]-1]:
 
599
                        return
 
600
            if DEBUG_NOBEVEL:
 
601
                return
 
602
        for symbol, f in self.geom.faces.items():
 
603
            for symbol2, (vi, vi2) in zip(f.faces, f.edges()):
 
604
                if symbol >= symbol2: # find the corners only once, no matter in which order
 
605
                    continue
 
606
                f2 = self.geom.faces[symbol2]
 
607
                # f and f2 have a common edge (vi,vi2)
 
608
                # we now need the vertices and normals of the adjacent labels.
 
609
                # remember, the face must be counterclockwise!
 
610
                bvi1 = f.scaled.vindices[f.vindices.index(vi2)]
 
611
                bvi2 = f.scaled.vindices[f.vindices.index(vi)]
 
612
                bvi3 = f2.scaled.vindices[f2.vindices.index(vi)]
 
613
                bvi4 = f2.scaled.vindices[f2.vindices.index(vi2)]
 
614
                if self.gl_never_visible_face(block, [bvi1, bvi2, bvi3, bvi4]):
 
615
                    continue
 
616
                n = Face('', [bvi1, bvi2, bvi3, bvi4]).center(self.geom.beveled_vertices)
 
617
                yield self.geom.beveled_vertices[bvi1], n
 
618
                yield self.geom.beveled_vertices[bvi2], n
 
619
                yield self.geom.beveled_vertices[bvi3], n
 
620
                yield self.geom.beveled_vertices[bvi4], n
 
621
                
 
622
    def gl_beveled_triangles(self, block):
 
623
        '''For every corner create a face'''
 
624
        if DEBUG:
 
625
            if DEBUG_INNER:
 
626
                for i in range(3):
 
627
                    if block.indices[i] in [0, self.sizes[i]-1]:
 
628
                        return
 
629
            if DEBUG_NOBEVEL:
 
630
                return
 
631
        for vi in range(len(self.geom.vertices)):
 
632
            bf = [] # one beveled face for each vertex
 
633
            for face_first in self.geom.faces.values():
 
634
                if vi in face_first.vindices:
 
635
                    # we now have one adjacent face, this code should be reached if the model is valid
 
636
                    break
 
637
            else:
 
638
                face_first = None
 
639
                assert False
 
640
            face = face_first
 
641
            while True:
 
642
                ivi = face.vindices.index(vi)
 
643
                bvi = face.scaled.vindices[ivi]
 
644
                bf.insert(0, bvi) # we need counterclockwise order
 
645
                symbol = face.faces[ivi]
 
646
                face = self.geom.faces[symbol]
 
647
                ## we now have the clockwise next face
 
648
                if face_first.symbol == face.symbol:
 
649
                    break
 
650
            if self.gl_never_visible_face(block, bf):
 
651
                continue
 
652
            n = Face('', bf).center(self.geom.beveled_vertices)
 
653
            for vi in bf:
 
654
                yield self.geom.beveled_vertices[vi], n
 
655
            
 
656
    def gl_pick_triangles(self, selection_mode):
 
657
        def edge_center(v1, v2):
 
658
            return [(_v1 + _v2) / 2 for _v1, _v2 in zip(v1, v2)]
 
659
        for block in self.blocks:
 
660
            cnt_faces = 0
 
661
            for i in range(3):
 
662
                if block.indices[i] in [0, self.sizes[i]-1]:
 
663
                    cnt_faces += 1
 
664
            for face, symbol in enumerate(self.faces):
 
665
                f = self.geom.faces[symbol]
 
666
                assert f.symbol == symbol
 
667
                if symbol not in block.visible_faces:
 
668
                    continue
 
669
                cnt_n = 0
 
670
                for _sym in f.faces:
 
671
                    if _sym in block.visible_faces:
 
672
                        cnt_n += 1
 
673
                ft = f.translated(self.geom.vertices, block.coords)
 
674
                vc = ft.center()
 
675
                if self.mirror_distance is None:
 
676
                    ftt = ft
 
677
                else:
 
678
                    ftt = ft.translated(None, f.normal, self.mirror_distance)
 
679
                vct = ftt.center()
 
680
                
 
681
                if selection_mode == 1 and cnt_n == 0:
 
682
                    maxis, mslice, mdir = self.get_selected_move_center(block, f)
 
683
                    color = len(self.pick_data)
 
684
                    self.pick_data.append((maxis, mslice, mdir, face, True, block, symbol, None, None))
 
685
                    for i in (0,1,2, 2,3,0):    # pylint: disable=C0324
 
686
                        yield color, ft.vertices[i]
 
687
                    if self.mirror_distance is not None:
 
688
                        color = len(self.pick_data)
 
689
                        self.pick_data.append((maxis, mslice, not mdir, face, True, block, symbol, None, None))
 
690
                        for i in (0,2,1, 2,0,3):    # pylint: disable=C0324
 
691
                            yield color, ftt.vertices[i]
 
692
                elif selection_mode == 1 and cnt_n == 1 and cnt_faces == 2:
 
693
                    for edgeno, symboledge in enumerate(f.faces): # find the other face on the block
 
694
                        if symboledge in block.visible_faces:
 
695
                            break
 
696
                    else:
 
697
                        edgeno = symboledge = None
 
698
                        assert False
 
699
                    maxis, mslice, mdir = self.get_selected_move(block, f, edgeno)
 
700
                    if DEBUG:
 
701
                        assert self.compare_move_to_pick(maxis, mslice, mdir, f, self.geom.faces[symboledge])
 
702
                    ec = edge_center(*list(ft.edges())[edgeno])
 
703
                    color = len(self.pick_data)
 
704
                    self.pick_data.append((maxis, mslice, mdir, face, False, block, symbol, vc, ec))
 
705
                    for i in (0,1,2, 2,3,0):    # pylint: disable=C0324
 
706
                        yield color, ft.vertices[i]
 
707
                    if self.mirror_distance is not None:
 
708
                        for i in (0,2,1, 2,0,3):    # pylint: disable=C0324
 
709
                            yield color, ftt.vertices[i]
 
710
                elif selection_mode == 1 and cnt_n == 2 and cnt_faces == 3:
 
711
                    # find the two other faces on the block
 
712
                    for i1, symboledge1 in enumerate(f.faces): # find the other face on the block
 
713
                        i2 = (i1 + 1) % 4
 
714
                        symboledge2 = f.faces[i2]
 
715
                        visible_faces = block.visible_faces
 
716
                        if symboledge1 in visible_faces and symboledge2 in visible_faces:
 
717
                            break
 
718
                    else:
 
719
                        i1 = symboledge1 = None
 
720
                        assert False
 
721
                    symboledges = (i1, symboledge1, i1-1), (i2, symboledge2, i2)
 
722
                    for edgeno, symboledge, offset in symboledges:
 
723
                        maxis, mslice, mdir = self.get_selected_move(block, f, edgeno)
 
724
                        if DEBUG:
 
725
                            assert self.compare_move_to_pick(maxis, mslice, mdir, f, self.geom.faces[symboledge])
 
726
                        ec = edge_center(*list(ft.edges())[edgeno])
 
727
                        color = len(self.pick_data)
 
728
                        self.pick_data.append((maxis, mslice, mdir, face, False, block, symbol, vc, ec))
 
729
                        for i in (0, 1, 2):
 
730
                            yield color, ft.vertices[(i+offset)%4]
 
731
                        if self.mirror_distance is not None:
 
732
                            for i in (0, 2, 1):
 
733
                                yield color, ftt.vertices[(i+offset)%4]
 
734
                else:
 
735
                    for edgeno, (edge, edget) in enumerate(zip(ft.edges(), ftt.edges())):
 
736
                        maxis, mslice, mdir = self.get_selected_move(block, f, edgeno)
 
737
                        ec = edge_center(*edge)
 
738
                        color = len(self.pick_data)
 
739
                        self.pick_data.append((maxis, mslice, mdir, face, False, block, symbol, vc, ec))
 
740
                        # pylint: disable=C0321
 
741
                        yield color, vc; yield color, edge[0]; yield color, edge[1]
 
742
                        if self.mirror_distance is not None:
 
743
                            yield color, vct; yield color, edget[1]; yield color, edget[0]
 
744
                    
 
745
    def gl_data(self):
 
746
        vertices = []
 
747
        normals = []
 
748
        colors = []
 
749
        texpost = []
 
750
        texposm = []
 
751
        # list of (labelinfo, bevelqinfo, beveltinfo)
 
752
        blockinfos = [[[], 0, 0] for unused_b in self.blocks]
 
753
        
 
754
        # label vertices
 
755
        for iblock, block in enumerate(self.blocks):
 
756
            cnt = 0
 
757
            facesinfo = []
 
758
            last_faceno = -1
 
759
            for v, n, f, tpt, tpm in self.gl_label_quads(block, True):
 
760
                vertices += v
 
761
                normals += n
 
762
                texpost += tpt
 
763
                texposm += tpm
 
764
                if last_faceno != f:
 
765
                    cnt = 1
 
766
                    facesinfo.append((cnt, f))
 
767
                    last_faceno = f
 
768
                else:
 
769
                    cnt += 1
 
770
                    facesinfo[-1] = (cnt, f)
 
771
            blockinfos[iblock][0][:] = facesinfo
 
772
        # beveled edges
 
773
        idx_vbevelq = len(vertices)
 
774
        for iblock, block in enumerate(self.blocks):
 
775
            cnt = 0
 
776
            for v, n in self.gl_beveled_quads(block):
 
777
                vertices += v
 
778
                normals += n
 
779
                cnt += 1
 
780
            for v, n, f, tpt, tpm in self.gl_label_quads(block, False):
 
781
                vertices += v
 
782
                normals += n
 
783
                cnt += 1
 
784
            blockinfos[iblock][1] = cnt
 
785
        # beveled corners
 
786
        idx_vbevelt = len(vertices)
 
787
        for iblock, block in enumerate(self.blocks):
 
788
            cnt = 0
 
789
            for v, n in self.gl_beveled_triangles(block):
 
790
                vertices += v
 
791
                normals += n
 
792
                cnt += 1
 
793
            blockinfos[iblock][2] = cnt
 
794
        
 
795
        # The arrays should never be empty. For empty numpy-arrays it is
 
796
        # not possible to get the C-address in module gldraw (compiled mode).
 
797
        # The fields can only become empty when certain debugging options are active.
 
798
        glvertices = np.array(vertices or [0], dtype='f')
 
799
        glnormals = np.array(normals or [0], dtype='f')
 
800
        glcolors = np.array(colors or [0], dtype=np.ubyte)
 
801
        gltexpostiled = np.array(texpost or [0], dtype='f')
 
802
        gltexposmosaic = np.array(texposm or [0], dtype='f')
 
803
        self.gldata.append(glvertices)
 
804
        self.gldata.append(glnormals)
 
805
        self.gldata.append(glcolors)
 
806
        self.gldata.append(gltexpostiled)
 
807
        self.gldata.append(gltexposmosaic)
 
808
        if DEBUG:
 
809
            assert idx_vbevelq * 2 == len(texpost) * 3 == len(texposm) * 3
 
810
            assert sum([cnt for _bi in blockinfos for cnt, unused_faceno in _bi[0]]) * 3 == idx_vbevelq
 
811
        
 
812
        return (glvertices, glnormals, glcolors, gltexpostiled, gltexposmosaic,
 
813
                    blockinfos,
 
814
                    idx_vbevelq, idx_vbevelt
 
815
               )
 
816
                
 
817
    def gl_pick_data(self, selection_mode):
 
818
        # Pick TRIANGLES vertices
 
819
        vertices = []
 
820
        colors = []
 
821
        for col, v in self.gl_pick_triangles(selection_mode):
 
822
            vertices += v
 
823
            color = [(col>>4) & 0xf0, (col) & 0xf0, (col<<4) & 0xf0]
 
824
            colors += color
 
825
        glvertices = np.array(vertices or [0], dtype='f')
 
826
        glcolors = np.array(colors or [0], dtype=np.ubyte)
 
827
        # pylint: disable=W0201
 
828
        self.gl_pick_vertices = glvertices
 
829
        self.gl_pick_colors = glcolors
 
830
        assert len(glvertices) == len(glcolors)
 
831
        return glvertices, glcolors
 
832
        
 
833
        
 
834
class TowerModel (BrickModel):
 
835
    type = N_('Tower')
 
836
    mformat = _(u'{0}×{1}-Tower')
 
837
    symmetry = 180., 90., 180.
 
838
    
 
839
    def __init__(self, size, mirror_distance):
 
840
        BrickModel.__init__(self, size, mirror_distance)
 
841
        self.size = size[:2]
 
842
        
 
843
    @classmethod
 
844
    def norm_sizes(cls, sizes):
 
845
        x, y, unused_z = sizes
 
846
        return x, y, x
 
847
        
 
848
        
 
849
class CubeModel (BrickModel):
 
850
    type = N_('Cube')
 
851
    mformat = _(u'{0}×{0}×{0}-Cube')
 
852
    symmetry = 90., 90., 90.
 
853
    
 
854
    def __init__(self, size, mirror_distance):
 
855
        BrickModel.__init__(self, size, mirror_distance)
 
856
        self.size = size[0],
 
857
        
 
858
    @classmethod
 
859
    def norm_sizes(cls, sizes):
 
860
        x, unused_y, unused_z = sizes
 
861
        return x, x, x
 
862
        
 
863
        
 
864
empty_model = EmptyModel()
 
865
models = [CubeModel, TowerModel, BrickModel]
 
866
 
 
867
def from_string(modelstr):
 
868
    if modelstr == '*':
 
869
        return '*', '*', (), None
 
870
    re_model = r'''^(\w+)
 
871
                    (?: \s+(\w+)
 
872
                        (?: \s*(\W)
 
873
                            \s*(\w+)
 
874
                            (?: \s*\3
 
875
                                \s*(\w+)
 
876
                        )?)?
 
877
                        (?:\s+with
 
878
                            \s+(.+)
 
879
                    )?)?\s*$'''
 
880
    match = re.match(re_model, modelstr, re.X)
 
881
    if match is None:
 
882
        raise ValueError('Invalid model: ' + modelstr)
 
883
    mtype, width, height, depth, exp = match.group(1, 2, 4, 5, 6)
 
884
    for Model in models:
 
885
        if mtype == Model.type:
 
886
            break
 
887
    else:
 
888
        Model = None
 
889
        raise ValueError('Unknown model type %r' % mtype)
 
890
    def convert_if_int(value):
 
891
        if value is None:
 
892
            return value
 
893
        try:
 
894
            return int(value)
 
895
        except ValueError:
 
896
            return value
 
897
    sizes = tuple(convert_if_int(s) for s in (width, height, depth))
 
898
    return modelstr, Model, sizes, exp
 
899
    
 
900