Related
I am writing a script to store movements over a hexgrid using Tkinter. As part of this I want to use a mouse-click on a Tkinter canvas to first identify the click location, and then draw a line between this point and the location previously clicked.
Generally this works, except that after I've drawn a line, it become an object that qualifies for future calls off the find_closest method. This means I can still draw lines between points, but selecting the underlying Hex in the Hexgrid over times becomes nearly impossible. I was wondering if someone could help me find a solution to exclude particular objects (lines) from the find_closest method.
edit: I hope this code example is minimal enough.
import tkinter
from tkinter import *
from math import radians, cos, sin, sqrt
class App:
def __init__(self, parent):
self.parent = parent
self.c1 = Canvas(self.parent, width=int(1.5*340), height=int(1.5*270), bg='white')
self.c1.grid(column=0, row=0, sticky='nsew')
self.clickcount = 0
self.clicks = [(0,0)]
self.startx = int(20*1.5)
self.starty = int(20*1.5)
self.radius = int(20*1.5) # length of a side
self.hexagons = []
self.columns = 10
self.initGrid(self.startx, self.starty, self.radius, self.columns)
self.c1.bind("<Button-1>", self.click)
def initGrid(self, x, y, radius, cols):
"""
2d grid of hexagons
"""
radius = radius
column = 0
for j in range(cols):
startx = x
starty = y
for i in range(6):
breadth = column * (1.5 * radius)
if column % 2 == 0:
offset = 0
else:
offset = radius * sqrt(3) / 2
self.draw(startx + breadth, starty + offset, radius)
starty = starty + 2 * (radius * sqrt(3) / 2)
column = column + 1
def draw(self, x, y, radius):
start_x = x
start_y = y
angle = 60
coords = []
for i in range(6):
end_x = start_x + radius * cos(radians(angle * i))
end_y = start_y + radius * sin(radians(angle * i))
coords.append([start_x, start_y])
start_x = end_x
start_y = end_y
hex = self.c1.create_polygon(coords[0][0], coords[0][1], coords[1][0], coords[1][1], coords[2][0],
coords[2][1], coords[3][0], coords[3][1], coords[4][0], coords[4][1],
coords[5][0], coords[5][1], fill='black')
self.hexagons.append(hex)
def click(self, evt):
self.clickcount = self.clickcount + 1
x, y = evt.x, evt.y
tuple_alfa = (evt.x, evt.y)
self.clicks.append(tuple_alfa)
if self.clickcount >= 2:
start = self.clicks[self.clickcount - 1]
startx = start[0]
starty = start[1]
self.c1.create_line(evt.x, evt.y, startx, starty, fill='white')
clicked = self.c1.find_closest(x, y)[0]
print(clicked)
root = tkinter.Tk()
App(root)
root.mainloop()
I need to create a widget that is used to pick a time. QTimeEdit widget doesn't seem intuitive or a good design. So I decided to create a time picker similar to the time picker in smartphones.
I managed to create the clock and click that makes the pointer (something similar to the pointer in the image) move to the currently clicked position (note: it's not perfect, it still looks bad). I would like to have help with making the inner clock
Here is my code:
from PyQt5 import QtWidgets, QtGui, QtCore
import math, sys
class ClockWidget(QtWidgets.QWidget): # I want to be able to reuse this class for other programs also, so please don't hard code values of the list, start and end
def __init__(self, start, end, lst=[], *args, **kwargs):
super(ClockWidget, self).__init__(*args, **kwargs)
self.lst = lst
if not self.lst:
self.lst = [*range(start, end)]
self.index_start = 0 # tune this to move the letters in the circle
self.pointer_angles_multiplier = 9 # just setting the default values
self.current = None
self.rects = []
#property
def index_start(self):
return self._index_start
#index_start.setter
def index_start(self, index):
self._index_start = index
def paintEvent(self, event):
self.rects = []
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setColor(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
x, y = self.rect().x(), self.rect().y()
width, height = self.rect().width(), self.rect().height()
painter.drawEllipse(x, y, x + width, x + height)
s, t, equal_angles, radius = self.angle_calc()
radius -= 30
pen.setColor(QtCore.Qt.green)
pen.setWidth(2)
painter.setPen(pen)
""" pointer angle helps in determining to which position the pointer should be drawn"""
self.pointer_x, self.pointer_y = s + ((radius-30) * math.cos(self.pointer_angles_multiplier * equal_angles)), t \
+ ((radius-30) * math.sin(self.pointer_angles_multiplier * equal_angles))
""" The pendulum like pointer """
painter.drawLine(QtCore.QPointF(s, t), QtCore.QPointF(self.pointer_x, self.pointer_y))
painter.drawEllipse(QtCore.QRectF(QtCore.QPointF(self.pointer_x - 20, self.pointer_y - 40),
QtCore.QPointF(self.pointer_x + 30, self.pointer_y + 10)))
pen.setColor(QtCore.Qt.blue)
pen.setWidth(3)
font = self.font()
font.setPointSize(14)
painter.setFont(font)
painter.setPen(pen)
""" Drawing the number around the circle formula y = t + radius * cos(a)
y = s + radius * sin(a) where angle is in radians (s, t) are the mid point of the circle """
for index, char in enumerate(self.lst, start=self.index_start):
angle = equal_angles * index
y = t + radius * math.sin(angle)
x = s + radius * math.cos(angle)
# print(f"Add: {add_x}, index: {index}; char: {char}")
rect = QtCore.QRectF(x - 30, y - 40, x + 60, y) # clickable point
self.rects.append([index, char, rect]) # appends index, letter, rect
painter.setPen(QtCore.Qt.blue)
painter.drawRect(rect) # helps in visualizing the points where the click can received
print(f"Rect: {rect}; char: {char}")
painter.setPen(QtCore.Qt.red)
points = QtCore.QPointF(x, y)
painter.drawText(points, str(char))
def mousePressEvent(self, event):
for x in self.rects:
index, char, rect = x
if event.button() & QtCore.Qt.LeftButton and rect.contains(event.pos()):
self.pointer_angles_multiplier = index
self.current = char
self.update()
break
def angle_calc(self):
"""
This will simply return (midpoints of circle, divides a circle into the len(list) and return the
angle in radians, radius)
"""
return ((self.rect().width() - self.rect().x()) / 2, (self.rect().height() - self.rect().y()) / 2,
(360 / len(self.lst)) * (math.pi / 180), (self.rect().width() / 2))
def resizeEvent(self, event: QtGui.QResizeEvent):
"""This is supposed to maintain a Square aspect ratio on widget resizing but doesn't work
correctly as you will see when executing"""
if event.size().width() > event.size().height():
self.resize(event.size().height(), event.size().width())
else:
self.resize(event.size().width(), event.size().width())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
message = ClockWidget(1, 13)
message.index_start = 10
message.show()
sys.exit(app.exec())
The Output:
The blue rectangles represent the clickable region. I would be glad if you could also, make the pointer move to the closest number when clicked inside the clock (Not just move the pointer when the clicked inside the blue region)
There is one more problem in my code, that is the numbers are not evenly spaced from the outer circle. (like the number 12 is closer to the outer circle than the number 6)
Disclaimer: I will not explain the cause of the error but the code I provide I think should give a clear explanation of the errors.
The logic is to calculate the position of the centers of each small circle, and use the exinscribed rectangle to take it as a base to draw the text and check if the point where you click is close to the texts.
from functools import cached_property
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ClockWidget(QtWidgets.QWidget):
L = 12
r = 40.0
DELTA_ANGLE = 2 * math.pi / L
current_index = 9
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
R = min(self.rect().width(), self.rect().height()) / 2
margin = 4
Rect = QtCore.QRectF(0, 0, 2 * R - margin, 2 * R - margin)
Rect.moveCenter(self.rect().center())
painter.setBrush(QtGui.QColor("gray"))
painter.drawEllipse(Rect)
rect = QtCore.QRectF(0, 0, self.r, self.r)
if 0 <= self.current_index < 12:
c = self.center_by_index(self.current_index)
rect.moveCenter(c)
pen = QtGui.QPen(QtGui.QColor("red"))
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(c, self.rect().center())
painter.setBrush(QtGui.QColor("red"))
painter.drawEllipse(rect)
for i in range(self.L):
j = (i + 2) % self.L + 1
c = self.center_by_index(i)
rect.moveCenter(c)
painter.setPen(QtGui.QColor("white"))
painter.drawText(rect, QtCore.Qt.AlignCenter, str(j))
def center_by_index(self, index):
R = min(self.rect().width(), self.rect().height()) / 2
angle = self.DELTA_ANGLE * index
center = self.rect().center()
return center + (R - self.r) * QtCore.QPointF(math.cos(angle), math.sin(angle))
def index_by_click(self, pos):
for i in range(self.L):
c = self.center_by_index(i)
delta = QtGui.QVector2D(pos).distanceToPoint(QtGui.QVector2D(c))
if delta < self.r:
return i
return -1
def mousePressEvent(self, event):
i = self.index_by_click(event.pos())
if i >= 0:
self.current_index = i
self.update()
#property
def hour(self):
return (self.current_index + 2) % self.L + 1
def minumumSizeHint(self):
return QtCore.QSize(100, 100)
def main():
app = QtWidgets.QApplication(sys.argv)
view = ClockWidget()
view.resize(400, 400)
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
So i have been trying to learn how 3D rendering works. I tried write a script with the goal to rotate a flat (2D) square in 3D space. I started by defining a square in a normalised space (-1, 1). Note that only x and y is normalised.
class Vec3:
# 3D VECTOR
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
s = 1
p1 = Vec3(-s, -s, -s)
p2 = Vec3(s, -s, -s)
p3 = Vec3(s, s, -s)
p4 = Vec3(-s, s, -s)
Then translated the points into the screen:
p1.z += 6
p2.z += 6
p3.z += 6
p4.z += 6
Everything after this is done inside the application loop. I scaled the points into the the screen with projection applied using the function:
class Transform:
# IT TRANSFORMS THE X AND Y FROM NORMALISED SPACE TO SCREEN SPACE WITH PROJECTION APPLIED
def worldSpaceTransform(self, vec3, w, h):
if vec3.z == 0:
vec3.z = 0.001
zInverse = 1/ vec3.z
xTransformed = ((vec3.x * zInverse) + 1) * (w/2)
yTransformed = ((-vec3.y * zInverse) + 1) * (h/2)
xTransformed = str(xTransformed)[:6]
yTransformed = str(yTransformed)[:6]
return Vec2(float(xTransformed), float(yTransformed))
like this:
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
point1 = transform.worldSpaceTransform(p1, SCREENWIDTH, SCREENHEIGHT)
point2 = transform.worldSpaceTransform(p2, SCREENWIDTH, SCREENHEIGHT)
point3 = transform.worldSpaceTransform(p3, SCREENWIDTH, SCREENHEIGHT)
point4 = transform.worldSpaceTransform(p4, SCREENWIDTH, SCREENHEIGHT)
and drew the points:
# STORING THE POINTS TO A TUPLE SO IT CAN BE DRAWN USING pygame.draw.lines
points = ((point1.x, point1.y), (point2.x, point2.y),
(point2.x, point2.y), (point3.x, point3.y),
(point3.x, point3.y), (point4.x, point4.y),
(point4.x, point4.y), (point1.x, point1.y))
pygame.draw.lines(D, (0, 0, 0), False, points)
Everything so far works (i think) because it draws a square, as it's supposed to.
Now the rotation. I tried rotation for all the axis and none of them work, but for the sake of being specific, i will talk about the x-axis. The following is the rotation class. I copied the rotation matrices from wikipedia. I am not completely sure about how they work so i also dont know if it is compatible with the system i described above.
def multVecMatrix(vec3, mat3):
# MULTIPLIES A Vec3 OBJECT WITH Mat3 OBJECT AND RETURNS A NEW Vec3 ?
x = vec3.x * mat3.matrix[0][0] + vec3.y * mat3.matrix[0][1] + vec3.z * mat3.matrix[0][2]
y = vec3.x * mat3.matrix[1][0] + vec3.y * mat3.matrix[1][1] + vec3.z * mat3.matrix[1][2]
z = vec3.x * mat3.matrix[2][0] + vec3.y * mat3.matrix[2][1] + vec3.z * mat3.matrix[2][2]
return Vec3(x, y, z)
class Rotation:
def rotateX(self, theta):
# ROTATION MATRIX IN X AXIS ??
sinTheta = sin(theta)
cosTheta = cos(theta)
m = Mat3()
m.matrix = [[1, 0, 0],
[0, cosTheta, sinTheta],
[0, -sinTheta, cosTheta]]
return m
def rotate(self, vec3, theta, axis=None):
# ROTATES A Vec3 BY GIVEN THETA AND AXIS ??
if axis == "x":
return multVecMatrix(vec3, self.rotateX(theta))
if axis == "y":
return multVecMatrix(vec3, self.rotateY(theta))
if axis == "z":
return multVecMatrix(vec3, self.rotateZ(theta))
And it is called like this after filling the screen white and before scaling the points from normalised space to screen space.
# screen is filled with white color
# ROTATING THE POINTS AROUND X AXIS ?????
p1.x = rotation.rotate(p1, thetax, axis='x').x
p1.y = rotation.rotate(p1, thetay, axis='x').y
p1.z = rotation.rotate(p1, thetax, axis='x').z
p2.x = rotation.rotate(p2, thetax, axis='x').x
p2.y = rotation.rotate(p2, thetay, axis='x').y
p2.z = rotation.rotate(p2, thetax, axis='x').z
p3.x = rotation.rotate(p3, thetax, axis='x').x
p3.y = rotation.rotate(p3, thetay, axis='x').y
p3.z = rotation.rotate(p3, thetax, axis='x').z
p4.x = rotation.rotate(p4, thetax, axis='x').x
p4.y = rotation.rotate(p4, thetay, axis='x').y
p4.z = rotation.rotate(p4, thetax, axis='x').z
# then the points are translated into world space
After the rotation is applied, it looks like it is moving and circling the x-axis but not rotating. I want it to rotate while staying where it is. What am i doing wrong?
Complete copy-and-paste code for reference:
import pygame
from math import sin, cos, radians
pygame.init()
### PYGAME STUFF ######################################
SCREENWIDTH = 600
SCREENHEIGHT = 600
D = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption("PRESS SPACE TO ROTATE AROUND X")
######### MATH FUNCTIONS AND CLASSES ####################
class Mat3:
# 3X3 MATRIX INITIALIZED WITH ALL 0's
def __init__(self):
self.matrix = [[0 for i in range(3)],
[0 for i in range(3)],
[0 for i in range(3)]]
class Vec2:
# 2D VECTOR
def __init__(self, x, y):
self.x = x
self.y = y
class Vec3:
# 3D VECTOR
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def multVecMatrix(vec3, mat3):
# MULTIPLIES A Vec3 OBJECT WITH Mat3 OBJECT AND RETURNS A NEW Vec3
x = vec3.x * mat3.matrix[0][0] + vec3.y * mat3.matrix[0][1] + vec3.z * mat3.matrix[0][2]
y = vec3.x * mat3.matrix[1][0] + vec3.y * mat3.matrix[1][1] + vec3.z * mat3.matrix[1][2]
z = vec3.x * mat3.matrix[2][0] + vec3.y * mat3.matrix[2][1] + vec3.z * mat3.matrix[1][2]
return Vec3(x, y, z)
class Transform:
# IT TRANSFORMS THE X AND Y FROM NORMALIZED SPACE TO SCREEN SPACE WITH PROJECTION APPLIED
def worldSpaceTransform(self, vec3, w, h):
if vec3.z == 0:
vec3.z = 0.001
zInverse = 1/ vec3.z
xTransformed = ((vec3.x * zInverse) + 1) * (w/2)
yTransformed = ((-vec3.y * zInverse) + 1) * (h/2)
xTransformed = str(xTransformed)[:6]
yTransformed = str(yTransformed)[:6]
return Vec2(float(xTransformed), float(yTransformed))
class Rotation:
def rotateX(self, theta):
# ROTATION MATRIX IN X AXIS
sinTheta = sin(theta)
cosTheta = cos(theta)
m = Mat3()
m.matrix = [[1, 0, 0],
[0, cosTheta, sinTheta],
[0, -sinTheta, cosTheta]]
return m
def rotate(self, vec3, theta, axis=None):
# ROTATES A Vec3 BY GIVEN THETA AND AXIS
if axis == "x":
return multVecMatrix(vec3, self.rotateX(theta))
if axis == "y":
return multVecMatrix(vec3, self.rotateY(theta))
if axis == "z":
return multVecMatrix(vec3, self.rotateZ(theta))
transform = Transform()
rotation = Rotation()
# ASSIGNING 4 Vec3's FOR 4 SIDES OF SQUARE IN NORMALIZED SPACE
s = 1
p1 = Vec3(-s, -s, -s)
p2 = Vec3(s, -s, -s)
p3 = Vec3(s, s, -s)
p4 = Vec3(-s, s, -s)
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
p1.z += 6
p2.z += 6
p3.z += 6
p4.z += 6
# ASSIGNING THE ROTATION ANGLES
thetax = 0
# APPLICATION LOOP
while True:
pygame.event.get()
D.fill((255, 255, 255))
# ROTATING THE POINTS AROUND X AXIS
p1.x = rotation.rotate(p1, thetax, axis='x').x
p1.y = rotation.rotate(p1, thetax, axis='x').y
p1.z = rotation.rotate(p1, thetax, axis='x').z
p2.x = rotation.rotate(p2, thetax, axis='x').x
p2.y = rotation.rotate(p2, thetax, axis='x').y
p2.z = rotation.rotate(p2, thetax, axis='x').z
p3.x = rotation.rotate(p3, thetax, axis='x').x
p3.y = rotation.rotate(p3, thetax, axis='x').y
p3.z = rotation.rotate(p3, thetax, axis='x').z
p4.x = rotation.rotate(p4, thetax, axis='x').x
p4.y = rotation.rotate(p4, thetax, axis='x').y
p4.z = rotation.rotate(p4, thetax, axis='x').z
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
point1 = transform.worldSpaceTransform(p1, SCREENWIDTH, SCREENHEIGHT)
point2 = transform.worldSpaceTransform(p2, SCREENWIDTH, SCREENHEIGHT)
point3 = transform.worldSpaceTransform(p3, SCREENWIDTH, SCREENHEIGHT)
point4 = transform.worldSpaceTransform(p4, SCREENWIDTH, SCREENHEIGHT)
# STORING THE POINTS TO A TUPLE SO IT CAN BE DRAWN USING pygame.draw.lines
points = ((point1.x, point1.y), (point2.x, point2.y),
(point2.x, point2.y), (point3.x, point3.y),
(point3.x, point3.y), (point4.x, point4.y),
(point4.x, point4.y), (point1.x, point1.y))
keys = pygame.key.get_pressed()
# ROTATE X ?
if keys[pygame.K_SPACE]:
thetax -= 0.005
pygame.draw.lines(D, (0, 0, 0), False, points)
pygame.display.flip()
It is not necessary to rotate each component of a vector separately. If you do
p1.x = rotation.rotate(p1, thetax, axis='x').x
then the x component of p1 has changed and the p1 which is passed to the next instruction is different
p1.y = rotation.rotate(p1, thetay, axis='x').y
It is sufficient to rotate the entire vertices once:
p1 = rotation.rotate(p1, thetax, axis='x')
p2 = rotation.rotate(p2, thetax, axis='x')
p3 = rotation.rotate(p3, thetax, axis='x')
p4 = rotation.rotate(p4, thetax, axis='x')
When you multiply a vector by a rotation matrix, then the vector is rotated a round (0, 0, 0). You have to do the translation after the rotation.
Add a +-operator to the Vec3 class:
class Vec3:
# 3D VECTOR
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __add__(a, b):
return Vec3(a.x+b.x, a.y+b.y, a.z+b.z)
Never change the original vertex coordinates p1, p2, p3 and p4. Compute the rotation and then the translation:
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
#p1.z += 6 <--- DELETE
#p2.z += 6
#p3.z += 6
#p4.z += 6
transVec = Vec3(0, 0, 6)
# [...]
while run:
# ROTATING THE POINTS AROUND X AXIS
point1 = rotation.rotate(p1, thetax, axis='x')
# [...]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
point1 = point1 + transVec
# [...]
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
point1 = transform.worldSpaceTransform(point1, SCREENWIDTH, SCREENHEIGHT)
# [...]
I recommend to organize the vertex coordinates in lists:
# ASSIGNING 4 Vec3's FOR 4 SIDES OF SQUARE IN NORMALIZED SPACE
s = 1
modelPoints = [Vec3(-s, -s, -s), Vec3(s, -s, -s), Vec3(s, s, -s), Vec3(-s, s, -s)]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
transVec = Vec3(0, 0, 6)
# ASSIGNING THE ROTATION ANGLES
thetax = 0
# APPLICATION LOOP
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
D.fill((255, 255, 255))
# ROTATING THE POINTS AROUND X AXIS
points = [rotation.rotate(pt, thetax, axis='x') for pt in modelPoints]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
points = [pt + transVec for pt in points]
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
points = [transform.worldSpaceTransform(pt, SCREENWIDTH, SCREENHEIGHT) for pt in points]
# STORING THE POINTS TO A TUPLE SO IT CAN BE DRAWN USING pygame.draw.lines
points = [(pt.x, pt.y) for pt in points]
See the complete example:
import pygame
from math import sin, cos, radians
pygame.init()
### PYGAME STUFF ######################################
SCREENWIDTH = 600
SCREENHEIGHT = 600
D = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
pygame.display.set_caption("PRESS SPACE TO ROTATE AROUND X")
######### MATH FUNCTIONS AND CLASSES ####################
class Mat3:
# 3X3 MATRIX INITIALIZED WITH ALL 0's
def __init__(self):
self.matrix = [[0 for i in range(3)],
[0 for i in range(3)],
[0 for i in range(3)]]
class Vec2:
# 2D VECTOR
def __init__(self, x, y):
self.x = x
self.y = y
class Vec3:
# 3D VECTOR
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __add__(a, b):
return Vec3(a.x+b.x, a.y+b.y, a.z+b.z)
def multVecMatrix(vec3, mat3):
# MULTIPLIES A Vec3 OBJECT WITH Mat3 OBJECT AND RETURNS A NEW Vec3
x = vec3.x * mat3.matrix[0][0] + vec3.y * mat3.matrix[0][1] + vec3.z * mat3.matrix[0][2]
y = vec3.x * mat3.matrix[1][0] + vec3.y * mat3.matrix[1][1] + vec3.z * mat3.matrix[1][2]
z = vec3.x * mat3.matrix[2][0] + vec3.y * mat3.matrix[2][1] + vec3.z * mat3.matrix[2][2]
return Vec3(x, y, z)
class Transform:
# IT TRANSFORMS THE X AND Y FROM NORMALIZED SPACE TO SCREEN SPACE WITH PROJECTION APPLIED
def worldSpaceTransform(self, vec3, w, h):
if vec3.z == 0:
vec3.z = 0.001
zInverse = 1/ vec3.z
xTransformed = ((vec3.x * zInverse) + 1) * (w/2)
yTransformed = ((-vec3.y * zInverse) + 1) * (h/2)
xTransformed = str(xTransformed)[:6]
yTransformed = str(yTransformed)[:6]
return Vec2(float(xTransformed), float(yTransformed))
class Rotation:
def rotateX(self, theta):
# ROTATION MATRIX IN X AXIS
sinTheta = sin(theta)
cosTheta = cos(theta)
m = Mat3()
m.matrix = [[1, 0, 0],
[0, cosTheta, sinTheta],
[0, -sinTheta, cosTheta]]
return m
def rotate(self, vec3, theta, axis=None):
# ROTATES A Vec3 BY GIVEN THETA AND AXIS
if axis == "x":
return multVecMatrix(vec3, self.rotateX(theta))
if axis == "y":
return multVecMatrix(vec3, self.rotateY(theta))
if axis == "z":
return multVecMatrix(vec3, self.rotateZ(theta))
transform = Transform()
rotation = Rotation()
# ASSIGNING 4 Vec3's FOR 4 SIDES OF SQUARE IN NORMALIZED SPACE
s = 1
modelPoints = [Vec3(-s, -s, -s), Vec3(s, -s, -s), Vec3(s, s, -s), Vec3(-s, s, -s)]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
transVec = Vec3(0, 0, 6)
# ASSIGNING THE ROTATION ANGLES
thetax = 0
# APPLICATION LOOP
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
D.fill((255, 255, 255))
# ROTATING THE POINTS AROUND X AXIS
points = [rotation.rotate(pt, thetax, axis='x') for pt in modelPoints]
# TRANSLATING THE POINTS OF THE CUBE A LITTLE BIT INTO THE SCREEN
points = [pt + transVec for pt in points]
# TRANSLATING THE SQUARE SHEET INTO THE SCREEN SPACE
points = [transform.worldSpaceTransform(pt, SCREENWIDTH, SCREENHEIGHT) for pt in points]
# STORING THE POINTS TO A TUPLE SO IT CAN BE DRAWN USING pygame.draw.lines
points = [(pt.x, pt.y) for pt in points]
keys = pygame.key.get_pressed()
# ROTATE X ?
if keys[pygame.K_SPACE]:
thetax -= 0.005
pygame.draw.lines(D, (0, 0, 0), True, points)
pygame.display.flip()
I wrote this function that draw a grid of triangles:
def create_triangles(side_length):
result = []
half_width = int(side_length / 2)
# height = int(side_length * math.sqrt(3) / 2)
height = side_length
max_width = 15 * side_length
max_height = 10 * height
for i in range(0, max_height, height):
if (i / height) % 2 == 0:
for j in range(0, max_width-half_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-half_width, i+height/2, j, i-height/2, j+half_width)
else:
triangle = (i-height/2, j, i+height/2, j+half_width, i+height/2, j-half_width)
result.append(triangle)
else:
for j in range(half_width, max_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-2*half_width, i+height/2, j-half_width+2, i-height/2, j)
else:
triangle = (i-height/2, j-half_width, i+height/2, j, i+height/2, j-2*half_width)
result.append(triangle)
return result
The current output is this:
As you can see some triangles are misaligned but I don't understand why.
As mentioned in the comments, floating points give you incorrect results; You want to make sure that the shared points representing the vertices of two adjacent triangles are concurrent. A simple approach is to reduce the points coordinates to ints, and organize the calculations so errors do not add up.
In the following examples, the misalignment is corrected, every triangle on the canvas is represented by a polygon, and individually drawn; each triangle can therefore be referenced when moused over, or addressed via an index, or a mapping (not implemented).
import tkinter as tk
import math
WIDTH, HEIGHT = 500, 500
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def tile_with_triangles(canvas, side_length=50):
"""tiles the entire surface of the canvas with triangular polygons
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='red')
p2 = Point(-triangle_height, half_side) # flip the model triangle
for idx, x in enumerate(range(-triangle_height, WIDTH+triangle_height+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='blue')
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='cyan')
canvas.pack()
tile_with_triangles(canvas) #, side_length=10)
root.mainloop()
I added an active fill property that will change the colors of each triangle when you mouse over.
I'm attempting to create a triangle tessellation like the following in Python:
All I've gotten is Sierpensky's triangle. I assume it'd use some of the same code.
import turtle as t
import math
import colorsys
t.hideturtle()
t.speed(0)
t.tracer(0,0)
h = 0
def draw_tri(x,y,size):
global h
t.up()
t.goto(x,y)
t.seth(0)
t.down()
color = colorsys.hsv_to_rgb(h,1,1)
h += 0.1
t.color(color)
t.left(120)
t.fd(size)
t.left(120)
t.fd(size)
t.end_fill()
def draw_s(x,y,size,n):
if n == 0:
draw_tri(x,y,size)
return
draw_s(x,y,size/2,n-1)
draw_s(x+size/2,y,size/2,n-1)
draw_s(x+size/4,y+size*math.sqrt(3)/4,size/2,n-1)
draw_s(-300,-250,600,6)
t.update()
There are various approaches; the following example generates all line segments prior to directing the turtle to draw them on the canvas.
import turtle as t
import math
WIDTH, HEIGHT = 800, 800
OFFSET = -WIDTH // 2, -HEIGHT // 2
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def get_line_segments(side_length=50):
"""calculates the coordinates of all vertices
organizes them by line segment
stores the segments in a container and returns it
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
segments = []
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
segments += [[pa, pb], [pb, pc], [pc, pa]]
return segments
def draw_segment(segment):
p0, p1 = segment
p0, p1 = p0 + offset, p1 + offset
t.penup()
t.goto(p0)
t.pendown()
t.goto(p1)
def draw_tiling():
for segment in get_line_segments():
draw_segment(segment)
t.hideturtle()
t.speed(0)
t.tracer(0,0)
offset = Point(*OFFSET)
draw_tiling()
t.update()
t.exitonclick()
If you want to see how the tiling is traced, you can replace the following lines:
# t.hideturtle()
t.speed(1)
# t.tracer(0, 0)
and enlarge the canvas screen with your mouse to see the boundary of the tiling (I made it overlap the standard size of the window)
As #ReblochonMasque notes, there are multiple approaches to the problem. Here's one I worked out to use as little turtle code as possible to solve the problem:
from turtle import Screen, Turtle
TRIANGLE_SIDE = 60
TRIANGLE_HEIGHT = TRIANGLE_SIDE * 3 ** 0.5 / 2
CURSOR_SIZE = 20
screen = Screen()
width = TRIANGLE_SIDE * (screen.window_width() // TRIANGLE_SIDE)
height = TRIANGLE_HEIGHT * (screen.window_height() // TRIANGLE_HEIGHT)
diagonal = width + height
turtle = Turtle('square', visible=False)
turtle.shapesize(diagonal / CURSOR_SIZE, 1 / CURSOR_SIZE)
turtle.penup()
turtle.sety(height/2)
turtle.setheading(270)
turtle = turtle.clone()
turtle.setx(width/2)
turtle.setheading(210)
turtle = turtle.clone()
turtle.setx(-width/2)
turtle.setheading(330)
for _ in range(int(diagonal / TRIANGLE_HEIGHT)):
for turtle in screen.turtles():
turtle.forward(TRIANGLE_HEIGHT)
turtle.stamp()
screen.exitonclick()
It probably could use optimizing but it gets the job done. And it's fun to watch...