Search code examples
python-3.xopenglresizewindowpyglet

Why is pyglet.app.run() dispatching on_resize() repeatedly leading to max recursion depth error?


I am trying to make a simple pyglet app (with Python 3.7) where I can draw a grid, some tiles, color them etc. While also being able to pan, zoom around, so I followed this question :

How to pan and zoom properly in 2D?

However the on_resize() event of the MyWindow subclass seems to be called repeatedly after run() is called, even if I don't touch the window, and the script crashes with a max recursion depth, except when I comment out its self.width = width and self.height = height lines. But then resizing the window squishes the objects I'm drawing...

I would like resizing to just show more of the map, keeping the aspect ratio and size of drawn objects.

I have added some print() calls to check what happens, but I'm truly at a loss with what's happening here.

import pyglet
from pyglet.gl import *

from algorithm.priorityqueue import PriorityQueue
from algorithm.squaregrid import heuristic, SquareGrid

ZOOM_IN_FACTOR = 1.2
ZOOM_OUT_FACTOR = 1/ZOOM_IN_FACTOR

class Triangle:
    def __init__(self, color):
        self.color = color
        self.vertices = pyglet.graphics.vertex_list(3, ('v3f', [0,0,0, 1000,0,0, 1000,1000,0]),
                                                       ('c4B', self.color*3))

    def draw(self, x, y, z):
        glTranslatef(x, y, z)
        self.vertices.draw(GL_TRIANGLES)
        glTranslatef(-x, -y, -z)

class Tile:

    def __init__(self, size, color):
        self.size = size
        self.color = color
        self.vertices = pyglet.graphics.vertex_list(
                                        4, ('v3f', [0,0,0, 0+self.size,0,0, 
                                        0+self.size,0+self.size,0, 0,0+self.size,0]),
                                        ('c4B', self.color*4))

    def draw(self, x, y, z):
        glTranslatef(x, y, z)
        self.vertices.draw(GL_QUADS)
        glTranslatef(-x, -y, -z)

class Line:

    def __init__(self, start, end, color, stroke):
        self.color = color
        self.stroke = stroke
        self.sx, self.sy = start
        self.ex, self.ey = end

        self.vertices = pyglet.graphics.vertex_list(
                                        2, ('v3f', [self.sx,self.sy,0, self.ex,self.ey,0]),
                                        ('c4B', self.color*2))

    def draw(self):
        glLineWidth(self.stroke)
        self.vertices.draw(GL_LINES)


class MyWindow(pyglet.window.Window):
    def __init__(self, width, height, *args, **kwargs):
        conf = Config(sample_buffers=1,
                      samples=4,
                      depth_size=16,
                      double_buffer=True)
        super().__init__(width, height, config=conf, *args, **kwargs)

        #self.set_minimum_size(960, 540)
        #glClearColor(0.8, 0.8, 0.8, 1.0)

        #glOrtho(0, self.width, 0, self.height, -10, 10) # setup orthogonal projection

        self.left = 0
        self.right = width
        self.bottom = 0
        self.top = height
        self.zoom_level = 1
        self.zoomed_width = width
        self.zoomed_height = height
        print("init finished")

    def init_gl(self, width, height):
        print("init_gl started")
        # Clear color
        glClearColor(255/255, 255/255, 255/255, 255/255)

        # Antialiasing
        glEnable(GL_LINE_SMOOTH)
        glEnable(GL_POLYGON_SMOOTH)
        glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)

        # Alpha Blending
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

        # Viewport
        glViewport(0, 0, width, height)


    def drawstuff(self):
        print("drawstuff called")
        self.triangle = Triangle((255, 0, 0, 255))
        self.square = Tile(10, (255,0,255,255))

        self.gridlines = []
        for j in range(0,25):
            self.gridlines.append(Line((0, 40*j),(1000, 40*j),(50,50,50,255), 2))
        for i in range(0,25):
            self.gridlines.append(Line((40*i, 0),(40*i, 1000),(50,50,50,255), 2))       

    def on_resize(self, width, height):

        print("on_resize called")

        # It crashes here!
        self.width = width
        print(width)
        self.height = height
        print(height)

        self.init_gl(width, height)

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        # Move camera
        self.left -= dx*self.zoom_level
        self.right -= dx*self.zoom_level
        self.bottom -= dy*self.zoom_level
        self.top -= dy*self.zoom_level       

    def on_mouse_scroll(self, x, y, dx, dy):
        # Scale factor
        f = ZOOM_IN_FACTOR if dy<0 else ZOOM_OUT_FACTOR if dy>0 else 1
        # If in proper range
        if .2 < self.zoom_level*f < 5:

            self.zoom_level *= f

            print(self.width, self.height)

            mouse_x = x/self.width
            mouse_y = y/self.height

            mouse_x_in_world = self.left + mouse_x * self.zoomed_width
            mouse_y_in_world = self.bottom + mouse_y * self.zoomed_height

            self.zoomed_width *= f
            self.zoomed_height *= f

            self.left = mouse_x_in_world - mouse_x * self.zoomed_width
            self.right = mouse_x_in_world + (1 - mouse_x) * self.zoomed_width
            self.bottom = mouse_y_in_world - mouse_y * self.zoomed_height
            self.top = mouse_y_in_world + (1 - mouse_y) * self.zoomed_height

    def on_draw(self):
        print("draw called")
        # Init projection matrix
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()

        # Init Modelview matrix
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        # Save default modelview matrix
        glPushMatrix()

        # Clear window with ClearColor
        glClear(GL_COLOR_BUFFER_BIT)

        # Set Orthographic projection matrix
        glOrtho(self.left, self.right, self.bottom, self.top, 1, -1)

        #self.draw_background()
        self.triangle.draw(0, 0, 0)
        self.square.draw(-100, -100, 0)
        for line in self.gridlines:
            line.draw()

        # Remove default modelview matrix
        glPopMatrix()

    def run(self):
        print("run called")
        pyglet.app.run()
        print("run finished")



