e2D 1.4.20__py3-none-any.whl → 1.4.23__py3-none-any.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/winrec.py CHANGED
@@ -1,27 +1,212 @@
1
- from e2D.envs import *
1
+ from typing import Optional, TYPE_CHECKING
2
+ from e2D import V2
3
+ if TYPE_CHECKING:
4
+ from e2D.envs import RootEnv, FONT_MONOSPACE_16
5
+ from e2D.colors import BLACK_COLOR_PYG
2
6
  import pygame as pg
3
7
  import numpy as np
4
8
  import cv2
9
+ import threading
10
+ import queue
11
+ import time
5
12
 
6
13
  class WinRec:
7
- def __init__(self, rootEnv:RootEnv, fps:int=30, path:str='output.mp4') -> None:
14
+ def __init__(self, rootEnv:RootEnv, fps:int=30, draw_on_screen:bool=True, path:str='output.mp4', font:pg.font.Font=FONT_MONOSPACE_16) -> None:
8
15
  self.rootEnv = rootEnv
9
16
  self.path = path
10
17
  self.fps = fps
11
- self.video_writer = cv2.VideoWriter(self.path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, self.rootEnv.screen_size()) #type: ignore
18
+ self.draw_on_screen = draw_on_screen
19
+ self.font = font
20
+ self.is_recording = True # Recording state (pause/resume)
21
+ self.screenshot_counter = 0 # Counter for screenshot filenames
22
+ self.recording_frames = 0 # Frames actually recorded (excludes paused frames)
23
+ self.pause_start_time = None # Track when recording was paused
24
+ self.total_pause_duration = 0.0 # Cumulative pause time
25
+ size = self.rootEnv.screen_size
26
+ self.video_writer = cv2.VideoWriter(self.path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, size()) #type: ignore
27
+
28
+ # Pre-allocate buffers for zero-copy operations
29
+ self.frame_buffer = np.empty(shape=(int(size.y), int(size.x), 3), dtype=np.uint8)
30
+
31
+ # Setup async video writing
32
+ self.frame_queue = queue.Queue(maxsize=120) # Buffer up to 4 seconds at 30fps
33
+ self.running = True
34
+
35
+ # Statistics tracking
36
+ self.frames_written = 0
37
+ self.frames_dropped = 0
38
+ self.write_start_time = time.time()
39
+ self.last_stat_update = time.time()
40
+ self.current_write_fps = 0.0
41
+
42
+ self.write_thread = threading.Thread(target=self._write_worker, daemon=False)
43
+ self.write_thread.start()
44
+
45
+ def _write_worker(self) -> None:
46
+ """Background thread that writes frames to video file."""
47
+ while self.running or not self.frame_queue.empty():
48
+ try:
49
+ frame = self.frame_queue.get(timeout=0.1)
50
+ self.video_writer.write(frame)
51
+ self.frames_written += 1
52
+ self.frame_queue.task_done()
53
+
54
+ # Update write FPS every second
55
+ current_time = time.time()
56
+ if current_time - self.last_stat_update >= 1.0:
57
+ elapsed = current_time - self.write_start_time
58
+ self.current_write_fps = self.frames_written / elapsed if elapsed > 0 else 0
59
+ self.last_stat_update = current_time
60
+ except queue.Empty:
61
+ continue
62
+
63
+ def handle_input(self) -> None:
64
+ """Handle recording control keyboard inputs (F9-F12)."""
65
+ # F9: Toggle pause/resume recording
66
+ if self.rootEnv.keyboard.get_key(pg.K_F9, "just_pressed"):
67
+ self.toggle_recording()
68
+ status = "REC" if self.is_recording else "PAUSED"
69
+ print(f"[Recording] {status}")
70
+
71
+ # F10: Restart recording (reset all and resume)
72
+ if self.rootEnv.keyboard.get_key(pg.K_F10, "just_pressed"):
73
+ self.restart()
74
+ print("[Recording] Restarted (buffer cleared, timers reset)")
75
+
76
+ # F12: Take screenshot
77
+ if self.rootEnv.keyboard.get_key(pg.K_F12, "just_pressed"):
78
+ screenshot_path = self.take_screenshot()
79
+ print(f"[Screenshot] Saved: {screenshot_path}")
12
80
 
