pyverse2d 0.4.2__tar.gz
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.
- pyverse2d-0.4.2/LICENSE +21 -0
- pyverse2d-0.4.2/PKG-INFO +16 -0
- pyverse2d-0.4.2/README.md +2 -0
- pyverse2d-0.4.2/pyproject.toml +23 -0
- pyverse2d-0.4.2/pyverse2d/__init__.py +72 -0
- pyverse2d-0.4.2/pyverse2d/_flag/__init__.py +9 -0
- pyverse2d-0.4.2/pyverse2d/_flag/_stack_mode.py +8 -0
- pyverse2d-0.4.2/pyverse2d/_flag/_update_phase.py +8 -0
- pyverse2d-0.4.2/pyverse2d/_internal/__init__.py +19 -0
- pyverse2d-0.4.2/pyverse2d/_internal/validators.py +247 -0
- pyverse2d-0.4.2/pyverse2d/_rendering/__init__.py +13 -0
- pyverse2d-0.4.2/pyverse2d/_rendering/_camera.py +138 -0
- pyverse2d-0.4.2/pyverse2d/_rendering/_pipeline.py +61 -0
- pyverse2d-0.4.2/pyverse2d/_rendering/_screen.py +45 -0
- pyverse2d-0.4.2/pyverse2d/_rendering/_viewport.py +111 -0
- pyverse2d-0.4.2/pyverse2d/_rendering/_window.py +212 -0
- pyverse2d-0.4.2/pyverse2d/_version.py +5 -0
- pyverse2d-0.4.2/pyverse2d/abc/__init__.py +23 -0
- pyverse2d-0.4.2/pyverse2d/abc/_asset.py +11 -0
- pyverse2d-0.4.2/pyverse2d/abc/_component.py +8 -0
- pyverse2d-0.4.2/pyverse2d/abc/_layer.py +23 -0
- pyverse2d-0.4.2/pyverse2d/abc/_math_object.py +39 -0
- pyverse2d-0.4.2/pyverse2d/abc/_shape.py +45 -0
- pyverse2d-0.4.2/pyverse2d/abc/_system.py +21 -0
- pyverse2d-0.4.2/pyverse2d/asset/__init__.py +11 -0
- pyverse2d-0.4.2/pyverse2d/asset/_color.py +95 -0
- pyverse2d-0.4.2/pyverse2d/asset/_image.py +114 -0
- pyverse2d-0.4.2/pyverse2d/asset/_text.py +74 -0
- pyverse2d-0.4.2/pyverse2d/map/__init__.py +3 -0
- pyverse2d-0.4.2/pyverse2d/math/__init__.py +11 -0
- pyverse2d-0.4.2/pyverse2d/math/_line.py +307 -0
- pyverse2d-0.4.2/pyverse2d/math/_point.py +256 -0
- pyverse2d-0.4.2/pyverse2d/math/_vector.py +330 -0
- pyverse2d-0.4.2/pyverse2d/scene/__init__.py +76 -0
- pyverse2d-0.4.2/pyverse2d/scene/_scene.py +117 -0
- pyverse2d-0.4.2/pyverse2d/scene/_world_layer.py +48 -0
- pyverse2d-0.4.2/pyverse2d/shape/__init__.py +17 -0
- pyverse2d-0.4.2/pyverse2d/shape/_capsule.py +117 -0
- pyverse2d-0.4.2/pyverse2d/shape/_circle.py +96 -0
- pyverse2d-0.4.2/pyverse2d/shape/_ellipse.py +117 -0
- pyverse2d-0.4.2/pyverse2d/shape/_polygon.py +161 -0
- pyverse2d-0.4.2/pyverse2d/shape/_rect.py +110 -0
- pyverse2d-0.4.2/pyverse2d/shape/_segment.py +137 -0
- pyverse2d-0.4.2/pyverse2d/tool/__init__.py +7 -0
- pyverse2d-0.4.2/pyverse2d/tool/_walls_definer.py +7 -0
- pyverse2d-0.4.2/pyverse2d/ui/__init__.py +0 -0
- pyverse2d-0.4.2/pyverse2d/world/__init__.py +37 -0
- pyverse2d-0.4.2/pyverse2d/world/_component/__init__.py +17 -0
- pyverse2d-0.4.2/pyverse2d/world/_component/_collider.py +103 -0
- pyverse2d-0.4.2/pyverse2d/world/_component/_rigid_body.py +212 -0
- pyverse2d-0.4.2/pyverse2d/world/_component/_shape_renderer.py +94 -0
- pyverse2d-0.4.2/pyverse2d/world/_component/_sprite_renderer.py +95 -0
- pyverse2d-0.4.2/pyverse2d/world/_component/_text_renderer.py +95 -0
- pyverse2d-0.4.2/pyverse2d/world/_component/_transform.py +130 -0
- pyverse2d-0.4.2/pyverse2d/world/_entity.py +159 -0
- pyverse2d-0.4.2/pyverse2d/world/_system/__init__.py +13 -0
- pyverse2d-0.4.2/pyverse2d/world/_system/_collision.py +892 -0
- pyverse2d-0.4.2/pyverse2d/world/_system/_gravity.py +57 -0
- pyverse2d-0.4.2/pyverse2d/world/_system/_physics.py +44 -0
- pyverse2d-0.4.2/pyverse2d/world/_system/_render.py +249 -0
- pyverse2d-0.4.2/pyverse2d/world/_world.py +179 -0
- pyverse2d-0.4.2/pyverse2d.egg-info/PKG-INFO +16 -0
- pyverse2d-0.4.2/pyverse2d.egg-info/SOURCES.txt +65 -0
- pyverse2d-0.4.2/pyverse2d.egg-info/dependency_links.txt +1 -0
- pyverse2d-0.4.2/pyverse2d.egg-info/requires.txt +1 -0
- pyverse2d-0.4.2/pyverse2d.egg-info/top_level.txt +1 -0
- pyverse2d-0.4.2/setup.cfg +4 -0
pyverse2d-0.4.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 WhiteWolf45380
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyverse2d-0.4.2/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyverse2d
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: 2D Game Engine using pyglet (OpenGL) for rendering
|
|
5
|
+
Author: WhiteWolf45380
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/WhiteWolf45380/pyverse2d
|
|
8
|
+
Keywords: game,engine,2d,pyglet,opengl
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pyglet>=2.0
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
pip install https://github.com/WhiteWolf45380/PyVerse2D/archive/refs/heads/main.zip
|
|
16
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyverse2d"
|
|
7
|
+
version = "0.4.2"
|
|
8
|
+
description = "2D Game Engine using pyglet (OpenGL) for rendering"
|
|
9
|
+
authors = [{ name = "WhiteWolf45380" }]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["game", "engine", "2d", "pyglet", "opengl"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"pyglet>=2.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/WhiteWolf45380/pyverse2d"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["."]
|
|
23
|
+
include = ["pyverse2d*"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from . import abc, math, shape, asset, world, map, ui, scene
|
|
5
|
+
|
|
6
|
+
from ._rendering import Camera, Viewport, Screen, Window
|
|
7
|
+
|
|
8
|
+
import pyglet
|
|
9
|
+
|
|
10
|
+
# ======================================== STATE ========================================
|
|
11
|
+
_window: Window | None = None
|
|
12
|
+
_fps: int = 60
|
|
13
|
+
|
|
14
|
+
# ======================================== SETTERS ========================================
|
|
15
|
+
def set_window(window: Window):
|
|
16
|
+
"""
|
|
17
|
+
Définit la fenêtre du moteur
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
window (Window): fenêtre à utiliser
|
|
21
|
+
"""
|
|
22
|
+
global _window
|
|
23
|
+
if not isinstance(window, Window):
|
|
24
|
+
raise TypeError("Expected a Window instance")
|
|
25
|
+
_window = window
|
|
26
|
+
|
|
27
|
+
@_window.native.event
|
|
28
|
+
def on_draw():
|
|
29
|
+
_window.clear()
|
|
30
|
+
scene.draw()
|
|
31
|
+
|
|
32
|
+
def set_fps(fps: int):
|
|
33
|
+
"""
|
|
34
|
+
Définit le nombre de mises à jour par seconde
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
fps (int): fps cible
|
|
38
|
+
"""
|
|
39
|
+
global _fps
|
|
40
|
+
_fps = int(fps)
|
|
41
|
+
|
|
42
|
+
# ======================================== LOOP ========================================
|
|
43
|
+
def run():
|
|
44
|
+
"""Démarre la boucle de mise à jour"""
|
|
45
|
+
if _window is None:
|
|
46
|
+
raise RuntimeError("No window set — call engine.set_window() before engine.run()")
|
|
47
|
+
pyglet.clock.schedule_interval(_update, 1 / _fps)
|
|
48
|
+
pyglet.app.run()
|
|
49
|
+
|
|
50
|
+
def _update(dt: float):
|
|
51
|
+
scene.update(dt)
|
|
52
|
+
|
|
53
|
+
# ======================================== EXPORTS ========================================
|
|
54
|
+
__all__ = [
|
|
55
|
+
"Camera",
|
|
56
|
+
"Viewport",
|
|
57
|
+
"Screen",
|
|
58
|
+
"Window",
|
|
59
|
+
|
|
60
|
+
"abc",
|
|
61
|
+
"math",
|
|
62
|
+
"shape",
|
|
63
|
+
"asset",
|
|
64
|
+
"world",
|
|
65
|
+
"map",
|
|
66
|
+
"ui",
|
|
67
|
+
"scene",
|
|
68
|
+
|
|
69
|
+
"set_window",
|
|
70
|
+
"set_fps",
|
|
71
|
+
"run",
|
|
72
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from ._stack_mode import StackMode
|
|
3
|
+
from ._update_phase import UpdatePhase
|
|
4
|
+
|
|
5
|
+
# ======================================== EXPORTS ========================================
|
|
6
|
+
__all__ = [
|
|
7
|
+
"StackMode",
|
|
8
|
+
"UpdatePhase",
|
|
9
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
# ======================================== FLAG ========================================
|
|
5
|
+
class StackMode(Enum):
|
|
6
|
+
PAUSE = "pause" # scene du dessous stop tout
|
|
7
|
+
SUSPEND = "suspend" # scene du dessous stop update mais continue draw
|
|
8
|
+
OVERLAY = "overlay" # scene du dessous continue tout
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
# ======================================== FLAG ========================================
|
|
5
|
+
class UpdatePhase(Enum):
|
|
6
|
+
EARLY = 0 # Pré-actualisation
|
|
7
|
+
UPDATE = 1 # Actualisation
|
|
8
|
+
LATE = 2 # Post-actualisation
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from .validators import (
|
|
3
|
+
typename,
|
|
4
|
+
expect,
|
|
5
|
+
not_null,
|
|
6
|
+
positive,
|
|
7
|
+
clamped,
|
|
8
|
+
rgba
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# ======================================== EXPORTS ========================================
|
|
12
|
+
__all__ = [
|
|
13
|
+
"typename",
|
|
14
|
+
"expect",
|
|
15
|
+
"not_null",
|
|
16
|
+
"positive",
|
|
17
|
+
"clamped",
|
|
18
|
+
"rgba",
|
|
19
|
+
]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from types import UnionType
|
|
3
|
+
from typing import Tuple, get_args, get_origin, Union
|
|
4
|
+
from numbers import Real
|
|
5
|
+
|
|
6
|
+
# ======================================== TYPE CHECK ========================================
|
|
7
|
+
def typename(t: type):
|
|
8
|
+
"""
|
|
9
|
+
Renvoie le str du type
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
t(type): type à vérifier
|
|
13
|
+
"""
|
|
14
|
+
return getattr(t, "__name__", str(t))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def expect(value: object, types: type | Tuple[type, ...]):
|
|
18
|
+
"""
|
|
19
|
+
Vérifie la valeur contre un type supporté :
|
|
20
|
+
T type simple
|
|
21
|
+
(T1, T2, T3) multi-types
|
|
22
|
+
T1 | T2 union
|
|
23
|
+
list[T] liste typée
|
|
24
|
+
set[T] set typé
|
|
25
|
+
tuple[T] tuple broadcast
|
|
26
|
+
tuple[T1, T2, T3] tuple positionnel
|
|
27
|
+
dict[K, V] dictionnaire typé
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
value(object): valeur à vérifier
|
|
31
|
+
types(type|Tuple[type, ...]): types à vérifier
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
value(object): si valide
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
TypeError: si la valeur n'est pas conforme
|
|
38
|
+
"""
|
|
39
|
+
# (T1, T2, T3)
|
|
40
|
+
if isinstance(types, tuple):
|
|
41
|
+
types = tuple(type(None) if t is None else t for t in types)
|
|
42
|
+
if not isinstance(value, types):
|
|
43
|
+
readable = " | ".join(typename(t) for t in types)
|
|
44
|
+
raise TypeError(f"expected ({readable}), got ({typename(value)})")
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
# T
|
|
48
|
+
origin = get_origin(types)
|
|
49
|
+
if origin is None:
|
|
50
|
+
types = type(None) if types is None else types
|
|
51
|
+
if not isinstance(value, types):
|
|
52
|
+
raise TypeError(f"expected ({typename(types)}), got ({typename(value)})")
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
# T1 |T2
|
|
56
|
+
args = get_args(types)
|
|
57
|
+
if origin in (UnionType, Union):
|
|
58
|
+
for t in args:
|
|
59
|
+
try:
|
|
60
|
+
return expect(value, t)
|
|
61
|
+
except TypeError:
|
|
62
|
+
continue
|
|
63
|
+
readable = " | ".join(typename(t) for t in args)
|
|
64
|
+
raise TypeError(f"expected ({readable}), got ({typename(value)})")
|
|
65
|
+
|
|
66
|
+
# list[T] / set[T] / frozenset[T]
|
|
67
|
+
if origin in (list, set, frozenset):
|
|
68
|
+
if not isinstance(value, origin):
|
|
69
|
+
raise TypeError(f"expected ({typename(origin)}), got ({typename(value)})")
|
|
70
|
+
inner = args[0]
|
|
71
|
+
if get_origin(inner) is None:
|
|
72
|
+
for i, v in enumerate(value):
|
|
73
|
+
if not isinstance(v, inner):
|
|
74
|
+
raise TypeError(f"at index {i}: expected ({typename(inner)}), got ({typename(v)})")
|
|
75
|
+
else:
|
|
76
|
+
for i, v in enumerate(value):
|
|
77
|
+
try:
|
|
78
|
+
expect(v, inner)
|
|
79
|
+
except TypeError as e:
|
|
80
|
+
raise TypeError(f"at index {i}: {e}") from e
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
# tuple[T] / tuple[T1, T2, T3]
|
|
84
|
+
if origin is tuple:
|
|
85
|
+
if not isinstance(value, tuple):
|
|
86
|
+
raise TypeError(f"expected (tuple), got ({typename(value)})")
|
|
87
|
+
if len(args) == 1 or len(args) == 2 and args[1] is Ellipsis:
|
|
88
|
+
inner = args[0]
|
|
89
|
+
if get_origin(inner) is None:
|
|
90
|
+
for i, v in enumerate(value):
|
|
91
|
+
if not isinstance(v, inner):
|
|
92
|
+
raise TypeError(f"at index {i}: expected ({typename(inner)}), got ({typename(v)})")
|
|
93
|
+
else:
|
|
94
|
+
for i, v in enumerate(value):
|
|
95
|
+
try:
|
|
96
|
+
expect(v, inner)
|
|
97
|
+
except TypeError as e:
|
|
98
|
+
raise TypeError(f"at index {i}: {e}") from e
|
|
99
|
+
else:
|
|
100
|
+
if len(value) != len(args):
|
|
101
|
+
raise TypeError(f"expected (tuple) of len {len(args)}, got {len(value)}")
|
|
102
|
+
for i, (v, t) in enumerate(zip(value, args)):
|
|
103
|
+
if get_origin(t) is None:
|
|
104
|
+
if not isinstance(v, t):
|
|
105
|
+
raise TypeError(f"at index {i}: expected ({typename(t)}), got ({typename(v)})")
|
|
106
|
+
else:
|
|
107
|
+
try:
|
|
108
|
+
expect(v, t)
|
|
109
|
+
except TypeError as e:
|
|
110
|
+
raise TypeError(f"at index {i}: {e}") from e
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
# dict[K, V]
|
|
114
|
+
if origin is dict:
|
|
115
|
+
if not isinstance(value, dict):
|
|
116
|
+
raise TypeError(f"expected (dict), got ({typename(value)})")
|
|
117
|
+
kt, vt = args
|
|
118
|
+
kt_simple = get_origin(kt) is None
|
|
119
|
+
vt_simple = get_origin(vt) is None
|
|
120
|
+
for k, v in value.items():
|
|
121
|
+
if kt_simple:
|
|
122
|
+
if not isinstance(k, kt):
|
|
123
|
+
raise TypeError(f"at key {k!r}: invalid key: expected ({typename(kt)}), got ({typename(k)})")
|
|
124
|
+
else:
|
|
125
|
+
try:
|
|
126
|
+
expect(k, kt)
|
|
127
|
+
except TypeError as e:
|
|
128
|
+
raise TypeError(f"at key {k!r}: invalid key: {e}") from e
|
|
129
|
+
if vt_simple:
|
|
130
|
+
if not isinstance(v, vt):
|
|
131
|
+
raise TypeError(f"at key {k!r}: expected ({typename(vt)}), got ({typename(v)})")
|
|
132
|
+
else:
|
|
133
|
+
try:
|
|
134
|
+
expect(v, vt)
|
|
135
|
+
except TypeError as e:
|
|
136
|
+
raise TypeError(f"at key {k!r}: {e}") from e
|
|
137
|
+
return value
|
|
138
|
+
|
|
139
|
+
raise TypeError(f"unsupported type annotation: {types!r}")
|
|
140
|
+
|
|
141
|
+
# ======================================== VALUE CHECK ========================================
|
|
142
|
+
def not_null(value: object, arg: str = "Argument"):
|
|
143
|
+
"""
|
|
144
|
+
Vérifie que la valeur ne soit pas nulle
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
value(object): valeur à vérifier
|
|
148
|
+
arg(str): nom de l'argument à vérifier
|
|
149
|
+
"""
|
|
150
|
+
# None
|
|
151
|
+
if value is None:
|
|
152
|
+
raise ValueError(f"{arg} cannot be None")
|
|
153
|
+
|
|
154
|
+
# Nombre
|
|
155
|
+
if isinstance(value, (int, float, complex)):
|
|
156
|
+
if value == 0:
|
|
157
|
+
raise ValueError(f"{arg} cannot be None")
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
# Types composés
|
|
161
|
+
if isinstance(value, (str, list, tuple, set, dict, frozenset)):
|
|
162
|
+
if len(value) == 0:
|
|
163
|
+
raise ValueError(f"{arg} cannot be empty")
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
# Objet possédant une méthode __len__
|
|
167
|
+
if hasattr(value, "__len__"):
|
|
168
|
+
if len(value) == 0:
|
|
169
|
+
raise ValueError(f"{arg} cannot be empty")
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
# Objet custom
|
|
173
|
+
return value
|
|
174
|
+
|
|
175
|
+
def positive(value: object, arg: str = "Argument"):
|
|
176
|
+
"""
|
|
177
|
+
Vérifie que la valeur soit positive
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
value(object): valeur à vérifier
|
|
181
|
+
arg(str): nom de l'argument à vérifier
|
|
182
|
+
"""
|
|
183
|
+
# Nombres
|
|
184
|
+
if isinstance(value, Real):
|
|
185
|
+
if float(value) < 0:
|
|
186
|
+
raise ValueError(f"{arg} cannot be negative")
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
# Par défaut
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
def clamped(value: object, min: float = 0.0, max: float = 1.0, arg: str ="Argument"):
|
|
193
|
+
"""
|
|
194
|
+
Vérifie que la valeur soit comprise entre min et max
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
value(object): valeur à vérifier
|
|
198
|
+
min(float, optional): valeur minimale autorisée
|
|
199
|
+
max(float, optional): valeur maximale autorisée
|
|
200
|
+
arg(str, optional): nom de l'argument à vérifier
|
|
201
|
+
"""
|
|
202
|
+
# Nombres
|
|
203
|
+
if isinstance(value, Real):
|
|
204
|
+
if float(value) < min or float(value) > max:
|
|
205
|
+
raise ValueError(f"{arg} must be between {min} and {max}")
|
|
206
|
+
return value
|
|
207
|
+
|
|
208
|
+
# Par défaut
|
|
209
|
+
return value
|
|
210
|
+
|
|
211
|
+
# ======================================== CONVERSIONS ========================================
|
|
212
|
+
def rgba(value: object, argument: str = "Argument") -> tuple[int, int, int, float]:
|
|
213
|
+
"""
|
|
214
|
+
Renvoie, si cela est possible, la valeur en couleur rgba (255, 255, 255, 1.0)
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
value(object): valeur à convertir
|
|
218
|
+
"""
|
|
219
|
+
# Type
|
|
220
|
+
if type(value) is not tuple:
|
|
221
|
+
raise TypeError(f"{argument} doit être un tuple")
|
|
222
|
+
|
|
223
|
+
# Taille
|
|
224
|
+
n = len(value)
|
|
225
|
+
if n == 3:
|
|
226
|
+
r, g, b = value
|
|
227
|
+
a = 1.0
|
|
228
|
+
elif n == 4:
|
|
229
|
+
r, g, b, a = value
|
|
230
|
+
elif n < 3:
|
|
231
|
+
v = value + (0, 0, 0, 1.0)
|
|
232
|
+
r, g, b, a = v[0], v[1], v[2], v[3]
|
|
233
|
+
else:
|
|
234
|
+
raise ValueError(f"{argument} doit être un tuple de longueur <= 4")
|
|
235
|
+
|
|
236
|
+
# RGB
|
|
237
|
+
if type(r) is float: r = int(r * 255 + 0.5)
|
|
238
|
+
if type(g) is float: g = int(g * 255 + 0.5)
|
|
239
|
+
if type(b) is float: b = int(b * 255 + 0.5)
|
|
240
|
+
|
|
241
|
+
# Alpha
|
|
242
|
+
if type(a) is int:
|
|
243
|
+
a = a / 255
|
|
244
|
+
else:
|
|
245
|
+
a = float(a)
|
|
246
|
+
|
|
247
|
+
return r, g, b, a
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from ._camera import Camera
|
|
3
|
+
from ._viewport import Viewport
|
|
4
|
+
from ._screen import Screen
|
|
5
|
+
from ._window import Window
|
|
6
|
+
|
|
7
|
+
# ======================================== EXPORTS ========================================
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Camera",
|
|
10
|
+
"Viewport",
|
|
11
|
+
"Screen",
|
|
12
|
+
"Window",
|
|
13
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .._internal import expect, not_null, positive
|
|
5
|
+
from ..math import Point, Vector
|
|
6
|
+
from ..world import Entity, Transform
|
|
7
|
+
|
|
8
|
+
from pyglet.math import Mat4, Vec3
|
|
9
|
+
from numbers import Real
|
|
10
|
+
|
|
11
|
+
# ======================================== CAMERA ========================================
|
|
12
|
+
class Camera:
|
|
13
|
+
"""
|
|
14
|
+
Définit le point de vue dans le monde
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
pos(Point): position de la caméra
|
|
18
|
+
zoom (Real): facteur de zoom
|
|
19
|
+
"""
|
|
20
|
+
__slots__ = ("_pos", "_following", "_offset", "_zoom")
|
|
21
|
+
|
|
22
|
+
def __init__(self, pos: Point = (0.0, 0.0), zoom: Real = 1.0):
|
|
23
|
+
self._pos: Point = Point(pos)
|
|
24
|
+
self._following: Entity | None = None
|
|
25
|
+
self._offset: Vector = Vector(0.0, 0.0)
|
|
26
|
+
self._zoom: float = float(positive(not_null(expect(zoom, Real))))
|
|
27
|
+
|
|
28
|
+
# ======================================== GETTERS ========================================
|
|
29
|
+
@property
|
|
30
|
+
def x(self) -> float:
|
|
31
|
+
"""Renvoie la position horizontale"""
|
|
32
|
+
return self._pos.x
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def y(self) -> float:
|
|
36
|
+
"""Renvoie la position verticale"""
|
|
37
|
+
return self._pos.y
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def pos(self) -> Point:
|
|
41
|
+
"""Renvoie la position"""
|
|
42
|
+
return self._pos
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def offset(self) -> Vector:
|
|
46
|
+
"""Renvoie le vecteur de décalage à la position"""
|
|
47
|
+
return self._offset
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def final_x(self) -> float:
|
|
51
|
+
"""Position horizontale finale"""
|
|
52
|
+
self._check_follow()
|
|
53
|
+
base = self._following.get(Transform).x if self._following else self._pos.x
|
|
54
|
+
return base + self._offset.x
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def final_y(self) -> float:
|
|
58
|
+
"""Position verticale finale"""
|
|
59
|
+
self._check_follow()
|
|
60
|
+
base = self._following.get(Transform).y if self._following else self._pos.y
|
|
61
|
+
return base + self._offset.y
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def final_pos(self) -> Point:
|
|
65
|
+
"""Renvoie la position finale"""
|
|
66
|
+
self._check_follow()
|
|
67
|
+
base = self._following.get(Transform).pos if self._following else self._pos
|
|
68
|
+
return base + self._offset
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def zoom(self) -> float:
|
|
72
|
+
return self._zoom
|
|
73
|
+
|
|
74
|
+
# ======================================== SETTERS ========================================
|
|
75
|
+
@x.setter
|
|
76
|
+
def x(self, value: Real):
|
|
77
|
+
self._pos.x = float(expect(value, Real))
|
|
78
|
+
|
|
79
|
+
@y.setter
|
|
80
|
+
def y(self, value: Real):
|
|
81
|
+
self._pos.y = float(expect(value, Real))
|
|
82
|
+
|
|
83
|
+
@pos.setter
|
|
84
|
+
def pos(self, value: Point):
|
|
85
|
+
self._pos = Point(value)
|
|
86
|
+
|
|
87
|
+
@zoom.setter
|
|
88
|
+
def zoom(self, value: Real):
|
|
89
|
+
if float(value) <= 0:
|
|
90
|
+
raise ValueError("Zoom must be greater than 0")
|
|
91
|
+
self._zoom = float(value)
|
|
92
|
+
|
|
93
|
+
# ======================================== FOLLOW ========================================
|
|
94
|
+
def follow(self, entity: Entity):
|
|
95
|
+
"""
|
|
96
|
+
Suit le Transform d'une entité
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
entity (Entity): entité à suivre
|
|
100
|
+
"""
|
|
101
|
+
if not entity.has(Transform):
|
|
102
|
+
raise ValueError(f"Entity {entity.id[:8]}... has no Transform component")
|
|
103
|
+
self._following = entity
|
|
104
|
+
|
|
105
|
+
def unfollow(self):
|
|
106
|
+
"""Détache la camera de l'entité suivie"""
|
|
107
|
+
self._following = None
|
|
108
|
+
|
|
109
|
+
def _check_follow(self):
|
|
110
|
+
"""Unfollow automatique si l'entité est inactive"""
|
|
111
|
+
if self._following is not None and not self._following.is_active():
|
|
112
|
+
self._following = None
|
|
113
|
+
|
|
114
|
+
# ======================================== DÉPLACEMENT ========================================
|
|
115
|
+
def move(self, vector: Vector):
|
|
116
|
+
"""
|
|
117
|
+
Déplace la position manuelle de la camera
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
vectorr(Vector): vecteur de translation
|
|
121
|
+
"""
|
|
122
|
+
self._pos += Vector(vector)
|
|
123
|
+
|
|
124
|
+
# ======================================== RENDU ========================================
|
|
125
|
+
def view_matrix(self, virtual_width: int, virtual_height: int) -> Mat4:
|
|
126
|
+
"""
|
|
127
|
+
Produit la matrice de vue à appliquer à la fenêtre
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
virtual_width (int): largeur de l'espace virtuel
|
|
131
|
+
virtual_height (int): hauteur de l'espace virtuel
|
|
132
|
+
"""
|
|
133
|
+
cx = virtual_width / 2
|
|
134
|
+
cy = virtual_height / 2
|
|
135
|
+
fx, fy = self.final_pos
|
|
136
|
+
translate = Mat4.from_translation(Vec3(cx - fx, cy - fy, 0))
|
|
137
|
+
scale = Mat4.from_scale(Vec3(self._zoom, self._zoom, 1))
|
|
138
|
+
return translate @ scale
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# ======================================== IMPORTS ========================================
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pyglet
|
|
5
|
+
import pyglet.gl as gl
|
|
6
|
+
from pyglet.graphics import Batch, Group
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..scene import Scene
|
|
12
|
+
|
|
13
|
+
# ======================================== RENDERER ========================================
|
|
14
|
+
class Pipeline:
|
|
15
|
+
"""
|
|
16
|
+
Pipeline de rendu exploitant OpenGL
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
virtual_width (int): largeur de l'espace virtuel
|
|
20
|
+
virtual_height (int): hauteur de l'espace virtuel
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, virtual_width: int, virtual_height: int):
|
|
24
|
+
self._virtual_width: int = int(virtual_width)
|
|
25
|
+
self._virtual_height: int = int(virtual_height)
|
|
26
|
+
self._batch: Batch = Batch()
|
|
27
|
+
self._groups: dict[int, Group] = {}
|
|
28
|
+
|
|
29
|
+
# ======================================== GETTERS ========================================
|
|
30
|
+
@property
|
|
31
|
+
def batch(self) -> Batch:
|
|
32
|
+
"""Renvoie le batch global"""
|
|
33
|
+
return self._batch
|
|
34
|
+
|
|
35
|
+
def get_group(self, z: int = 0) -> Group:
|
|
36
|
+
"""
|
|
37
|
+
Renvoie le Group associé au z_order, le crée si inexistant
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
z (int): z_order du group
|
|
41
|
+
"""
|
|
42
|
+
if z not in self._groups:
|
|
43
|
+
self._groups[z] = Group(order=z)
|
|
44
|
+
return self._groups[z]
|
|
45
|
+
|
|
46
|
+
# ======================================== PIPELINE ========================================
|
|
47
|
+
def begin(self, scene: Scene):
|
|
48
|
+
"""
|
|
49
|
+
Configure le contexte de rendu depuis la scene active.
|
|
50
|
+
Applique la caméra et le viewport.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
scene (Scene): scene à rendre
|
|
54
|
+
"""
|
|
55
|
+
x, y, w, h = scene.viewport.resolve(self._virtual_width, self._virtual_height)
|
|
56
|
+
gl.glViewport(int(x), int(y), int(w), int(h))
|
|
57
|
+
pyglet.get_default_window().view = scene.camera.view_matrix(self._virtual_width, self._virtual_height)
|
|
58
|
+
|
|
59
|
+
def flush(self):
|
|
60
|
+
"""Envoie tout le batch au GPU"""
|
|
61
|
+
self._batch.draw()
|