Skip to content

Commit 4cd77b6

Browse files
Add type hints across the Pretzel package
Annotate all function signatures, mark module-level constants as Final, and reuse the geometry aliases from pretzel.engine. The make_assets script gets its own local aliases. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7bd8227 commit 4cd77b6

10 files changed

Lines changed: 112 additions & 57 deletions

File tree

python315-sampling-profiler/examples/fetch_textures.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@
99
}
1010

1111

12-
async def download_texture(name, latency):
12+
async def download_texture(name: str, latency: float) -> bytes:
1313
await asyncio.sleep(latency)
1414
return name.encode() * 100_000
1515

1616

17-
async def decode_texture(payload):
17+
async def decode_texture(payload: bytes) -> int:
1818
checksum = 0
1919
for byte in payload:
2020
checksum = (checksum * 31 + byte) % 1_000_003
2121
return checksum
2222

2323

24-
async def main():
24+
async def main() -> None:
2525
async with asyncio.TaskGroup() as group:
2626
downloads = {
2727
name: group.create_task(

python315-sampling-profiler/make_assets.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,56 @@
88

99
import math
1010
import pathlib
11+
from typing import Final
12+
13+
type Vector = tuple[float, float, float]
14+
type Face = tuple[int, int, int]
15+
type Color = tuple[int, int, int]
1116

1217
ASSETS_DIR = pathlib.Path(__file__).parent / "src" / "pretzel" / "assets"
1318

14-
SEGMENTS = 240
15-
RING_POINTS = 16
16-
TUBE_RADIUS = 0.62
19+
SEGMENTS: Final = 240
20+
RING_POINTS: Final = 16
21+
TUBE_RADIUS: Final = 0.62
1722

18-
WALLPAPER_SIZE = 512
19-
WALLPAPERS = {
23+
WALLPAPER_SIZE: Final = 512
24+
WALLPAPERS: Final = {
2025
"bakery_dawn.ppm": ((252, 210, 153), (146, 90, 118), (255, 236, 179)),
2126
"bakery_noon.ppm": ((214, 235, 251), (245, 232, 201), (255, 255, 224)),
2227
"bakery_dusk.ppm": ((94, 63, 107), (233, 156, 90), (255, 214, 138)),
2328
}
2429

2530

26-
def trace_trefoil(t):
31+
def trace_trefoil(t: float) -> Vector:
2732
return (
2833
math.sin(t) + 2.0 * math.sin(2.0 * t),
2934
math.cos(t) - 2.0 * math.cos(2.0 * t),
3035
-math.sin(3.0 * t) * 1.2,
3136
)
3237

3338

34-
def subtract(a, b):
39+
def subtract(a: Vector, b: Vector) -> Vector:
3540
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
3641

3742

38-
def cross(a, b):
43+
def cross(a: Vector, b: Vector) -> Vector:
3944
return (
4045
a[1] * b[2] - a[2] * b[1],
4146
a[2] * b[0] - a[0] * b[2],
4247
a[0] * b[1] - a[1] * b[0],
4348
)
4449

4550

46-
def dot(a, b):
51+
def dot(a: Vector, b: Vector) -> float:
4752
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
4853

4954

50-
def normalize(vector):
55+
def normalize(vector: Vector) -> Vector:
5156
length = math.sqrt(dot(vector, vector)) or 1.0
5257
return (vector[0] / length, vector[1] / length, vector[2] / length)
5358

5459

55-
def sweep_tube():
60+
def sweep_tube() -> tuple[list[Vector], list[Face]]:
5661
step = 2.0 * math.pi / SEGMENTS
5762
centers = [trace_trefoil(index * step) for index in range(SEGMENTS)]
5863
tangents = [
@@ -96,7 +101,7 @@ def sweep_tube():
96101
return vertices, faces
97102

98103

99-
def write_mesh(path):
104+
def write_mesh(path: pathlib.Path) -> None:
100105
vertices, faces = sweep_tube()
101106
with path.open("w", encoding="utf-8") as file:
102107
file.write(f"# Trefoil-knot pretzel: {len(vertices)} vertices,")
@@ -108,14 +113,16 @@ def write_mesh(path):
108113
print(f"Wrote {path} ({len(vertices)} vertices, {len(faces)} faces)")
109114

110115

111-
def blend(low, high, amount):
116+
def blend(low: Color, high: Color, amount: float) -> Color:
112117
return tuple(
113118
round(low[channel] + (high[channel] - low[channel]) * amount)
114119
for channel in range(3)
115120
)
116121

117122

118-
def write_wallpaper(path, top, bottom, glow):
123+
def write_wallpaper(
124+
path: pathlib.Path, top: Color, bottom: Color, glow: Color
125+
) -> None:
119126
size = WALLPAPER_SIZE
120127
lights = [
121128
(size * 0.25, size * 0.3, size * 0.22),
@@ -139,7 +146,7 @@ def write_wallpaper(path, top, bottom, glow):
139146
print(f"Wrote {path} ({size}x{size})")
140147

141148

142-
def main():
149+
def main() -> None:
143150
ASSETS_DIR.mkdir(parents=True, exist_ok=True)
144151
write_mesh(ASSETS_DIR / "pretzel.mdl")
145152
for name, (top, bottom, glow) in WALLPAPERS.items():

python315-sampling-profiler/src/pretzel/assets.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
from dataclasses import dataclass
44
from importlib.resources import files
5+
from typing import BinaryIO
56

67
import numpy as np
78
import numpy.typing as npt
89

9-
from pretzel.engine import Mesh
10+
from pretzel.engine import Face, Mesh, Vertex
1011

1112
ASSETS_DIR = files("pretzel") / "assets"
1213

@@ -18,12 +19,13 @@ class Background:
1819
pixels: npt.NDArray[np.float32]
1920

2021

21-
def asset_path(name):
22+
def asset_path(name: str) -> str:
2223
return str(ASSETS_DIR / name)
2324

2425

25-
def load_mesh(name):
26-
vertices, faces = [], []
26+
def load_mesh(name: str) -> Mesh:
27+
vertices: list[Vertex] = []
28+
faces: list[Face] = []
2729
with open(asset_path(name), encoding="utf-8") as file:
2830
for line in file:
2931
kind, _, rest = line.partition(" ")
@@ -36,13 +38,13 @@ def load_mesh(name):
3638
return Mesh(vertices, faces)
3739

3840

39-
def load_background(name, fast=False):
41+
def load_background(name: str, fast: bool = False) -> Background:
4042
path = asset_path(name)
4143
decode = decode_ppm if fast else decode_ppm_pixel_by_pixel
4244
return Background(name=name, path=path, pixels=decode(path))
4345

4446

45-
def decode_ppm_pixel_by_pixel(path):
47+
def decode_ppm_pixel_by_pixel(path: str) -> npt.NDArray[np.float32]:
4648
with open(path, "rb") as file:
4749
width, height = read_ppm_header(file)
4850
rows = []
@@ -58,11 +60,11 @@ def decode_ppm_pixel_by_pixel(path):
5860
return np.array(rows, dtype=np.float32)
5961

6062

61-
def to_linear(value):
63+
def to_linear(value: int) -> float:
6264
return 255.0 * (value / 255.0) ** 2.2
6365

6466

65-
def decode_ppm(path):
67+
def decode_ppm(path: str) -> npt.NDArray[np.float32]:
6668
with open(path, "rb") as file:
6769
width, height = read_ppm_header(file)
6870
data = file.read(width * height * 3)
@@ -71,7 +73,7 @@ def decode_ppm(path):
7173
return 255.0 * (pixels / 255.0) ** 2.2
7274

7375

74-
def read_ppm_header(file):
76+
def read_ppm_header(file: BinaryIO) -> tuple[int, int]:
7577
magic = file.readline().strip()
7678
if magic != b"P6":
7779
raise ValueError(f"{file.name} is not a binary PPM file")

python315-sampling-profiler/src/pretzel/cli.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22

33
import argparse
44
import time
5+
from typing import Final
56

67
from pretzel import assets
8+
from pretzel.engine import Mesh
79
from pretzel.render import compute_frame
810
from pretzel.streaming import BackgroundStreamer
911
from pretzel.telemetry import FrameLog
1012

11-
MODEL = "pretzel.mdl"
13+
MODEL: Final = "pretzel.mdl"
1214

1315

14-
def main():
16+
def main() -> None:
1517
parser = argparse.ArgumentParser(prog="pretzel", description=__doc__)
1618
parser.add_argument(
1719
"--frames",
@@ -47,15 +49,22 @@ def main():
4749
frame_log.close()
4850

4951

50-
def show_window(mesh, streamer, frame_log):
52+
def show_window(
53+
mesh: Mesh, streamer: BackgroundStreamer, frame_log: FrameLog
54+
) -> None:
5155
from pretzel.viewer import Viewer
5256

5357
Viewer(mesh, streamer, frame_log).run()
5458

5559

5660
def render_headlessly(
57-
mesh, streamer, frame_log, frames, cache_colors=False, size=512
58-
):
61+
mesh: Mesh,
62+
streamer: BackgroundStreamer,
63+
frame_log: FrameLog,
64+
frames: int,
65+
cache_colors: bool = False,
66+
size: int = 512,
67+
) -> None:
5968
started = time.perf_counter()
6069
for frame in range(frames):
6170
frame_started = time.perf_counter()

python315-sampling-profiler/src/pretzel/engine.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Mesh:
1919
faces: list[Face]
2020

2121

22-
def rotation_matrix(pitch, yaw, roll):
22+
def rotation_matrix(pitch: float, yaw: float, roll: float) -> Matrix:
2323
sin_p, cos_p = math.sin(pitch), math.cos(pitch)
2424
sin_y, cos_y = math.sin(yaw), math.cos(yaw)
2525
sin_r, cos_r = math.sin(roll), math.cos(roll)
@@ -38,7 +38,9 @@ def rotation_matrix(pitch, yaw, roll):
3838
)
3939

4040

41-
def transform_vertices(vertices, matrix, distance):
41+
def transform_vertices(
42+
vertices: list[Vertex], matrix: Matrix, distance: float
43+
) -> list[Vertex]:
4244
(xx, xy, xz), (yx, yy, yz), (zx, zy, zz) = matrix
4345
transformed = []
4446
for x, y, z in vertices:
@@ -49,7 +51,9 @@ def transform_vertices(vertices, matrix, distance):
4951
return transformed
5052

5153

52-
def project_vertices(transformed, width, height, focal_length):
54+
def project_vertices(
55+
transformed: list[Vertex], width: int, height: int, focal_length: float
56+
) -> list[Point]:
5357
half_width, half_height = width / 2, height / 2
5458
projected = []
5559
for x, y, z in transformed:
@@ -58,7 +62,7 @@ def project_vertices(transformed, width, height, focal_length):
5862
return projected
5963

6064

61-
def cull_backfaces(faces, projected):
65+
def cull_backfaces(faces: list[Face], projected: list[Point]) -> list[Face]:
6266
visible = []
6367
for face in faces:
6468
a, b, c = face
@@ -70,8 +74,10 @@ def cull_backfaces(faces, projected):
7074
return visible
7175

7276

73-
def sort_back_to_front(faces, transformed):
74-
def face_depth(face):
77+
def sort_back_to_front(
78+
faces: list[Face], transformed: list[Vertex]
79+
) -> list[Face]:
80+
def face_depth(face: Face) -> float:
7581
a, b, c = face
7682
return transformed[a][2] + transformed[b][2] + transformed[c][2]
7783

python315-sampling-profiler/src/pretzel/render.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
"""Turn a mesh, a background, and a clock tick into shaded polygons."""
22

33
import math
4+
from typing import Final
45

56
from pretzel import engine, shading
7+
from pretzel.assets import Background
8+
from pretzel.engine import Mesh
69

7-
CAMERA_DISTANCE = 7.0
10+
CAMERA_DISTANCE: Final = 7.0
811

12+
type Polygon = tuple[list[float], str]
913

10-
def compute_frame(mesh, background, tick, width, height, cache_colors=False):
14+
15+
def compute_frame(
16+
mesh: Mesh,
17+
background: Background,
18+
tick: float,
19+
width: int,
20+
height: int,
21+
cache_colors: bool = False,
22+
) -> list[Polygon]:
1123
matrix = engine.rotation_matrix(
1224
pitch=0.45 * math.sin(tick * 0.6),
1325
yaw=tick * 0.9,
@@ -23,9 +35,9 @@ def compute_frame(mesh, background, tick, width, height, cache_colors=False):
2335
ordered = engine.sort_back_to_front(visible, transformed)
2436
ambient = shading.sample_ambient_light(background.pixels, tick)
2537
channels = shading.shade_faces(ordered, transformed, ambient)
26-
polygons = []
38+
polygons: list[Polygon] = []
2739
for face, (red, green, blue) in zip(ordered, channels):
28-
coordinates = []
40+
coordinates: list[float] = []
2941
for index in face:
3042
coordinates.extend(projected[index])
3143
polygons.append(
@@ -39,10 +51,10 @@ def compute_frame(mesh, background, tick, width, height, cache_colors=False):
3951
return polygons
4052

4153

42-
_HEX_CACHE = {}
54+
_HEX_CACHE: dict[tuple[int, int, int], str] = {}
4355

4456

45-
def hex_color(red, green, blue):
57+
def hex_color(red: int, green: int, blue: int) -> str:
4658
key = (red, green, blue)
4759
color = _HEX_CACHE.get(key)
4860
if color is None:

python315-sampling-profiler/src/pretzel/shading.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
"""NumPy-powered lighting. Most of this work runs in native code."""
22

33
import numpy as np
4+
import numpy.typing as npt
5+
6+
from pretzel.engine import Face, Vertex
47

58
LIGHT_DIRECTION = np.array([0.35, 0.55, -0.75])
69
LIGHT_DIRECTION /= np.linalg.norm(LIGHT_DIRECTION)
710

811
CRUST_COLOR = np.array([224.0, 147.0, 65.0])
912

1013

11-
def sample_ambient_light(pixels, tick):
14+
def sample_ambient_light(
15+
pixels: npt.NDArray[np.float32], tick: float
16+
) -> float:
1217
samples = pixels[::2, ::2]
1318
height, width = samples.shape[:2]
1419
rows = np.arange(height, dtype=np.float32)[:, np.newaxis]
@@ -18,7 +23,9 @@ def sample_ambient_light(pixels, tick):
1823
return 0.15 + 0.3 * float(lit.mean()) / 255.0
1924

2025

21-
def shade_faces(faces, transformed, ambient):
26+
def shade_faces(
27+
faces: list[Face], transformed: list[Vertex], ambient: float
28+
) -> list[list[int]]:
2229
vertices = np.asarray(transformed)
2330
corners = vertices[np.asarray(faces)]
2431
edges_ab = corners[:, 1] - corners[:, 0]

0 commit comments

Comments
 (0)