Skip to content

Commit c8f6dba

Browse files
committed
fix return types
1 parent 1252170 commit c8f6dba

1 file changed

Lines changed: 107 additions & 49 deletions

File tree

graphics/interactive_3d_renderer.py

Lines changed: 107 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
https://en.wikipedia.org/wiki/3D_projection
1010
"""
1111

12-
import tkinter as tk
1312
import math
13+
import os
14+
import tkinter as tk
1415

1516

1617
class Vector3D:
@@ -38,46 +39,76 @@ class Vector3D:
3839
Vector3D(3.0, 2.0, -1.0)
3940
"""
4041

41-
def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0):
42+
def __init__(
43+
self,
44+
x_coordinate: float = 0.0,
45+
y_coordinate: float = 0.0,
46+
z_coordinate: float = 0.0,
47+
) -> None:
4248
"""Initialize a 3D vector."""
43-
self.x: float = x
44-
self.y: float = y
45-
self.z: float = z
49+
self.x_coordinate: float = x_coordinate
50+
self.y_coordinate: float = y_coordinate
51+
self.z_coordinate: float = z_coordinate
4652

4753
def __add__(self, other: "Vector3D") -> "Vector3D":
4854
"""Vector addition."""
49-
return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z)
55+
return Vector3D(
56+
self.x_coordinate + other.x_coordinate,
57+
self.y_coordinate + other.y_coordinate,
58+
self.z_coordinate + other.z_coordinate,
59+
)
5060

5161
def __sub__(self, other: "Vector3D") -> "Vector3D":
5262
"""Vector subtraction."""
53-
return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z)
63+
return Vector3D(
64+
self.x_coordinate - other.x_coordinate,
65+
self.y_coordinate - other.y_coordinate,
66+
self.z_coordinate - other.z_coordinate,
67+
)
5468

5569
def __mul__(self, scalar: float) -> "Vector3D":
5670
"""Scalar multiplication."""
57-
return Vector3D(self.x * scalar, self.y * scalar, self.z * scalar)
71+
return Vector3D(
72+
self.x_coordinate * scalar,
73+
self.y_coordinate * scalar,
74+
self.z_coordinate * scalar,
75+
)
5876

5977
def dot(self, other: "Vector3D") -> float:
6078
"""Dot product of two vectors."""
61-
return self.x * other.x + self.y * other.y + self.z * other.z
79+
return (
80+
self.x_coordinate * other.x_coordinate
81+
+ self.y_coordinate * other.y_coordinate
82+
+ self.z_coordinate * other.z_coordinate
83+
)
6284

6385
def cross(self, other: "Vector3D") -> "Vector3D":
6486
"""Cross product of two vectors."""
6587
return Vector3D(
66-
self.y * other.z - self.z * other.y,
67-
self.z * other.x - self.x * other.z,
68-
self.x * other.y - self.y * other.x,
88+
self.y_coordinate * other.z_coordinate
89+
- self.z_coordinate * other.y_coordinate,
90+
self.z_coordinate * other.x_coordinate
91+
- self.x_coordinate * other.z_coordinate,
92+
self.x_coordinate * other.y_coordinate
93+
- self.y_coordinate * other.x_coordinate,
6994
)
7095

7196
def magnitude(self) -> float:
7297
"""Return the magnitude (length) of the vector."""
73-
return math.sqrt(self.x**2 + self.y**2 + self.z**2)
98+
return math.sqrt(
99+
self.x_coordinate**2 + self.y_coordinate**2 + self.z_coordinate**2
100+
)
74101

75102
def normalize(self) -> "Vector3D":
76103
"""Return a normalized (unit length) vector."""
77104
magnitude = self.magnitude()
78105
if magnitude == 0:
79106
return Vector3D(0, 0, 0)
80-
return Vector3D(self.x / magnitude, self.y / magnitude, self.z / magnitude)
107+
return Vector3D(
108+
self.x_coordinate / magnitude,
109+
self.y_coordinate / magnitude,
110+
self.z_coordinate / magnitude,
111+
)
81112

