e2D 2.0.0__cp313-cp313-win_amd64.whl → 2.0.2__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
e2D/types.pyi ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ Type stubs for types module
3
+ Common type definitions for e2D library
4
+ """
5
+
6
+ from typing import Union, Sequence
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ import moderngl
10
+ from .cvectors import Vector2D
11
+ from glfw import _GLFWwindow
12
+
13
+ # Type alias for vector-like objects - use this for ALL 2D vectors, positions, and sizes
14
+ # Accepts Vector2D instances, tuples (x, y), or lists [x, y]
15
+ VectorType = Union[Vector2D, tuple[int | float, int | float], Sequence[int | float]]
16
+
17
+ # Color type (RGBA values between 0.0 and 1.0)
18
+ ColorType = tuple[float, float, float, float]
19
+
20
+ # Numeric types
21
+ Number = int | float
22
+ IntVec2 = tuple[int, int]
23
+ FloatVec2 = tuple[float, float]
24
+ NumVec2 = tuple[Number, Number]
25
+
26
+ # Array-like types for numpy and buffers
27
+ pArray = list | tuple
28
+ ArrayLike = npt.NDArray[np.float32] | npt.NDArray[np.float64] | npt.NDArray[np.int32]
29
+
30
+ # Shader and GPU resource types
31
+ ContextType = moderngl.Context
32
+ ProgramType = moderngl.Program
33
+ ComputeShaderType = moderngl.ComputeShader
34
+ BufferType = moderngl.Buffer
35
+ VAOType = moderngl.VertexArray
36
+ TextureType = moderngl.Texture
37
+
38
+ ProgramAttrType = moderngl.Uniform | moderngl.UniformBlock | moderngl.Attribute | moderngl.Varying
39
+ UniformType = moderngl.Uniform
40
+ UniformBlockType = moderngl.UniformBlock
41
+
42
+ # Window type
43
+ WindowType = _GLFWwindow
44
+
45
+
46
+ __all__ = [
47
+ 'VectorType',
48
+ 'Vector2D',
49
+ 'ColorType',
50
+ 'Number',
51
+ 'IntVec2',
52
+ 'FloatVec2',
53
+ 'NumVec2',
54
+ 'ArrayLike',
55
+ 'ContextType',
56
+ 'ProgramType',
57
+ 'BufferType',
58
+ 'VAOType',
59
+ 'TextureType',
60
+ 'WindowType',
61
+ ]
e2D/vectors.py CHANGED
@@ -3,73 +3,164 @@ High-level Python wrapper for Vector2D with additional utilities
3
3
  Provides compatibility layer and convenience functions
4
4
  """
5
5
 