13
81
  def update(self) -> None:
14
- frame = cv2.cvtColor(np.swapaxes(pg.surfarray.array3d(self.rootEnv.screen), 0, 1), cv2.COLOR_RGB2BGR)
15
- self.video_writer.write(frame)
82
+ # Handle keyboard input first
83
+ self.handle_input()
84
+
85
+ # Skip frame capture if recording is paused
86
+ if not self.is_recording:
87
+ return
88
+
89
+ # Increment recording frame counter
90
+ self.recording_frames += 1
91
+
92
+ # Use pixels3d for zero-copy view, then transpose (creates view, not copy)
93
+ pixels = pg.surfarray.pixels3d(self.rootEnv.screen)
94
+ transposed = np.transpose(pixels, (1, 0, 2))
95
+
96
+ # Convert color in-place to pre-allocated buffer
97
+ cv2.cvtColor(transposed, cv2.COLOR_RGB2BGR, dst=self.frame_buffer)
98
+
99
+ # Queue frame copy for async writing (non-blocking)
100
+ try:
101
+ self.frame_queue.put_nowait(self.frame_buffer.copy())
102
+ except queue.Full:
103
+ self.frames_dropped += 1 # Track dropped frames
16
104
 
17
105
  def get_rec_seconds(self) -> float:
18
- return self.rootEnv.current_frame/self.fps
106
+ """Get recorded time in seconds (excludes paused time)."""
107
+ return self.recording_frames / self.fps
108
+
109
+ def draw(self) -> None:
110
+ # Calculate statistics
111
+ buffer_size = self.frame_queue.qsize()
112
+ buffer_percent = (buffer_size / self.frame_queue.maxsize) * 100
113
+ buffer_seconds = buffer_size / self.fps if self.fps > 0 else 0
114
+
115
+ # Estimate optimal buffer size based on write performance
116
+ if self.current_write_fps > 0 and self.fps > 0:
117
+ write_lag = self.fps / self.current_write_fps
118
+ estimated_buffer = int(self.fps * 2 * write_lag) # 2 seconds of lag compensation
119
+ else:
120
+ estimated_buffer = self.frame_queue.maxsize
121
+
122
+ # Recording state indicator
123
+ rec_status = "REC" if self.is_recording else "PAUSED"
124
+
125
+ # Format with fixed width for stable display (monospace-friendly)
126
+ row1 = (f"[{rec_status}] RecFrames:{self.recording_frames:>6} | "
127
+ f"RecTime:{self.get_rec_seconds():>6.2f}s | "
128
+ f"AppTime:{self.rootEnv.runtime_seconds:>6.2f}s")
129
+ row2 = (f"Buffer:{buffer_size:>3}/{self.frame_queue.maxsize:<3} ({buffer_percent:>5.1f}%, {buffer_seconds:>4.1f}s) | "
130
+ f"WriteFPS:{self.current_write_fps:>5.1f}")
131
+ row3 = (f"Written:{self.frames_written:>6} | Dropped:{self.frames_dropped:>4} | OptBuf:{estimated_buffer:>3}")
132
+ row4 = "[F9]Pause/Resume [F10]Restart [F12]Screenshot"
19
133
 
