99https://en.wikipedia.org/wiki/3D_projection
1010"""
1111
12- import tkinter as tk
1312import math
13+ import os
14+ import tkinter as tk
1415
1516
1617class 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
114147class 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
251284def 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