82113
def rotate(
83114
self, angle_x: float = 0.0, angle_y: float = 0.0, angle_z: float = 0.0
@@ -93,9 +124,9 @@ def rotate(
93124
ay = math.radians(angle_y)
94125
az = math.radians(angle_z)
95126
# Yaw (Y)
96-
x = self.x * math.cos(ay) + self.z * math.sin(ay)
97-
z = -self.x * math.sin(ay) + self.z * math.cos(ay)
98-
y = self.y
127+
x = self.x_coordinate * math.cos(ay) + self.z_coordinate * math.sin(ay)
128+
z = -self.x_coordinate * math.sin(ay) + self.z_coordinate * math.cos(ay)
129+
y = self.y_coordinate
99130
# Pitch (X)
100131
y2 = y * math.cos(ax) - z * math.sin(ax)
101132
z2 = y * math.sin(ax) + z * math.cos(ax)
@@ -108,7 +139,9 @@ def rotate(
108139

109140
def __repr__(self) -> str:
110141
"""String representation of the vector."""
111-
return f"Vector3D({self.x}, {self.y}, {self.z})"
142+
return (
143+
f"Vector3D({self.x_coordinate}, {self.y_coordinate}, {self.z_coordinate})"
144+
)
112145

113146

114147
class Mesh:
@@ -124,14 +157,14 @@ class Mesh:
124157
Vector3D(0.0, 0.0, 1.0)
125158
"""
126159

127-
def __init__(self):
160+
def __init__(self) -> None:
128161
self.vertices: list[Vector3D] = []
129162
self.triangles: list[tuple[int, int, int]] = []
130163
self.normals: list[Vector3D] = []
131164
self.position: Vector3D = Vector3D(0, 0, 0)
132165
self.rotation: Vector3D = Vector3D(0, 0, 0)
133166

134-
def calculate_normals(self):
167+
def calculate_normals(self) -> None:
135168
"""
136169
Recalculate the normals for every triangle (call after modifying geometry).
137170
@@ -163,7 +196,7 @@ class Cube(Mesh):
163196
Vector3D(0.0, 0.0, 1.0)
164197
"""
165198

166-
def __init__(self):
199+
def __init__(self) -> None:
167200
super().__init__()
168201
self.vertices = [
169202
Vector3D(0, 0, 0),
@@ -215,19 +248,19 @@ class Camera:
215248
Vector3D(1, 2, 13)
216249
"""
217250

218-
def __init__(self, position: Vector3D, fov: float = 90):
251+
def __init__(self, position: Vector3D, fov: float = 90) -> None:
219252
self.position = position
220253
self.fov = fov
221254
self.yaw = 0.0
222255
self.pitch = 0.0
223256

224-
def move(self, dx: float = 0.0, dy: float = 0.0, dz: float = 0.0):
257+
def move(self, dx: float, dy: float, dz: float) -> None:
225258
"""Move the camera in 3D."""
226-
self.position.x += dx
227-
self.position.y += dy
228-
self.position.z += dz
259+
self.position.x_coordinate += dx
260+
self.position.y_coordinate += dy
261+
self.position.z_coordinate += dz
229262

230-
def rotate(self, dyaw=0.0, dpitch=0.0):
263+
def rotate(self, dyaw: float = 0.0, dpitch: float = 0.0) -> None:
231264
"""Rotate camera view direction by given yaw/pitch degrees."""
232265
self.yaw += dyaw
233266
self.pitch += dpitch
@@ -250,7 +283,7 @@ def get_view_direction(self) -> Vector3D:
250283

251284
def project_point(
252285
point: Vector3D, camera: Camera, canvas_width: int, canvas_height: int
253-
):
286+
) -> tuple[float, float]:
254287
"""
255288
Projects a 3D point to 2D screen coordinates using the camera's perspective.
256289
@@ -292,7 +325,12 @@ class GraphicsWindow:
292325
- Shift/Space: Move camera up/down
293326
"""
294327

295-
def __init__(self, width=400, height=300, title="Tkinter Graphics Window"):
328+
def __init__(
329+
self,
330+
width: int = 400,
331+
height: int = 300,
332+
title: int = "Tkinter Graphics Window",
333+
) -> None:
296334
self.root = tk.Tk()
297335
self.root.title(title)
298336
self.width = width
@@ -312,26 +350,34 @@ def __init__(self, width=400, height=300, title="Tkinter Graphics Window"):
312350
cube.position = Vector3D(-0.5, -0.5, 3)
313351
self.meshes.append(cube)
314352

315-
def on_key(self, event):
353+
def on_key(self, event: tk.Event) -> None:
316354
"""
317355
Handle keyboard controls for interactive camera movement and rotation.
318356
"""
319357
step = 0.22
320358
angle_step = 3
321359
if event.keysym == "w":
322360
move = self.camera.get_view_direction() * step
323-
self.camera.move(move.x, move.y, move.z)
361+
self.camera.move(move.x_coordinate, move.y_coordinate, move.z_coordinate)
324362
elif event.keysym == "s":
325363
move = self.camera.get_view_direction() * -step
326-
self.camera.move(move.x, move.y, move.z)
364+
self.camera.move(move.x_coordinate, move.y_coordinate, move.z_coordinate)
327365
elif event.keysym == "a":
328366
view_dir = self.camera.get_view_direction()
329367
right = view_dir.cross(Vector3D(0, 1, 0)).normalize()
330-
self.camera.move(-right.x * step, -right.y * step, -right.z * step)
368+
self.camera.move(
369+
-right.x_coordinate * step,
370+
-right.y_coordinate * step,
371+
-right.z_coordinate * step,
372+
)
331373
elif event.keysym == "d":
332374
view_dir = self.camera.get_view_direction()
333375
right = view_dir.cross(Vector3D(0, 1, 0)).normalize()
334-
self.camera.move(right.x * step, right.y * step, right.z * step)
376+
self.camera.move(
377+
right.x_coordinate * step,
378+
right.y_coordinate * step,
379+
right.z_coordinate * step,
380+
)
335381
elif event.keysym == "space":
336382
self.camera.move(0, -step, 0)
337383
elif event.keysym == "Shift_L":
@@ -345,28 +391,28 @@ def on_key(self, event):
345391
elif event.keysym == "Right":
346392
self.camera.rotate(dyaw=angle_step)
347393

348-
def on_resize(self, event):
394+
def on_resize(self, event: tk.Event) -> None:
349395
"""Resize canvas and update projection parameters on window resize."""
350396
if event.widget == self.root:
351397
self.width = event.width
352398
self.height = event.height
353399
self.canvas.config(width=self.width, height=self.height)
354400

355-
def update(self):
401+
def update(self) -> None:
356402
"""
357403
Animate mesh rotation or handle other per-frame updates.
358404
359405
Example:
360406
for mesh in self.meshes:
361-
mesh.rotation.y += 2
407+
mesh.rotation.y_coordinate += 2
362408
"""
363409
for mesh in self.meshes:
364-
mesh.rotation.y += 2
410+
mesh.rotation.y_coordinate += 2
365411

366-
def mainloop(self):
412+
def mainloop(self) -> None:
367413
"""Start the animation and rendering loop."""
368414

369-
def loop():
415+
def loop() -> None:
370416
if self.running:
371417
self.update()
372418
self.canvas.delete("all")
@@ -376,12 +422,12 @@ def loop():
376422
loop()
377423
self.root.mainloop()
378424

379-
def close(self):
425+
def close(self) -> None:
380426
"""Close the Tkinter window and stop the rendering loop."""
381427
self.running = False
382428
self.root.destroy()
383429

384-
def render(self):
430+
def render(self) -> None:
385431
"""
386432
Render all meshes with current camera and lighting.
387433
@@ -393,7 +439,9 @@ def render(self):
393439
for i, tri in enumerate(mesh.triangles):
394440
# Rotate normal for lighting/culling
395441
normal = mesh.normals[i].rotate(
396-
mesh.rotation.x, mesh.rotation.y, mesh.rotation.z
442+
mesh.rotation.x_coordinate,
443+
mesh.rotation.y_coordinate,
444+
mesh.rotation.z_coordinate,
397445
)
398446
# Camera looks along -Z
399447
view_dir = self.camera.get_view_direction()
@@ -407,19 +455,25 @@ def render(self):
407455

408456
v1 = (
409457
mesh.vertices[tri[0]].rotate(
410-
mesh.rotation.x, mesh.rotation.y, mesh.rotation.z
458+
mesh.rotation.x_coordinate,
459+
mesh.rotation.y_coordinate,
460+
mesh.rotation.z_coordinate,
411461
)
412462
+ mesh.position
413463
)
414464
v2 = (
415465
mesh.vertices[tri[1]].rotate(
416-
mesh.rotation.x, mesh.rotation.y, mesh.rotation.z
466+
mesh.rotation.x_coordinate,
467+
mesh.rotation.y_coordinate,
468+
mesh.rotation.z_coordinate,
417469
)
418470
+ mesh.position
419471
)
420472
v3 = (
421473
mesh.vertices[tri[2]].rotate(
422-
mesh.rotation.x, mesh.rotation.y, mesh.rotation.z
474+
mesh.rotation.x_coordinate,
475+
mesh.rotation.y_coordinate,
476+
mesh.rotation.z_coordinate,
423477
)
424478
+ mesh.position
425479
)
@@ -437,7 +491,7 @@ def render(self):
437491
avg_z = (z1 + z2 + z3) / 3.0
438492
to_draw.append((avg_z, (x1, y1, x2, y2, x3, y3), hex_color))
439493

440-
to_draw.sort(key=lambda t: t[0], reverse=True)
494+
to_draw.sort(key=lambda triangle: triangle[0], reverse=True)
441495
for _, verts, color in to_draw:
442496
self.canvas.create_polygon(*verts, outline="", fill=color, width=1)
443497

@@ -447,5 +501,9 @@ def render(self):
447501
Launch the interactive 3D cube renderer.
448502
A window will appear; use keyboard controls to move and rotate camera.
449503
"""
450-
win = GraphicsWindow()
451-
win.mainloop()
504+
# Only run GUI if display is available
505+
if os.environ.get("DISPLAY") or os.name == "nt":
506+
win = GraphicsWindow()
507+
win.mainloop()
508+
else:
509+
print("No display detected. Skipping GUI launch.")

0 commit comments

Comments
 (0)