if __name__ == "__main__":
    App = MyWindow(800, 500, resizable=True)
    App.drawstuff()
    App.run()

And the console shows :

$ python3 environments/testapp.py
init finished
drawstuff called
run called
on_resize called
on_resize called
on_resize called
on_resize called
on_resize called

etc. with at some point :

on_resize called
on_resize called
Traceback (most recent call last):
  File "environments/testapp.py", line 185, in <module>
    App.run()
  File "environments/testapp.py", line 177, in run
    pyglet.app.run()
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/app/__init__.py", line 107, in run
    event_loop.run()
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/app/base.py", line 159, in run
    self._legacy_setup()
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/app/base.py", line 182, in _legacy_setup
    window.dispatch_pending_events()
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/window/xlib/__init__.py", line 914, in dispatch_pending_events
    EventDispatcher.dispatch_event(self, *self._event_queue.pop(0))
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/event.py", line 415, in dispatch_event
    if getattr(self, event_type)(*args):
  File "environments/testapp.py", line 110, in on_resize
    self.width = width
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/window/__init__.py", line 964, in width
    self.set_size(new_width, self.height)
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/window/xlib/__init__.py", line 574, in set_size
    self.dispatch_event('on_resize', width, height)
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/window/__init__.py", line 1323, in dispatch_event
    if EventDispatcher.dispatch_event(self, *args) != False:
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/event.py", line 415, in dispatch_event
    if getattr(self, event_type)(*args):
  File "environments/testapp.py", line 110, in on_resize
    self.width = width
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/window/__init__.py", line 964, in width
    self.set_size(new_width, self.height)
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/window/xlib/__init__.py", line 574, in set_size
    self.dispatch_event('on_resize', width, height)
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/window/__init__.py", line 1323, in dispatch_event
    if EventDispatcher.dispatch_event(self, *args) != False:

With the last 10 lines repeating for a while, and at the end :

if EventDispatcher.dispatch_event(self, *args) != False:
  File "/home/admin/.local/lib/python3.7/site-packages/pyglet/event.py", line 415, in dispatch_event
    if getattr(self, event_type)(*args):
  File "environments/testapp.py", line 108, in on_resize
    print("on_resize called")
RecursionError: maximum recursion depth exceeded while calling a Python object

I find it weird that run() is calling on_resize() on its own, and I really have no clue as to why reassigning the attributes self.width and self.height is entering a recursion.

It also crashes when the value assigned is not width or height, but any constant number as well, and also whether the Window instance is passed resizable = True or = False as argument


Solution

  • run does not call on_resize(), but run executes the event loop and the resize event occurs once after initializing the window.
    pyglet.window provides the properties .width and .height, thus there is no need to set them by self.width = width respectively self.height = height. Likely, assigning a value to self.width or self.height triggers the on_resize event. If you do that in on_resize, that causes an endless recursion.

    That self.width and self.height contain the current size of the window, can be verified with ease, by printing the values in on_resize:

    class MyWindow(pyglet.window.Window):
        # [...]
    
        def on_resize(self, width, height):
            print("on_resize called")
            print(self.width, width)
            print(self.height, height)
    

    Do not assign a value to self.width or self.height, if the new value is equal the current value:

    class MyWindow(pyglet.window.Window):
        # [...]
    
        def on_resize(self, width, height):
    
            new_width = ...
            new_height = ...
    
            if new_width != width:
                self.width = new_width
            if new_height != height:
                self.height = new_height