6
- try:
7
- from .cvectors import (
8
- Vector2D,
9
- batch_add_inplace,
10
- batch_scale_inplace,
11
- batch_normalize_inplace,
12
- vectors_to_array,
13
- array_to_vectors,
14
- )
15
- _COMPILED = True
16
- except ImportError:
17
- print("WARNING: Compiled cvectors module not found. Please run: python setup.py build_ext --inplace")
18
- print("Falling back to pure Python implementation (slower)")
19
- _COMPILED = False
20
-
21
- # Fallback implementation
22
- import numpy as np
23
-
24
- class Vector2D:
25
- """Pure Python fallback (much slower than compiled version)"""
26
- def __init__(self, x=0.0, y=0.0):
27
- self.data = np.array([x, y], dtype=np.float64)
28
-
29
- @property
30
- def x(self):
31
- return self.data[0]
32
-
33
- @x.setter
34
- def x(self, value):
35
- self.data[0] = value
36
-
37
- @property
38
- def y(self):
39
- return self.data[1]
40
-
41
- @y.setter
42
- def y(self, value):
43
- self.data[1] = value
44
-
45
- @property
46
- def length(self):
47
- return np.linalg.norm(self.data)
6
+ from typing import Any, List, Literal, Protocol, runtime_checkable, TYPE_CHECKING, Sequence, cast
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+
10
+ # Define a protocol that both implementations must follow
11
+ @runtime_checkable
12
+ class Vector2DProtocol(Protocol):
13
+ """Protocol defining the Vector2D interface for type checking"""
14
+ x: float
15
+ y: float
16
+
17
+ def __init__(self, x: float = 0.0, y: float = 0.0) -> None: ...
18
+ def copy(self) -> "Vector2DProtocol": ...
19
+ def normalize(self) -> None: ...
20
+ def __add__(self, other: Any) -> "Vector2DProtocol": ...
21
+ def __sub__(self, other: Any) -> "Vector2DProtocol": ...
22
+ def __mul__(self, other: Any) -> "Vector2DProtocol": ...
23
+ def __getitem__(self, idx: int) -> float: ...
24
+ def __setitem__(self, idx: int, value: float) -> None: ...
25
+ def __len__(self) -> int: ...
26
+ @property
27
+ def length(self) -> float: ...
28
+ @property
29
+ def length_sqrd(self) -> float: ...
30
+
31
+ # For type checking - define stubs
32
+ if TYPE_CHECKING:
33
+ # Constructor function for type checking
34
+ def Vector2D(x: float = 0.0, y: float = 0.0) -> Vector2DProtocol: ... # noqa: F811
35
+
36
+ # Type stubs for batch operations
37
+ def batch_add_inplace(vectors: Sequence[Vector2DProtocol], displacement: Vector2DProtocol) -> None: ...
38
+ def batch_scale_inplace(vectors: Sequence[Vector2DProtocol], scalar: float) -> None: ...
39
+ def batch_normalize_inplace(vectors: Sequence[Vector2DProtocol]) -> None: ...
40
+ def vectors_to_array(vectors: Sequence[Vector2DProtocol]) -> NDArray[np.float64]: ...
41
+ def array_to_vectors(arr: NDArray[np.float64]) -> list[Vector2DProtocol]: ...
42
+
43
+ # Try to import the optimized Cython version, fall back to pure Python
44
+ _COMPILED = False
45
+ if not TYPE_CHECKING:
46
+ try:
47
+ from .cvectors import ( # type: ignore[assignment]
48
+ Vector2D,
49
+ batch_add_inplace,
50
+ batch_scale_inplace,
51
+ batch_normalize_inplace,
52
+ vectors_to_array,
53
+ array_to_vectors,
54
+ )
55
+ _COMPILED = True
56
+ except ImportError:
57
+ print("WARNING: Compiled cvectors module not found. Please run: python setup.py build_ext --inplace")
58
+ print("Falling back to pure Python implementation (slower)")
59
+ _COMPILED = False
48
60
 