20
- def draw(self, draw_on_screen=False) -> None:
21
- text = f"[cfps:{self.rootEnv.current_frame} || realtime:{round(self.get_rec_seconds(),2)} || apptime:{round(self.rootEnv.runtime_seconds,2)}]"
22
- pg.display.set_caption(text)
23
- if draw_on_screen: self.rootEnv.print(text, self.rootEnv.screen_size, pivot_position='bottom_right')
134
+ if self.draw_on_screen:
135
+ size = self.rootEnv.print(row4, self.rootEnv.screen_size - V2(16, 16), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
136
+ self.rootEnv.print(row3, self.rootEnv.screen_size - V2(16, 16 + size.y), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
137
+ self.rootEnv.print(row2, self.rootEnv.screen_size - V2(16, 16 + size.y * 2), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
138
+ self.rootEnv.print(row1, self.rootEnv.screen_size - V2(16, 16 + size.y * 3), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
139
+
140
+ def pause(self) -> None:
141
+ """Pause recording (stop capturing frames)."""
142
+ if self.is_recording:
143
+ self.is_recording = False
144
+ self.pause_start_time = time.time()
145
+
146
+ def resume(self) -> None:
147
+ """Resume recording (continue capturing frames)."""
148
+ if not self.is_recording and self.pause_start_time is not None:
149
+ self.total_pause_duration += time.time() - self.pause_start_time
150
+ self.pause_start_time = None
151
+ self.is_recording = True
152
+
153
+ def toggle_recording(self) -> None:
154
+ """Toggle between pause and resume."""
155
+ self.is_recording = not self.is_recording
156
+
157
+ def restart(self) -> None:
158
+ """Restart recording: clear buffer, reset all counters and timers, resume recording."""
159
+ self.clear_buffer()
160
+ self.recording_frames = 0
161
+ self.total_pause_duration = 0.0
162
+ self.pause_start_time = None
163
+ self.is_recording = True
164
+
165
+ def clear_buffer(self) -> None:
166
+ """Clear the frame queue and reset write statistics."""
167
+ # Clear the queue
168
+ while not self.frame_queue.empty():
169
+ try:
170
+ self.frame_queue.get_nowait()
171
+ self.frame_queue.task_done()
172
+ except queue.Empty:
173
+ break
174
+
175
+ # Reset write counters (but keep recording frames)
176
+ self.frames_written = 0
177
+ self.frames_dropped = 0
178
+ self.write_start_time = time.time()
179
+ self.last_stat_update = time.time()
180
+ self.current_write_fps = 0.0
181
+
182
+ def take_screenshot(self, filename:Optional[str]=None) -> str:
183
+ """Save current screen as PNG screenshot.
184
+
185
+ Args:
186
+ filename: Optional custom filename. If None, auto-generates with counter.
187
+
188
+ Returns:
189
+ str: Path to saved screenshot
190
+ """
191
+ if filename is None:
192
+ # Auto-generate filename with counter
193
+ base_path = self.path.rsplit('.', 1)[0] # Remove .mp4 extension
194
+ filename = f"{base_path}_screenshot_{self.screenshot_counter:04d}.png"
195
+ self.screenshot_counter += 1
196
+
197
+ # Capture current screen
198
+ pixels = pg.surfarray.pixels3d(self.rootEnv.screen)
199
+ transposed = np.transpose(pixels, (1, 0, 2))
200
+ screenshot_buffer = np.empty(shape=(int(self.rootEnv.screen_size.y), int(self.rootEnv.screen_size.x), 3), dtype=np.uint8)
201
+ cv2.cvtColor(transposed, cv2.COLOR_RGB2BGR, dst=screenshot_buffer)
202
+
203
+ # Save as PNG
204
+ cv2.imwrite(filename, screenshot_buffer)
205
+ return filename
24
206
 
25
207
  def quit(self) -> None:
26
- self.video_writer.release()
27
-
208
+ # Stop accepting new frames and wait for queue to flush
209
+ self.running = False
210
+ self.frame_queue.join() # Wait for all queued frames to be written
211
+ self.write_thread.join(timeout=5.0) # Wait for thread to finish
212
+ self.video_writer.release()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e2D
3
- Version: 1.4.20
3
+ Version: 1.4.23
4
4
  Summary: Python library for 2D games. Streamlines dev with keyboard/mouse input, vector calculations, color manipulation, and collision detection. Simplify game creation and unleash creativity!
5
5
  Home-page: https://github.com/marick-py/e2D
6
6
  Author: Riccardo Mariani
@@ -0,0 +1,13 @@
1
+ e2D/__init__.py,sha256=ZTja5nMPyg3_fulzuSWhjA-JexSG9QHHr3sc0w7Qcpk,23857
2
+ e2D/__init__.pyi,sha256=1WNOYOyNyroL2LLrzE7CIJ7uubh_zhVbMvJINpy5PIY,53979
3
+ e2D/colors.py,sha256=4ZFcp6ZfdQ2eG3INw_Hx-KI2PkVustZ5TurnooHGSvw,19063
4
+ e2D/def_colors.py,sha256=3sJq2L6qFZ3svn2qEWIx0SinNXjb9huNaFigDeJipm8,43805
5
+ e2D/envs.py,sha256=5ah8SW9HNC3NqqZZfCBZs1BWtWHLA43dWwQruawzm9U,9240
6
+ e2D/plots.py,sha256=_d72ZJo-GIhcJ44XCphFH288cf_ZSwWcbLh_olgGjBc,35880
7
+ e2D/utils.py,sha256=Yamt6_JS6MVKrNtfzjP7BRWjCeXHuzFcFlL7H2Au7_M,29687
8
+ e2D/winrec.py,sha256=JzHW-fbnuyUoPG5_hoGe7RNBG4WnxjvjHNT2vJxiAGk,9767
9
+ e2d-1.4.23.dist-info/licenses/LICENSE,sha256=hbjljn38VVW9en51B0qzRK-v2FBDijqRWbZIVTk7ipU,1094
10
+ e2d-1.4.23.dist-info/METADATA,sha256=_nwwI7muu3u9xkJvrpHlRUOA0Ofskf5IfXohLZY5KBA,9634
11
+ e2d-1.4.23.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
12
+ e2d-1.4.23.dist-info/top_level.txt,sha256=3vKZ-CGzNlTCpzVMmM0Ht76krCofKw7hZ0wBf-dnKdM,4
13
+ e2d-1.4.23.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,13 +0,0 @@
1
- e2D/__init__.py,sha256=7UmgGI4WotqPLGXyGlNzddUdd5eK1VWqsb2BMAOA78g,22522
2
- e2D/__init__.pyi,sha256=ckPL2_iz5439-lD-wRLF7t54b_ndF4LT_A24_oyCCTU,45403
3
- e2D/colors.py,sha256=SwO52zowBs9l_VK1TjyT2GK1Npn5UAsHdCxz3lvaSKQ,18011
4
- e2D/def_colors.py,sha256=3sJq2L6qFZ3svn2qEWIx0SinNXjb9huNaFigDeJipm8,43805
5
- e2D/envs.py,sha256=1QnOEI2lCO3B6_EkIBBqgaXfPYLFE_u0_H6AwmZ1R9U,7171
6
- e2D/plots.py,sha256=_d72ZJo-GIhcJ44XCphFH288cf_ZSwWcbLh_olgGjBc,35880
7
- e2D/utils.py,sha256=42anOSUxvQ9SOof26VkJPsm6vN9AvckeW0U0RILzi8w,27843
8
- e2D/winrec.py,sha256=EFFfWYbk27NhS-rWD-BLChXvLjFW1uYZ5LkRGMj_Xo0,1116
9
- e2d-1.4.20.dist-info/licenses/LICENSE,sha256=hbjljn38VVW9en51B0qzRK-v2FBDijqRWbZIVTk7ipU,1094
10
- e2d-1.4.20.dist-info/METADATA,sha256=YOwcgB_fj4q1bHxAbh1uK-qcFEWAK8qoA4lZ4-dOFZI,9634
11
- e2d-1.4.20.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
12
- e2d-1.4.20.dist-info/top_level.txt,sha256=3vKZ-CGzNlTCpzVMmM0Ht76krCofKw7hZ0wBf-dnKdM,4
13
- e2d-1.4.20.dist-info/RECORD,,