3
# Pybik -- A 3 dimensional magic cube game.
4
# Copyright © 2009-2012 B. Clausius <barcc@gmx.de>
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.
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.
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/>.
20
# Original filename: glarea.c
21
# Original copyright and license: 2003, 2004 John Darrington, GPL3+
23
from __future__ import print_function, division, unicode_literals
26
from collections import namedtuple
29
# pylint: disable=W0614,W0401
32
from PySide.QtOpenGL import *
33
from PySide.QtCore import *
34
from PySide.QtGui import *
35
# pylint: enable=W0614,W0401
37
# no need for OpenGL module for just two constants
38
GL_RGBA, GL_TEXTURE_2D = 6408, 3553
41
assert GL.GL_RGBA == GL_RGBA and GL.GL_TEXTURE_2D == GL_TEXTURE_2D
46
from .textures import textures
47
from .settings import settings
51
class CubeArea (QGLWidget):
52
end_animation = Signal(bool)
53
request_rotation = Signal((int, int, bool))
54
request_swap_blocks = Signal((int, int, int, bool))
55
request_rotate_block = Signal((int, bool))
56
drop_color = Signal((int, str, str))
57
drop_file = Signal((int, str, str))
60
glformat = QGLFormat()
61
if settings.draw.samples > 0:
62
glformat.setSampleBuffers(True)
63
glformat.setSamples(2**settings.draw.samples)
64
if settings.draw.accum > 0:
65
glformat.setAccum(True)
68
QGLWidget.__init__(self, glformat)
70
self.model = model.empty_model
72
self.selection_debug_info = None
74
self.last_mouse_x = -1
75
self.last_mouse_y = -1
76
self.button_down_background = False
77
self.timer_animate = QTimer() # the animate timer
78
self.timer_animate.timeout.connect(self._on_animate)
79
self.mouse_xy = -1, -1
80
# Structure to hold copy of the last selection taken or None
82
self.selection_mode = None
83
self.editing_model = False
84
self.stop_requested = False
85
settings.keystore.changed.connect(self.on_settings_changed, Qt.QueuedConnection)
88
self.monotonic_time = QElapsedTimer()
89
self.monotonic_time.start()
93
self.speed = settings.draw.speed
96
self.rotation_x, self.rotation_y = glarea.set_rotation_xy(*settings.draw.default_rotation)
97
accum_buffers = settings.draw.accum if self.format().accum() else 0
98
sample_buffers = self.format().sampleBuffers()
99
glarea.set_antialiasing(accum_buffers, sample_buffers)
101
self.setAcceptDrops(True)
102
self.setFocusPolicy(Qt.StrongFocus)
103
self.setMinimumSize(200, 200)
107
self.update_selection_pending = False
109
self.setMouseTracking(True)
111
def load_cursors(self):
113
# Load 3 cursors from file (n - ne)
114
for i, (x, y) in enumerate([(8, 0), (15, 0), (15, 0)]):
115
filename = os.path.join(config.UI_DIR, 'mouse_{}.png'.format(i))
116
image = QImage(filename)
117
cursors.append((image, x, y))
119
image, x, y = cursors[1]
120
cursors.insert(0, (image.mirrored(True, False), 15-x, y))
121
# 12 cursors (ene - nw)
122
transform = QTransform()
124
for i in range(4, 16):
125
image, x, y = cursors[-4]
126
cursors.append((image.transformed(transform), 15-y, x))
127
cursors.append(cursors[0])
128
self.cursors = [QCursor(QPixmap.fromImage(image), x, y) for image, x, y in cursors[1:]]
129
# cursor for center faces
130
filename = os.path.join(config.UI_DIR, 'mouse_ccw.png')
131
cursor = QCursor(QPixmap(filename), 7, 7)
132
self.cursors.append(cursor)
135
cursor.setShape(Qt.CursorShape.CrossCursor)
136
self.cursors.append(cursor)
138
def apply_design(self):
140
color = settings.theme.face[i].color
141
imagefile = settings.theme.face[i].image
142
imagemode = settings.theme.face[i].mode
143
self.set_face_design(i, color, imagefile, imagemode)
144
self.set_lighting(settings.draw.lighting)
145
self.set_background_color(settings.theme.bgcolor)
147
stock_texnames = [-1, 0]
148
def set_face_texture(self, faceidx, imagefile):
149
if imagefile.startswith('/'):
150
pixbuf = textures.create_pixbuf_from_file(imagefile)
152
pixbuf = textures.get_stock_pixbuf(imagefile)
156
texname = self.bindTexture(pixbuf, GL_TEXTURE_2D, GL_RGBA,
157
QGLContext.BindOption.LinearFilteringBindOption|
158
QGLContext.BindOption.MipmapBindOption)
159
if not imagefile.startswith('/') and texname not in self.stock_texnames:
160
self.stock_texnames.append(texname)
161
texname = gldraw.set_face_texture(faceidx, texname)
162
if texname not in self.stock_texnames:
163
self.deleteTexture(texname)
165
def set_face_design(self, i, color, imagefile, imagemode):
167
rgba.setNamedColor(color)
168
gldraw.set_face_rendering(i, red=rgba.redF(), green=rgba.greenF(), blue=rgba.blueF(),
170
self.set_face_texture(i, imagefile)
172
def set_model(self, model_):
174
glarea.set_frustrum(self.model.bounding_sphere_radius, settings.draw.zoom)
175
gldraw.set_model(self.model)
176
self.set_selection_mode(settings.draw.selection)
179
def apply_to_glmodel(cubestate):
180
gldraw.set_transformations(cubestate.blocks)
182
def initializeGL(self):
184
glcontext = self.context()
185
glformat = glcontext.format()
186
glrformat = glcontext.requestedFormat()
187
def printglattr(name, *attrs):
188
print(' {}: '.format(name), end='')
189
def get_value(glformat, attr):
190
if isinstance(attr, basestring):
191
return getattr(glformat, attr)()
193
return attr(glformat)
194
values = [get_value(glformat, a) for a in attrs]
195
rvalues = [get_value(glrformat, a) for a in attrs]
196
if values == rvalues:
199
print(*values, end=' (')
200
print(*rvalues, end=')\n')
201
print('OpenGL format:')
202
printglattr('accum', 'accum', 'accumBufferSize')
203
printglattr('alpha', 'alpha', 'alphaBufferSize')
204
printglattr('rgb', 'redBufferSize', 'greenBufferSize', 'blueBufferSize')
205
printglattr('depth', 'depth', 'depthBufferSize')
206
printglattr('directRendering', 'directRendering')
207
printglattr('doubleBuffer', 'doubleBuffer')
208
printglattr('hasOverlay', 'hasOverlay')
209
printglattr('version', lambda glformat: '{}.{} 0x{:x}'.format(
210
glformat.majorVersion(),
211
glformat.minorVersion(),
212
int(glformat.openGLVersionFlags())))
213
printglattr('plane', 'plane')
214
printglattr('profile', 'profile')
215
printglattr('rgba', 'rgba')
216
printglattr('samples', 'sampleBuffers', 'samples')
217
printglattr('stencil', 'stencil', 'stencilBufferSize')
218
printglattr('stereo', 'stereo')
219
printglattr('swapInterval', 'swapInterval')
223
def draw_face_debug(self):
224
maxis, mslice, mdir, block, symbol, face, center, angle = self.pickdata
225
self.qglColor(QColor(255, 255, 255))
226
self.renderText(2, 18, "block %s, face %s (%s), center %s" % (block.index, symbol, face, center))
227
self.renderText(2, 34, "axis %s, slice %s, dir %s" % (maxis, mslice, mdir))
228
self.renderText(2, 50, "angle %s" % (angle))
234
if self.pickdata is not None:
235
self.draw_face_debug()
236
if self.selection_debug_info is not None:
237
gldraw.draw_select_debug(*self.selection_debug_info)
239
self.render_count += 1
240
if self.monotonic_time.elapsed() > 1000:
241
elapsed = self.monotonic_time.restart()
242
self.fps = 1000. / elapsed * self.render_count
243
self.render_count = 0
244
self.qglColor(QColor(255, 255, 255))
245
self.renderText(2, 18, "FPS %.1f" % self.fps)
247
def resizeGL(self, width, height):
248
glarea.resize(width, height)
250
MODIFIER_MASK = int(Qt.ShiftModifier | Qt.ControlModifier)
251
def keyPressEvent(self, event):
252
modifiers = int(event.modifiers()) & self.MODIFIER_MASK
254
return QGLWidget.keyPressEvent(self, event)
255
elif event.key() == Qt.Key_Right:
257
elif event.key() == Qt.Key_Left:
259
elif event.key() == Qt.Key_Up:
261
elif event.key() == Qt.Key_Down:
264
return QGLWidget.keyPressEvent(self, event)
266
self.rotation_x, self.rotation_y = glarea.set_rotation_xy(self.rotation_x, self.rotation_y)
268
self.update_selection()
270
PickData = namedtuple('PickData', 'maxis mslice mdir block symbol face center angle')
271
def pick_polygons(self, x, y):
272
'''Identify the block at screen co-ordinates x,y.'''
274
height = self.height()
275
index = glarea.pick_polygons(x, height-y, 1)
277
self.selection_debug_info = None
280
maxis, mslice, mdir, face, center, block, symbol, face_center, edge_center = self.model.pick_data[index]
283
angle, self.selection_debug_info = None, None
285
angle, self.selection_debug_info = glarea.get_cursor_angle(face_center, edge_center)
286
self.pickdata = self.PickData(maxis, mslice, mdir, block, symbol, face, center, angle)
288
def update_selection(self):
289
'''This func determines which block the mouse is pointing at'''
290
if self.timer_animate.isActive():
291
if self.pickdata is not None:
295
if self.update_selection_pending:
297
QTimer.singleShot(0, self.on_idle_update_selection)
298
self.update_selection_pending = True
300
def on_idle_update_selection(self):
301
if self.timer_animate.isActive():
302
if self.pickdata is not None:
306
self.pick_polygons(*self.mouse_xy)
309
if not self.timer_animate.isActive():
311
self.update_selection_pending = False
313
def mouseMoveEvent(self, event):
314
self.mouse_xy = event.x(), event.y()
316
if not self.button_down_background:
317
self.update_selection()
321
offset_x = event.x() - self.last_mouse_x
322
offset_y = event.y() - self.last_mouse_y
323
self.rotation_x, self.rotation_y = glarea.set_rotation_xy(
324
self.rotation_x + offset_x,
325
self.rotation_y + offset_y)
326
self.rotation_x -= offset_x
327
self.rotation_y -= offset_y
330
def mousePressEvent(self, event):
331
if self.pickdata is not None:
332
if self.timer_animate.isActive():
335
if self.editing_model:
336
if event.modifiers() & Qt.ControlModifier:
337
if event.button() == Qt.LeftButton:
338
self.request_rotate_block.emit(self.pickdata.block.index, False)
339
elif event.button() == Qt.RightButton:
340
self.request_rotate_block.emit(self.pickdata.block.index, True)
342
if event.button() == Qt.LeftButton:
343
self.request_swap_blocks.emit(self.pickdata.block.index,
344
self.pickdata.maxis, self.pickdata.mslice, self.pickdata.mdir)
346
mslice = -1 if event.modifiers() & Qt.ControlModifier else self.pickdata.mslice
347
if event.button() == Qt.LeftButton:
348
self.request_rotation.emit(self.pickdata.maxis, mslice, self.pickdata.mdir)
349
elif event.button() == Qt.RightButton and settings.draw.selection_nick == 'simple':
350
self.request_rotation.emit(self.pickdata.maxis, mslice, not self.pickdata.mdir)
351
elif event.button() == Qt.LeftButton:
353
self.button_down_background = True
354
self.last_mouse_x = event.x()
355
self.last_mouse_y = event.y()
358
def mouseReleaseEvent(self, event):
359
if event.button() != Qt.LeftButton:
362
if self.button_down_background:
364
self.rotation_x += event.x() - self.last_mouse_x
365
self.rotation_y += event.y() - self.last_mouse_y
366
self.button_down_background = False
367
self.update_selection()
369
def wheelEvent(self, event):
370
if event.orientation() == Qt.Vertical:
371
zoom = settings.draw.zoom * math.pow(1.1, -event.delta() / 120)
372
zoom_min, zoom_max = settings.draw.zoom_range
377
settings.draw.zoom = zoom
379
def dragEnterEvent(self, event):
380
debug('drag enter:', event.mimeData().formats())
381
if (event.mimeData().hasFormat("text/uri-list") or
382
event.mimeData().hasFormat("application/x-color")):
383
event.acceptProposedAction()
385
def dropEvent(self, event):
386
# when a drag is in progress the pickdata is not updated, so do it now
387
self.pick_polygons(event.pos().x(), event.pos().y())
389
mime_data = event.mimeData()
390
if mime_data.hasFormat("application/x-color"):
391
color = mime_data.colorData()
393
debug("Received invalid color data")
396
if self.pickdata is not None:
397
self.drop_color.emit(self.pickdata.block.index, self.pickdata.symbol, color.name())
399
self.drop_color.emit(-1, '', color.name())
400
elif mime_data.hasFormat("text/uri-list"):
401
if self.pickdata is None:
402
debug('Background image is not supported.')
404
uris = mime_data.urls()
406
if not uri.isLocalFile():
407
debug('filename "%s" not found or not a local file.' % uri.toString())
409
filename = uri.toLocalFile()
410
if not filename or not os.path.exists(filename):
411
debug('filename "%s" not found.' % filename)
413
self.drop_file.emit(self.pickdata.block.index, self.pickdata.symbol, filename)
414
break # For now, just use the first one.
417
def set_cursor(self):
418
if self.pickdata is None or self.button_down_background:
420
elif self.pickdata.angle is None:
423
index = int((self.pickdata.angle+180) / 22.5 + 0.5) % 16
424
self.setCursor(self.cursors[index])
426
def set_std_cursor(self):
427
QTimer.singleShot(0, lambda: self.unsetCursor())
430
def set_lighting(enable):
431
glarea.set_lighting(enable)
433
def set_selection_mode(self, mode):
434
self.selection_mode = mode
435
gldraw.set_pick_model(*self.model.gl_pick_data(mode))
436
self.update_selection()
438
def set_editing_mode(self, enable):
439
self.editing_model = enable
441
gldraw.set_pick_model(*self.model.gl_pick_data(0))
443
gldraw.set_pick_model(*self.model.gl_pick_data(self.selection_mode))
444
self.update_selection()
447
def set_background_color(color):
449
rgba.setNamedColor(color)
450
glarea.set_background_color(rgba.redF(), rgba.greenF(), rgba.blueF())
452
def reset_rotation(self):
453
'''Reset cube rotation'''
454
self.rotation_x, self.rotation_y = glarea.set_rotation_xy(*settings.draw.default_rotation)
459
def animate_rotation(self, move_data, blocks, stop_after):
460
self.stop_requested = stop_after
461
axis = (self.model.axesI if move_data.dir else self.model.axes)[move_data.axis]
462
angle = self.model.symmetry[move_data.axis]
463
gldraw.start_animate(angle, *axis)
464
for block_id, selected in blocks:
465
gldraw.set_animation_block(block_id, selected)
466
self.timer_animate.start(0 if DEBUG_VFPS else 20)
467
self.update_selection()
469
def _on_animate(self):
470
increment = self.speed * 1e-02 * 20
471
increment = min(increment, 45)
472
unfinished = gldraw.step_animate(increment)
477
# we have finished the animation sequence now
479
self.timer_animate.stop()
480
self.end_animation.emit(self.stop_requested)
482
self.update_selection()
483
self.stop_requested = False
485
def animate_abort(self, update=True):
486
self.timer_animate.stop()
489
self.end_animation.emit(self.stop_requested)
491
self.update_selection()
492
self.stop_requested = False
494
def on_settings_changed(self, key):
495
if key == 'draw.speed':
496
self.speed = settings.draw.speed
497
elif key == 'draw.lighting':
498
self.set_lighting(settings.draw.lighting)
499
elif key == 'draw.accum':
500
if self.format().accum():
501
glarea.set_antialiasing(settings.draw.accum)
502
elif key == 'draw.samples':
503
if self.format().samples():
504
samples = 2**settings.draw.samples
506
glarea.set_antialiasing(None, False)
507
elif samples == self.format().samples():
508
glarea.set_antialiasing(None, True)
509
elif key == 'draw.selection':
510
self.set_selection_mode(settings.draw.selection)
511
elif key.startswith('theme.face.'):
512
i = int(key.split('.')[2])
513
if key == 'theme.face.{}.color'.format(i):
515
color.setNamedColor(settings.theme.face[i].color)
516
gldraw.set_face_rendering(i, red=color.redF(), green=color.greenF(), blue=color.blueF())
517
elif key == 'theme.face.{}.image'.format(i):
518
self.set_face_texture(i, settings.theme.face[i].image)
519
elif key == 'theme.face.{}.mode'.format(i):
520
imagemode = settings.theme.face[i].mode
521
gldraw.set_face_rendering(i, imagemode=imagemode)
522
elif key == 'theme.bgcolor':
523
self.set_background_color(settings.theme.bgcolor)
524
elif key == 'draw.zoom':
525
glarea.set_frustrum(self.model.bounding_sphere_radius, settings.draw.zoom)
527
debug('Unknown settings key changed:', key)