49
- @property
50
- def length_sqrd(self):
51
- return np.dot(self.data, self.data)
61
+ # Fallback pure Python implementation
62
+ class Vector2D: # type: ignore[no-redef]
63
+ """Pure Python fallback (much slower than compiled version)"""
64
+ def __init__(self, x: float = 0.0, y: float = 0.0) -> None:
65
+ self.data = np.array([x, y], dtype=np.float64)
66
+
67
+ @property
68
+ def x(self) -> float:
69
+ return self.data[0]
70
+
71
+ @x.setter
72
+ def x(self, value: float) -> None:
73
+ self.data[0] = value
74
+
75
+ @property
76
+ def y(self) -> float:
77
+ return self.data[1]
78
+
79
+ @y.setter
80
+ def y(self, value: float) -> None:
81
+ self.data[1] = value
82
+
83
+ @property
84
+ def length(self) -> np.floating:
85
+ return np.linalg.norm(self.data)
86
+
87
+ @property
88
+ def length_sqrd(self) -> float:
89
+ return np.dot(self.data, self.data)
90
+
91
+ def copy(self) -> "Vector2D":
92
+ return Vector2D(self.x, self.y)
93
+
94
+ def normalize(self) -> None:
95
+ """Normalize this vector in place"""
96
+ length = self.length
97
+ if length > 0:
98
+ self.data /= length
99
+
100
+ def __add__(self, other) -> "Vector2D":
101
+ if isinstance(other, Vector2D):
102
+ return Vector2D(self.x + other.x, self.y + other.y)
103
+ return Vector2D(self.x + other, self.y + other)
104
+
105
+ def __sub__(self, other) -> "Vector2D":
106
+ if isinstance(other, Vector2D):
107
+ return Vector2D(self.x - other.x, self.y - other.y)
108
+ return Vector2D(self.x - other, self.y - other)
109
+
110
+ def __mul__(self, other) -> "Vector2D":
111
+ if isinstance(other, Vector2D):
112
+ return Vector2D(self.x * other.x, self.y * other.y)
113
+ return Vector2D(self.x * other, self.y * other)
114
+
115
+ def __getitem__(self, idx) -> float:
116
+ """Support indexing like a tuple (v[0], v[1])"""
117
+ if idx == 0:
118
+ return self.x
119
+ elif idx == 1:
120
+ return self.y
121
+ raise IndexError("Vector2D index out of range")
122
+
123
+ def __setitem__(self, idx, value) -> None:
124
+ """Support setting by index (v[0] = x)"""
125
+ if idx == 0:
126
+ self.x = value
127
+ elif idx == 1:
128
+ self.y = value
129
+ else:
130
+ raise IndexError("Vector2D index out of range")
131
+
132
+ def __len__(self) -> Literal[2]:
133
+ """Support len(v) -> 2"""
134
+ return 2
135
+
136
+ def __repr__(self) -> str:
137
+ return f"Vector2D({self.x}, {self.y})"
52
138
 
53
- def copy(self):
54
- return Vector2D(self.x, self.y)
139
+ # Fallback batch operations
140
+ def batch_add_inplace(vectors: Sequence[Vector2D], displacement: Vector2D) -> None: # type: ignore[misc]
141
+ """Pure Python fallback for batch add"""
142
+ for vec in vectors:
143
+ vec.data += displacement.data # type: ignore[attr-defined]
55
144
 
56
- def __add__(self, other):
57
- if isinstance(other, Vector2D):
58
- return Vector2D(self.x + other.x, self.y + other.y)
59
- return Vector2D(self.x + other, self.y + other)
145
+ def batch_scale_inplace(vectors: Sequence[Vector2D], scalar: float) -> None: # type: ignore[misc]
146
+ """Pure Python fallback for batch scale"""
147
+ for vec in vectors:
148
+ vec.data *= scalar # type: ignore[attr-defined]
60
149
 
61
- def __sub__(self, other):
62
- if isinstance(other, Vector2D):
63
- return Vector2D(self.x - other.x, self.y - other.y)
64
- return Vector2D(self.x - other, self.y - other)
150
+ def batch_normalize_inplace(vectors: Sequence[Vector2D]) -> None: # type: ignore[misc]
151
+ """Pure Python fallback for batch normalize"""
152
+ for vec in vectors:
153
+ length = vec.length
154
+ if length > 0:
155
+ vec.data /= length # type: ignore[attr-defined]
65
156
 
66
- def __mul__(self, other):
67
- if isinstance(other, Vector2D):
68
- return Vector2D(self.x * other.x, self.y * other.y)
69
- return Vector2D(self.x * other, self.y * other)
157
+ def vectors_to_array(vectors: Sequence[Vector2D]) -> NDArray[np.float64]: # type: ignore[misc]
158
+ """Pure Python fallback for vectors to array"""
159
+ return np.array([v.data for v in vectors], dtype=np.float64) # type: ignore[attr-defined]
70
160
 
71
- def __repr__(self):
72
- return f"Vector2D({self.x}, {self.y})"
161
+ def array_to_vectors(arr: NDArray[np.float64]) -> list[Vector2D]: # type: ignore[misc]
162
+ """Pure Python fallback for array to vectors"""
163
+ return [Vector2D(row[0], row[1]) for row in arr]
73
164
 
74
165
 
75
166
  # Convenience aliases
