e2D 2.0.0__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/__init__.py ADDED
@@ -0,0 +1,461 @@
1
+ """
2
+ e2D - High-Performance 2D Graphics and Math Library
3
+ Combines ultra-optimized vector operations with moderngl rendering
4
+
5
+ Copyright (c) 2025 Riccardo Mariani
6
+ MIT License
7
+ """
8
+
9
+ __version__ = "2.0.0"
10
+ __author__ = "Riccardo Mariani"
11
+ __email__ = "ricomari2006@gmail.com"
12
+
13
+ import numpy as np
14
+ import moderngl
15
+ import glfw
16
+ import time
17
+ import os
18
+
19
+ # Import original e2D modules
20
+ from .text_renderer import DEFAULT_TEXT_STYLE, Pivots, TextRenderer, TextLabel, TextStyle
21
+ from .shapes import ShapeRenderer, ShapeLabel, InstancedShapeBatch, FillMode
22
+ from .devices import Keyboard, Mouse, KeyState
23
+ from .commons import get_pattr, get_pattr_value, set_pattr_value, get_uniform
24
+
25
+ from typing import Optional
26
+
27
+ # Import optimized Vector2D
28
+ try:
29
+ from .cvectors import (
30
+ Vector2D,
31
+ batch_add_inplace,
32
+ batch_scale_inplace,
33
+ batch_normalize_inplace,
34
+ vectors_to_array,
35
+ array_to_vectors,
36
+ )
37
+ _VECTOR_COMPILED = True
38
+ except ImportError:
39
+ # Fallback to pure Python implementation
40
+ import warnings
41
+ warnings.warn(
42
+ "Vector2D compiled extension not available. "
43
+ "Install Cython and rebuild for optimal performance.",
44
+ RuntimeWarning
45
+ )
46
+ from .vectors import (
47
+ Vector2D,
48
+ batch_add_inplace,
49
+ batch_scale_inplace,
50
+ batch_normalize_inplace,
51
+ vectors_to_array,
52
+ array_to_vectors,
53
+ )
54
+ _VECTOR_COMPILED = False
55
+
56
+ # Import vector utilities
57
+ from .vectors import (
58
+ V2,
59
+ Vec2,
60
+ CommonVectors,
61
+ lerp,
62
+ create_grid,
63
+ create_circle,
64
+ )
65
+
66
+
67
+ class DefEnv:
68
+ ctx: moderngl.Context
69
+ def __init__(self) -> None: ...
70
+
71
+ def draw(self) -> None: ...
72
+
73
+ def update(self) -> None: ...
74
+
75
+ def on_resize(self, width: int, height: int) -> None: ...
76
+
77
+ class RootEnv:
78
+ def __init__(
79
+ self,
80
+ window_size:tuple[int, int]=(1920, 1080),
81
+ target_fps:int=60,
82
+ vsync:bool=True,
83
+ version:tuple[int, int]=(4,3),
84
+ monitor:Optional[int]=None
85
+ ) -> None:
86
+
87
+ if not glfw.init():
88
+ raise RuntimeError("GLFW initialization failed")
89
+
90
+ glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, version[0])
91
+ glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, version[1])
92
+ glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
93
+ glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
94
+ glfw.window_hint(glfw.RESIZABLE, True)
95
+
96
+ self.window_size = window_size
97
+ self.target_fps = target_fps
98
+
99
+ self.window : glfw._GLFWwindow = glfw.create_window(window_size[0], window_size[1], "e2D", monitor, None)
100
+ if not self.window:
101
+ glfw.terminate()
102
+ raise RuntimeError("Failed to create GLFW window")
103
+
104
+ glfw.make_context_current(self.window)
105
+
106
+ try:
107
+ self.ctx = moderngl.create_context()
108
+ except Exception as e:
109
+ print(f"Error creating ModernGL context: {e}")
110
+ glfw.terminate()
111
+ raise
112
+
113
+ # VSync control - must be set AFTER context creation
114
+ if vsync:
115
+ glfw.swap_interval(1)
116
+ else:
117
+ glfw.swap_interval(0)
118
+
119
+ print(f"OpenGL Context: {self.ctx.version_code} / {self.ctx.info['GL_RENDERER']}")
120
+
121
+ self.ctx.enable(moderngl.BLEND)
122
+ self.ctx.blend_func = moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA
123
+
124
+ # Set initial viewport
125
+ fb_size = glfw.get_framebuffer_size(self.window)
126
+ self.ctx.viewport = (0, 0, fb_size[0], fb_size[1])
127
+
128
+ glfw.set_window_size_callback(self.window, self._on_resize)
129
+
130
+ self.programs :dict[str, moderngl.Program]= {}
131
+ self.compute_shaders :dict[str, moderngl.ComputeShader]= {}
132
+ self.buffers :dict[str, moderngl.Buffer]= {}
133
+
134
+ self.keyboard = Keyboard()
135
+ self.mouse = Mouse()
136
+ self.text_renderer = TextRenderer(self.ctx)
137
+ self.shape_renderer = ShapeRenderer(self.ctx)
138
+
139
+ # Delta time tracking
140
+ self.delta = 0.0
141
+ self.last_frame_time = time.time()
142
+
143
+ @property
144
+ def window_size_f(self) -> tuple[float, float]:
145
+ """Get window size as floats for shader uniforms."""
146
+ return (float(self.window_size[0]), float(self.window_size[1]))
147
+
148
+ def _on_resize(self, window, width, height) -> None:
149
+ self.window_size = (width, height)
150
+ fb_size = glfw.get_framebuffer_size(window)
151
+ self.ctx.viewport = (0, 0, fb_size[0], fb_size[1])
152
+ if hasattr(self, 'env') and hasattr(self.env, 'on_resize'):
153
+ self.env.on_resize(fb_size[0], fb_size[1])
154
+
155
+ def init(self, env:DefEnv) -> "RootEnv":
156
+ self.env = env
157
+ return self
158
+
159
+ def load_shader_file(self, path:str) -> str:
160
+ """Load shader source code from a file.
161
+
162
+ Args:
163
+ path: Path to shader file (relative to working directory or absolute)
164
+
165
+ Returns:
166
+ Shader source code as string
167
+ """
168
+ if not os.path.exists(path):
169
+ raise FileNotFoundError(f"Shader file not found: {path}")
170
+
171
+ with open(path, 'r', encoding='utf-8') as f:
172
+ return f.read()
173
+
174
+ def create_program(self, vertex_shader:str, fragment_shader:str, id:str) -> moderngl.Program:
175
+ new_program = self.ctx.program(
176
+ vertex_shader=vertex_shader,
177
+ fragment_shader=fragment_shader
178
+ )
179
+ if id in self.programs:
180
+ print(f"Warning: Program with id '{id}' already exists. Overwriting.")
181
+ self.programs[id] = new_program
182
+ return new_program
183
+
184
+ def create_program_from_files(self, vertex_path:str, fragment_path:str, id:str) -> moderngl.Program:
185
+ """Create a shader program from shader files.
186
+
187
+ Args:
188
+ vertex_path: Path to vertex shader file
189
+ fragment_path: Path to fragment shader file
190
+ id: Identifier for the program
191
+
192
+ Returns:
193
+ The created program
194
+ """
195
+ vertex_shader = self.load_shader_file(vertex_path)
196
+ fragment_shader = self.load_shader_file(fragment_path)
197
+ return self.create_program(vertex_shader, fragment_shader, id)
198
+
199
+ def get_program(self, id:str) -> moderngl.Program | None:
200
+ return self.programs.get(id, None)
201
+
202
+ def __draw__(self) -> None:
203
+ self.ctx.clear(0.0, 0.0, 0.0, 1.0)
204
+ self.env.draw()
205
+
206
+ def create_compute_shader(self, compute_shader:str, id:str) -> moderngl.ComputeShader:
207
+ """Create and store a compute shader program."""
208
+ new_compute = self.ctx.compute_shader(compute_shader)
209
+ if id in self.compute_shaders:
210
+ print(f"Warning: Compute shader with id '{id}' already exists. Overwriting.")
211
+ self.compute_shaders[id] = new_compute
212
+ return new_compute
213
+
214
+ def create_compute_shader_from_file(self, compute_path:str, id:str) -> moderngl.ComputeShader:
215
+ """Create a compute shader from a file.
216
+
217
+ Args:
218
+ compute_path: Path to compute shader file
219
+ id: Identifier for the compute shader
220
+
221
+ Returns:
222
+ The created compute shader
223
+ """
224
+ compute_shader = self.load_shader_file(compute_path)
225
+ return self.create_compute_shader(compute_shader, id)
226
+
227
+ def get_compute_shader(self, id:str) -> moderngl.ComputeShader | None:
228
+ """Retrieve a compute shader by id."""
229
+ return self.compute_shaders.get(id, None)
230
+
231
+ def create_buffer(self, data=None, reserve:int=0, id:Optional[str]=None, dynamic:bool=True) -> moderngl.Buffer:
232
+ """Create and optionally store a buffer.
233
+
234
+ Args:
235
+ data: Initial data (numpy array, bytes, or None)
236
+ reserve: Reserve size in bytes if data is None
237
+ id: Optional identifier to store the buffer
238
+ dynamic: If True, buffer is marked for frequent updates
239
+ """
240
+ if data is not None:
241
+ if isinstance(data, np.ndarray):
242
+ buffer = self.ctx.buffer(data.tobytes(), dynamic=dynamic)
243
+ else:
244
+ buffer = self.ctx.buffer(data, dynamic=dynamic)
245
+ else:
246
+ buffer = self.ctx.buffer(reserve=reserve, dynamic=dynamic)
247
+
248
+ if id is not None:
249
+ if id in self.buffers:
250
+ print(f"Warning: Buffer with id '{id}' already exists. Overwriting.")
251
+ self.buffers[id] = buffer
252
+
253
+ return buffer
254
+
255
+ def get_buffer(self, id:str) -> moderngl.Buffer | None:
256
+ """Retrieve a buffer by id."""
257
+ return self.buffers.get(id, None)
258
+
259
+ def bind_buffer_to_storage(self, buffer:moderngl.Buffer|str, binding:int) -> None:
260
+ """Bind a buffer to a shader storage binding point.
261
+
262
+ Args:
263
+ buffer: Buffer object or buffer id
264
+ binding: SSBO binding point (0, 1, 2, ...)
265
+ """
266
+ if isinstance(buffer, str):
267
+ loc_buffer = self.get_buffer(buffer)
268
+ if loc_buffer is None:
269
+ raise ValueError(f"Buffer with id '{buffer}' does not exist.")
270
+ buffer = loc_buffer
271
+
272
+ buffer.bind_to_storage_buffer(binding)
273
+
274
+ def dispatch_compute(self,
275
+ compute_id:str|moderngl.ComputeShader,
276
+ groups_x:int=1, groups_y:int=1, groups_z:int=1,
277
+ buffers:Optional[dict[int, moderngl.Buffer|str]]=None,
278
+ wait:bool=True
279
+ ) -> None:
280
+ """Dispatch a compute shader with automatic buffer binding.
281
+
282
+ Args:
283
+ compute_id: Compute shader object or id
284
+ groups_x, groups_y, groups_z: Number of work groups
285
+ buffers: Optional dict mapping binding points to buffers {0: buffer, 1: 'buffer_id', ...}
286
+ wait: If True, wait for compute to complete before returning
287
+ """
288
+ if isinstance(compute_id, str):
289
+ compute = self.get_compute_shader(compute_id)
290
+ if compute is None:
291
+ raise ValueError(f"Compute shader with id '{compute_id}' does not exist.")
292
+ else:
293
+ compute = compute_id
294
+
295
+ # Bind buffers if provided
296
+ if buffers:
297
+ for binding, buffer in buffers.items():
298
+ self.bind_buffer_to_storage(buffer, binding)
299
+
300
+ # Run compute shader
301
+ compute.run(groups_x, groups_y, groups_z)
302
+
303
+ # Memory barrier to ensure compute writes are visible
304
+ if wait:
305
+ self.ctx.memory_barrier()
306
+
307
+ def read_buffer(self, buffer:moderngl.Buffer|str, dtype='f4') -> np.ndarray:
308
+ """Read data from a buffer into a numpy array.
309
+
310
+ Args:
311
+ buffer: Buffer object or buffer id
312
+ dtype: numpy dtype for the output array
313
+ """
314
+ if isinstance(buffer, str):
315
+ loc_buffer = self.get_buffer(buffer)
316
+ if loc_buffer is None:
317
+ raise ValueError(f"Buffer with id '{buffer}' does not exist.")
318
+ buffer = loc_buffer
319
+
320
+ data = np.frombuffer(buffer.read(), dtype=dtype)
321
+ return data
322
+
323
+ def write_buffer(self, buffer:moderngl.Buffer|str, data, offset:int=0) -> None:
324
+ """Write data to a buffer.
325
+
326
+ Args:
327
+ buffer: Buffer object or buffer id
328
+ data: Data to write (numpy array or bytes)
329
+ offset: Byte offset in the buffer
330
+ """
331
+ if isinstance(buffer, str):
332
+ loc_buffer = self.get_buffer(buffer)
333
+ if loc_buffer is None:
334
+ raise ValueError(f"Buffer with id '{buffer}' does not exist.")
335
+ buffer = loc_buffer
336
+
337
+ if isinstance(data, np.ndarray):
338
+ data = data.tobytes()
339
+
340
+ buffer.write(data, offset=offset)
341
+
342
+ def __update__(self) -> None:
343
+ self.env.update()
344
+
345
+ def get_pattr(self, prog_id:str|moderngl.Program, name:str) -> moderngl.Uniform | moderngl.UniformBlock | moderngl.Attribute | moderngl.Varying:
346
+ return get_pattr(prog_id, name, programs=self.programs)
347
+
348
+ def get_uniform(self, prog_id:str|moderngl.Program|moderngl.ComputeShader, name:str) -> moderngl.Uniform:
349
+ return get_uniform(prog_id, name, compute_shaders=self.compute_shaders, programs=self.programs)
350
+
351
+ def get_pattr_value(self, prog_id:str|moderngl.Program, name:str) -> int|float|tuple|list:
352
+ return get_pattr_value(prog_id, name, programs=self.programs)
353
+
354
+ def set_pattr_value(self, prog_id:str|moderngl.Program, name: str, value, *, force_write: bool= False) -> None:
355
+ return set_pattr_value(prog_id, name, value, force_write=force_write, programs=self.programs)
356
+
357
+ def loop(self) -> None:
358
+ # Register callbacks
359
+ glfw.set_scroll_callback(self.window, self.mouse._on_scroll)
360
+ glfw.set_cursor_pos_callback(self.window, self.mouse._on_cursor_pos)
361
+ glfw.set_mouse_button_callback(self.window, self.mouse._on_mouse_button)
362
+ glfw.set_key_callback(self.window, self.keyboard._on_key)
363
+
364
+ target_frame_time = 1.0 / self.target_fps if (self.target_fps and self.target_fps > 0) else 0.0
365
+
366
+ while not glfw.window_should_close(self.window):
367
+ start_time = time.time()
368
+
369
+ # Calculate delta time
370
+ self.delta = start_time - self.last_frame_time
371
+ self.last_frame_time = start_time
372
+
373
+ self.keyboard.update()
374
+ self.mouse.update()
375
+ glfw.poll_events()
376
+
377
+ if self.keyboard.get_key(glfw.KEY_X, KeyState.JUST_PRESSED):
378
+ glfw.set_window_should_close(self.window, True)
379
+
380
+ try:
381
+ self.__update__()
382
+ self.__draw__()
383
+ glfw.swap_buffers(self.window)
384
+ except Exception as e:
385
+ print(f"Error in loop: {e}")
386
+ import traceback
387
+ traceback.print_exc()
388
+ break
389
+
390
+ # FPS Limiting
391
+ if target_frame_time > 0:
392
+ elapsed = time.time() - start_time
393
+ wait = target_frame_time - elapsed
394
+ if wait > 0:
395
+ time.sleep(wait)
396
+
397
+ glfw.terminate()
398
+
399
+ def print(
400
+ self,
401
+ text_or_label: str|TextLabel,
402
+ position: tuple[float, float],
403
+ scale: float = 1.0,
404
+ style: TextStyle = DEFAULT_TEXT_STYLE,
405
+ pivot: Pivots = Pivots.TOP_LEFT,
406
+ save_cache: bool = False
407
+ ) -> Optional[TextLabel]:
408
+
409
+ if isinstance(text_or_label, TextLabel):
410
+ text_or_label.draw()
411
+ else:
412
+ if save_cache:
413
+ return self.text_renderer.create_label(str(text_or_label), position[0], position[1], scale, style, pivot)
414
+ else:
415
+ self.text_renderer.draw_text(str(text_or_label), position, scale, style, pivot)
416
+
417
+ # ========== Shape Drawing Methods ==========
418
+
419
+ def draw_circle(self, center: tuple[float, float], radius: float, **kwargs) -> None:
420
+ """Draw a circle. See ShapeRenderer.draw_circle for parameters."""
421
+ self.shape_renderer.draw_circle(center, radius, **kwargs)
422
+
423
+ def draw_rect(self, position: tuple[float, float], size: tuple[float, float], **kwargs) -> None:
424
+ """Draw a rectangle. See ShapeRenderer.draw_rect for parameters."""
425
+ self.shape_renderer.draw_rect(position, size, **kwargs)
426
+
427
+ def draw_line(self, start: tuple[float, float], end: tuple[float, float], **kwargs) -> None:
428
+ """Draw a line. See ShapeRenderer.draw_line for parameters."""
429
+ self.shape_renderer.draw_line(start, end, **kwargs)
430
+
431
+ def draw_lines(self, points, **kwargs) -> None:
432
+ """Draw a polyline. See ShapeRenderer.draw_lines for parameters."""
433
+ self.shape_renderer.draw_lines(points, **kwargs)
434
+
435
+ def create_circle(self, center: tuple[float, float], radius: float, **kwargs) -> ShapeLabel:
436
+ """Create a cached circle. See ShapeRenderer.create_circle for parameters."""
437
+ return self.shape_renderer.create_circle(center, radius, **kwargs)
438
+
439
+ def create_rect(self, position: tuple[float, float], size: tuple[float, float], **kwargs) -> ShapeLabel:
440
+ """Create a cached rectangle. See ShapeRenderer.create_rect for parameters."""
441
+ return self.shape_renderer.create_rect(position, size, **kwargs)
442
+
443
+ def create_line(self, start: tuple[float, float], end: tuple[float, float], **kwargs) -> ShapeLabel:
444
+ """Create a cached line. See ShapeRenderer.create_line for parameters."""
445
+ return self.shape_renderer.create_line(start, end, **kwargs)
446
+
447
+ def create_lines(self, points, **kwargs) -> ShapeLabel:
448
+ """Create a cached polyline. See ShapeRenderer.create_lines for parameters."""
449
+ return self.shape_renderer.create_lines(points, **kwargs)
450
+
451
+ def create_circle_batch(self, max_shapes: int = 10000) -> InstancedShapeBatch:
452
+ """Create a batch for drawing multiple circles using GPU instancing."""
453
+ return self.shape_renderer.create_circle_batch(max_shapes)
454
+
455
+ def create_rect_batch(self, max_shapes: int = 10000) -> InstancedShapeBatch:
456
+ """Create a batch for drawing multiple rectangles using GPU instancing."""
457
+ return self.shape_renderer.create_rect_batch(max_shapes)
458
+
459
+ def create_line_batch(self, max_shapes: int = 10000) -> InstancedShapeBatch:
460
+ """Create a batch for drawing multiple lines using GPU instancing."""
461
+ return self.shape_renderer.create_line_batch(max_shapes)
e2D/commons.py ADDED
@@ -0,0 +1,56 @@
1
+ import moderngl
2
+ import numpy as np
3
+
4
+ def _is_array_uniform(u: moderngl.Uniform) -> bool:
5
+ return u.array_length > 1
6
+
7
+ def get_pattr(prog_id:str|moderngl.Program, name:str, *, programs: dict[str, moderngl.Program]= {}) -> moderngl.Uniform | moderngl.UniformBlock | moderngl.Attribute | moderngl.Varying:
8
+ if isinstance(prog_id, moderngl.Program):
9
+ return prog_id[name]
10
+ if prog_id not in programs:
11
+ raise ValueError(f"Program with id '{prog_id}' does not exist.")
12
+ return programs[prog_id][name]
13
+
14
+ def get_uniform(prog_id:str|moderngl.Program|moderngl.ComputeShader, name:str, *, compute_shaders: dict[str, moderngl.ComputeShader]= {}, programs: dict[str, moderngl.Program]= {}) -> moderngl.Uniform:
15
+ """Get a uniform from a program or compute shader with proper typing.
16
+
17
+ This is a type-safe alternative to program[name] which returns a union type.
18
+ Raises TypeError if the attribute is not a Uniform.
19
+ """
20
+ if isinstance(prog_id, str):
21
+ if prog_id in programs:
22
+ prog_id = programs[prog_id]
23
+ elif prog_id in compute_shaders:
24
+ prog_id = compute_shaders[prog_id]
25
+ else:
26
+ raise ValueError(f"Program or compute shader with id '{prog_id}' does not exist.")
27
+
28
+ attr = prog_id[name]
29
+ if not isinstance(attr, moderngl.Uniform):
30
+ raise TypeError(f"'{name}' is {type(attr).__name__}, not a Uniform")
31
+ return attr
32
+
33
+ def get_pattr_value(prog_id:str|moderngl.Program, name:str, *, programs: dict[str, moderngl.Program]= {}) -> int|float|tuple|list:
34
+ attr = get_pattr(prog_id, name, programs=programs)
35
+ if not isinstance(attr, moderngl.Uniform):
36
+ raise TypeError(f"'{name}' is {type(attr).__name__}, not a Uniform")
37
+ if _is_array_uniform(attr):
38
+ raise TypeError(f"Uniform '{name}' is an array; cannot use .value")
39
+ return attr.value
40
+
41
+ def set_pattr_value(prog_id:str|moderngl.Program, name: str, value, *, programs: dict[str, moderngl.Program]= {}, force_write: bool= False) -> None:
42
+ attr = get_pattr(prog_id, name, programs=programs)
43
+
44
+ if not isinstance(attr, moderngl.Uniform):
45
+ raise TypeError(f"'{name}' is {type(attr).__name__}, not a Uniform")
46
+
47
+ use_write = force_write or _is_array_uniform(attr)
48
+ if use_write:
49
+ if isinstance(value, np.ndarray):
50
+ data = value
51
+ else:
52
+ data = np.array(value, dtype="f4")
53
+
54
+ attr.write(data.tobytes())
55
+ else:
56
+ attr.value = value