@@ -129,6 +220,7 @@ __all__ = [
129
220
  'batch_normalize_inplace',
130
221
  'vectors_to_array',
131
222
  'array_to_vectors',
223
+ '_COMPILED',
132
224
  ]
133
225
 
134
226
 
e2D/vectors.pyi ADDED
@@ -0,0 +1,106 @@
1
+ """
2
+ Type stubs for vectors module
3
+ High-level Python wrapper for Vector2D with additional utilities
4
+ """
5
+
6
+ from typing import List, Tuple, Callable
7
+ from .cvectors import Vector2D, batch_add_inplace, batch_scale_inplace, batch_normalize_inplace, vectors_to_array, array_to_vectors
8
+
9
+ _COMPILED : bool
10
+ # Convenience aliases
11
+ V2 = Vector2D
12
+ Vec2 = Vector2D
13
+
14
+ # Pre-defined common vectors
15
+ class CommonVectors:
16
+ """Pre-allocated common vectors (do not modify these!)"""
17
+ ZERO: Vector2D
18
+ ONE: Vector2D
19
+ UP: Vector2D
20
+ DOWN: Vector2D
21
+ LEFT: Vector2D
22
+ RIGHT: Vector2D
23
+
24
+ @staticmethod
25
+ def zero() -> Vector2D:
26
+ """Create new zero vector"""
27
+ ...
28
+
29
+ @staticmethod
30
+ def one() -> Vector2D:
31
+ """Create new one vector"""
32
+ ...
33
+
34
+ @staticmethod
35
+ def up() -> Vector2D:
36
+ """Create new up vector"""
37
+ ...
38
+
39
+ @staticmethod
40
+ def down() -> Vector2D:
41
+ """Create new down vector"""
42
+ ...
43
+
44
+ @staticmethod
45
+ def left() -> Vector2D:
46
+ """Create new left vector"""
47
+ ...
48
+
49
+ @staticmethod
50
+ def right() -> Vector2D:
51
+ """Create new right vector"""
52
+ ...
53
+
54
+ # Additional utility functions
55
+ def lerp(start: float, end: float, t: float) -> float:
56
+ """Linear interpolation between two values"""
57
+ ...
58
+
59
+ def create_grid(width: int, height: int, spacing: float = 1.0) -> List[Vector2D]:
60
+ """
61
+ Create a grid of vectors
62
+
63
+ Args:
64
+ width: Number of columns
65
+ height: Number of rows
66
+ spacing: Distance between points
67
+
68
+ Returns:
69
+ List of Vector2D objects
70
+ """
71
+ ...
72
+
73
+ def create_circle(radius: float, num_points: int) -> List[Vector2D]:
74
+ """
75
+ Create vectors arranged in a circle
76
+
77
+ Args:
78
+ radius: Circle radius
79
+ num_points: Number of points
80
+
81
+ Returns:
82
+ List of Vector2D objects
83
+ """
84
+ ...
85
+
86
+ def benchmark(num_iterations: int = 100000) -> None:
87
+ """
88
+ Run a simple benchmark to test vector performance
89
+ """
90
+ ...
91
+
92
+ __all__ = [
93
+ 'Vector2D',
94
+ 'V2',
95
+ 'Vec2',
96
+ 'CommonVectors',
97
+ 'batch_add_inplace',
98
+ 'batch_scale_inplace',
99
+ 'batch_normalize_inplace',
100
+ 'vectors_to_array',
101
+ 'array_to_vectors',
102
+ 'lerp',
103
+ 'create_grid',
104
+ 'create_circle',
105
+ '_COMPILED',
106
+ ]
e2D/winrec.py ADDED
@@ -0,0 +1,275 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Optional
3
+ import numpy as np
4
+ import cv2
5
+ import threading
6
+ import queue
7
+ import time
8
+
9
+
10
+ class WinRec:
11
+ """
12
+ Asynchronous screen recorder for ModernGL-based e2D applications.
13
+
14
+ Usage:
15
+ rootEnv.init_rec(fps=30, draw_on_screen=True, path='output.mp4')
16
+ # Recording happens automatically in the render loop
17
+ # F9: Pause/Resume, F10: Restart, F12: Screenshot
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ rootEnv: Any,
23
+ fps: int = 30,
24
+ draw_on_screen: bool = True,
25
+ path: str = 'output.mp4'
26
+ ) -> None:
27
+ self.rootEnv = rootEnv
28
+ self.path = path
29
+ self.fps = fps
30
+ self.draw_on_screen = draw_on_screen
31
+ self.is_recording = True # Recording state (pause/resume)
32
+ self.screenshot_counter = 0 # Counter for screenshot filenames
33
+ self.recording_frames = 0 # Frames actually recorded (excludes paused frames)
34
+ self.pause_start_time: Optional[float] = None # Track when recording was paused
35
+ self.total_pause_duration = 0.0 # Cumulative pause time
36
+
37
+ # Get window size
38
+ width, height = self.rootEnv.window_size
39
+
40
+ # Setup video writer
41
+ self.video_writer = cv2.VideoWriter(
42
+ self.path,
43
+ cv2.VideoWriter_fourcc(*'mp4v'), # type: ignore
44
+ self.fps,
45
+ (width, height)
46
+ )
47
+
48
+ # Pre-allocate buffers for zero-copy operations
49
+ self.frame_buffer = np.empty(shape=(height, width, 3), dtype=np.uint8)
50
+
51
+ # Setup async video writing
52
+ self.frame_queue: queue.Queue = queue.Queue(maxsize=120) # Buffer up to 4 seconds at 30fps
53
+ self.running = True
54
+
55
+ # Statistics tracking
56
+ self.frames_written = 0
57
+ self.frames_dropped = 0
58
+ self.write_start_time = time.time()
59
+ self.last_stat_update = time.time()
60
+ self.current_write_fps = 0.0
61
+
62
+ self.write_thread = threading.Thread(target=self._write_worker, daemon=False)
63
+ self.write_thread.start()
64
+
65
+ def _write_worker(self) -> None:
66
+ """Background thread that writes frames to video file."""
67
+ while self.running or not self.frame_queue.empty():
68
+ try:
69
+ frame = self.frame_queue.get(timeout=0.1)
70
+ self.video_writer.write(frame)
71
+ self.frames_written += 1
72
+ self.frame_queue.task_done()
73
+
74
+ # Update write FPS every second
75
+ current_time = time.time()
76
+ if current_time - self.last_stat_update >= 1.0:
77
+ elapsed = current_time - self.write_start_time
78
+ self.current_write_fps = self.frames_written / elapsed if elapsed > 0 else 0
79
+ self.last_stat_update = current_time
80
+ except queue.Empty:
81
+ continue
82
+
83
+ def handle_input(self) -> None:
84
+ """Handle recording control keyboard inputs (F9-F12)."""
85
+ import glfw
86
+ from .devices import KeyState
87
+
88
+ # F9: Toggle pause/resume recording
89
+ if self.rootEnv.keyboard.get_key(glfw.KEY_F9, KeyState.JUST_PRESSED):
90
+ self.toggle_recording()
91
+ status = "REC" if self.is_recording else "PAUSED"
92
+ print(f"[Recording] {status}")
93
+
94
+ # F10: Restart recording (reset all and resume)
95
+ if self.rootEnv.keyboard.get_key(glfw.KEY_F10, KeyState.JUST_PRESSED):
96
+ self.restart()
97
+ print("[Recording] Restarted (buffer cleared, timers reset)")
98
+
99
+ # F12: Take screenshot
100
+ if self.rootEnv.keyboard.get_key(glfw.KEY_F12, KeyState.JUST_PRESSED):
101
+ screenshot_path = self.take_screenshot()
102
+ print(f"[Screenshot] Saved: {screenshot_path}")
103
+
104
+ def update(self) -> None:
105
+ """Capture frame from ModernGL framebuffer and queue for writing."""
106
+ # Handle keyboard input first
107
+ self.handle_input()
108
+
109
+ # Skip frame capture if recording is paused
110
+ if not self.is_recording:
111
+ return
112
+
113
+ # Increment recording frame counter
114
+ self.recording_frames += 1
115
+
116
+ # Read framebuffer from ModernGL context
117
+ # Note: OpenGL reads bottom-to-top, so we need to flip
118
+ width, height = self.rootEnv.window_size
119
+ pixels = self.rootEnv.ctx.screen.read(components=3)
120
+
121
+ # Convert bytes to numpy array and reshape
122
+ frame = np.frombuffer(pixels, dtype=np.uint8).reshape((height, width, 3))
123
+
124
+ # Flip vertically (OpenGL coordinates are bottom-up)
125
+ frame = np.flipud(frame)
126
+
127
+ # Convert RGB to BGR for OpenCV
128
+ cv2.cvtColor(frame, cv2.COLOR_RGB2BGR, dst=self.frame_buffer)
129
+
130
+ # Queue frame copy for async writing (non-blocking)
131
+ try:
132
+ self.frame_queue.put_nowait(self.frame_buffer.copy())
133
+ except queue.Full:
134
+ self.frames_dropped += 1 # Track dropped frames
135
+
136
+ def get_rec_seconds(self) -> float:
137
+ """Get recorded time in seconds (excludes paused time)."""
138
+ return self.recording_frames / self.fps if self.fps > 0 else 0.0
139
+
140
+ def draw(self) -> None:
141
+ """Draw recording statistics overlay on screen."""
142
+ if not self.draw_on_screen:
143
+ return
144
+
145
+ # Calculate statistics
146
+ buffer_size = self.frame_queue.qsize()
147
+ buffer_percent = (buffer_size / self.frame_queue.maxsize) * 100
148
+ buffer_seconds = buffer_size / self.fps if self.fps > 0 else 0
149
+
150
+ # Estimate optimal buffer size based on write performance
151
+ if self.current_write_fps > 0 and self.fps > 0:
152
+ write_lag = self.fps / self.current_write_fps
153
+ estimated_buffer = int(self.fps * 2 * write_lag) # 2 seconds of lag compensation
154
+ else:
155
+ estimated_buffer = self.frame_queue.maxsize
156
+
157
+ # Recording state indicator
158
+ rec_status = "REC" if self.is_recording else "PAUSED"
159
+
160
+ # Format with fixed width for stable display
161
+ from .text_renderer import TextStyle, Pivots
162
+ from .color_defs import WHITE, BLACK
163
+
164
+ row1 = (f"[{rec_status}] RecFrames:{self.recording_frames:>6} | "
165
+ f"RecTime:{self.get_rec_seconds():>6.2f}s | "
166
+ f"AppTime:{self.rootEnv.runtime:>6.2f}s")
167
+ row2 = (f"Buffer:{buffer_size:>3}/{self.frame_queue.maxsize:<3} "
168
+ f"({buffer_percent:>5.1f}%, {buffer_seconds:>4.1f}s) | "
169
+ f"WriteFPS:{self.current_write_fps:>5.1f}")
170
+ row3 = (f"Written:{self.frames_written:>6} | "
171
+ f"Dropped:{self.frames_dropped:>4} | "
172
+ f"OptBuf:{estimated_buffer:>3}")
173
+ row4 = "[F9]Pause/Resume [F10]Restart [F12]Screenshot"
174
+
175
+ # Position in bottom-right corner with spacing
176
+ win_width, win_height = self.rootEnv.window_size_f
177
+ x_pos = win_width - 16
178
+ y_base = win_height - 16
179
+ line_height = 30 # Approximate line height
180
+
181
+ style = TextStyle(color=WHITE, bg_color=(0.0, 0.0, 0.0, 0.9))
182
+
183
+ # Draw from bottom to top
184
+ self.rootEnv.print(row4, (x_pos, y_base), style=style, pivot=Pivots.BOTTOM_RIGHT)
185
+ self.rootEnv.print(row3, (x_pos, y_base - line_height), style=style, pivot=Pivots.BOTTOM_RIGHT)
186
+ self.rootEnv.print(row2, (x_pos, y_base - line_height * 2), style=style, pivot=Pivots.BOTTOM_RIGHT)
187
+ self.rootEnv.print(row1, (x_pos, y_base - line_height * 3), style=style, pivot=Pivots.BOTTOM_RIGHT)
188
+
189
+ def pause(self) -> None:
190
+ """Pause recording (stop capturing frames)."""
191
+ if self.is_recording:
192
+ self.is_recording = False
193
+ self.pause_start_time = time.time()
194
+
195
+ def resume(self) -> None:
196
+ """Resume recording (continue capturing frames)."""
197
+ if not self.is_recording and self.pause_start_time is not None:
198
+ self.total_pause_duration += time.time() - self.pause_start_time
199
+ self.pause_start_time = None
200
+ self.is_recording = True
201
+
202
+ def toggle_recording(self) -> None:
203
+ """Toggle between pause and resume."""
204
+ if self.is_recording:
205
+ self.pause()
206
+ else:
207
+ self.resume()
208
+
209
+ def restart(self) -> None:
210
+ """Restart recording: clear buffer, reset all counters and timers, resume recording."""
211
+ self.clear_buffer()
212
+ self.recording_frames = 0
213
+ self.total_pause_duration = 0.0
214
+ self.pause_start_time = None
215
+ self.is_recording = True
216
+
217
+ def clear_buffer(self) -> None:
218
+ """Clear the frame queue and reset write statistics."""
219
+ # Clear the queue
220
+ while not self.frame_queue.empty():
221
+ try:
222
+ self.frame_queue.get_nowait()
223
+ self.frame_queue.task_done()
224
+ except queue.Empty:
225
+ break
226
+
227
+ # Reset write counters (but keep recording frames)
228
+ self.frames_written = 0
229
+ self.frames_dropped = 0
230
+ self.write_start_time = time.time()
231
+ self.last_stat_update = time.time()
232
+ self.current_write_fps = 0.0
233
+
234
+ def take_screenshot(self, filename: Optional[str] = None) -> str:
235
+ """
236
+ Save current screen as PNG screenshot.
237
+
238
+ Args:
239
+ filename: Optional custom filename. If None, auto-generates with counter.
240
+
241
+ Returns:
242
+ str: Path to saved screenshot
243
+ """
244
+ if filename is None:
245
+ # Auto-generate filename with counter
246
+ base_path = self.path.rsplit('.', 1)[0] # Remove .mp4 extension
247
+ filename = f"{base_path}_screenshot_{self.screenshot_counter:04d}.png"
248
+ self.screenshot_counter += 1
249
+
250
+ # Capture current screen from ModernGL
251
+ width, height = self.rootEnv.window_size
252
+ pixels = self.rootEnv.ctx.screen.read(components=3)
253
+
254
+ # Convert bytes to numpy array and reshape
255
+ frame = np.frombuffer(pixels, dtype=np.uint8).reshape((height, width, 3))
256
+
257
+ # Flip vertically (OpenGL coordinates are bottom-up)
258
+ frame = np.flipud(frame)
259
+
260
+ # Convert RGB to BGR for OpenCV
261
+ screenshot_buffer = np.empty(shape=(height, width, 3), dtype=np.uint8)
262
+ cv2.cvtColor(frame, cv2.COLOR_RGB2BGR, dst=screenshot_buffer)
263
+
264
+ # Save as PNG
265
+ cv2.imwrite(filename, screenshot_buffer)
266
+ return filename
267
+
268
+ def quit(self) -> None:
269
+ """Stop recording and release resources."""
270
+ # Stop accepting new frames and wait for queue to flush
271
+ self.running = False
272
+ self.frame_queue.join() # Wait for all queued frames to be written
273
+ self.write_thread.join(timeout=5.0) # Wait for thread to finish
274
+ self.video_writer.release()
275
+ print(f"[Recording] Saved {self.frames_written} frames to {self.path}")