aether-engine 1.0.0__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.
Files changed (64) hide show
  1. aether/__init__.py +54 -0
  2. aether/audio/__init__.py +0 -0
  3. aether/audio/audio_manager.py +211 -0
  4. aether/audio/music.py +6 -0
  5. aether/audio/sound.py +6 -0
  6. aether/core/__init__.py +211 -0
  7. aether/core/achievement_system.py +304 -0
  8. aether/core/application.py +19 -0
  9. aether/core/config.py +75 -0
  10. aether/core/console.py +366 -0
  11. aether/core/engine.py +50 -0
  12. aether/core/event_bus.py +24 -0
  13. aether/core/input.py +58 -0
  14. aether/core/plugin_system.py +93 -0
  15. aether/core/save_system.py +409 -0
  16. aether/core/time_manager.py +25 -0
  17. aether/core/window.py +57 -0
  18. aether/graphics/__init__.py +0 -0
  19. aether/graphics/camera.py +69 -0
  20. aether/graphics/framebuffer.py +119 -0
  21. aether/graphics/light.py +32 -0
  22. aether/graphics/lod_system.py +170 -0
  23. aether/graphics/material.py +25 -0
  24. aether/graphics/mesh.py +83 -0
  25. aether/graphics/mesh_factory.py +67 -0
  26. aether/graphics/particle_system.py +268 -0
  27. aether/graphics/post_process.py +279 -0
  28. aether/graphics/renderer.py +135 -0
  29. aether/graphics/shader.py +61 -0
  30. aether/graphics/shader_library.py +157 -0
  31. aether/graphics/sky_atmosphere.py +309 -0
  32. aether/graphics/terrain.py +408 -0
  33. aether/graphics/texture.py +34 -0
  34. aether/graphics/water.py +432 -0
  35. aether/physics/__init__.py +0 -0
  36. aether/physics/bullet_backend.py +395 -0
  37. aether/physics/collider.py +22 -0
  38. aether/physics/physics_world.py +436 -0
  39. aether/physics/raycast.py +25 -0
  40. aether/physics/rigidbody.py +7 -0
  41. aether/resources/__init__.py +0 -0
  42. aether/resources/asset_database.py +76 -0
  43. aether/resources/resource_manager.py +147 -0
  44. aether/resources/serialization.py +114 -0
  45. aether/scene/__init__.py +0 -0
  46. aether/scene/component.py +368 -0
  47. aether/scene/entity.py +46 -0
  48. aether/scene/scene.py +33 -0
  49. aether/scene/scene_graph.py +252 -0
  50. aether/scene/scene_manager.py +191 -0
  51. aether/ui/__init__.py +0 -0
  52. aether/ui/button.py +297 -0
  53. aether/ui/canvas.py +11 -0
  54. aether/ui/text.py +286 -0
  55. aether/utils/__init__.py +0 -0
  56. aether/utils/logger.py +16 -0
  57. aether/utils/math_utils.py +31 -0
  58. aether/utils/object_pool.py +137 -0
  59. aether/utils/profiler.py +187 -0
  60. aether_engine-1.0.0.dist-info/METADATA +71 -0
  61. aether_engine-1.0.0.dist-info/RECORD +64 -0
  62. aether_engine-1.0.0.dist-info/WHEEL +5 -0
  63. aether_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
  64. aether_engine-1.0.0.dist-info/top_level.txt +1 -0
aether/ui/button.py ADDED
@@ -0,0 +1,297 @@
1
+ """UI Button и интерактивные элементы"""
2
+ import numpy as np
3
+ from OpenGL.GL import *
4
+ from typing import Callable, Optional
5
+ from aether.ui.text import TextRenderer, Font
6
+
7
+
8
+ class UIElement:
9
+ """Базовый класс UI элемента"""
10
+
11
+ def __init__(self, x: float = 0, y: float = 0,
12
+ width: float = 200, height: float = 50):
13
+ self.x = x
14
+ self.y = y
15
+ self.width = width
16
+ self.height = height
17
+ self.enabled = True
18
+ self.visible = True
19
+ self.parent = None
20
+ self.children = []
21
+
22
+ # Состояние
23
+ self.is_hovered = False
24
+ self.is_pressed = False
25
+ self.is_focused = False
26
+
27
+ def update(self, dt: float, mouse_x: float, mouse_y: float,
28
+ mouse_pressed: bool, mouse_just_pressed: bool):
29
+ """Обновляет состояние элемента"""
30
+ if not self.enabled or not self.visible:
31
+ return
32
+
33
+ # Проверка наведения
34
+ self.is_hovered = self._point_inside(mouse_x, mouse_y)
35
+
36
+ # Обновляем детей
37
+ for child in self.children:
38
+ child.update(dt, mouse_x, mouse_y, mouse_pressed, mouse_just_pressed)
39
+
40
+ def render(self):
41
+ """Рендерит элемент (переопределяется в наследниках)"""
42
+ if not self.visible:
43
+ return
44
+
45
+ for child in self.children:
46
+ child.render()
47
+
48
+ def _point_inside(self, px: float, py: float) -> bool:
49
+ """Проверяет, находится ли точка внутри элемента"""
50
+ return (self.x <= px <= self.x + self.width and
51
+ self.y <= py <= self.y + self.height)
52
+
53
+ def get_absolute_position(self) -> Tuple[float, float]:
54
+ """Получает абсолютную позицию с учётом родителей"""
55
+ x, y = self.x, self.y
56
+ if self.parent:
57
+ px, py = self.parent.get_absolute_position()
58
+ x += px
59
+ y += py
60
+ return (x, y)
61
+
62
+
63
+ class Button(UIElement):
64
+ """Кнопка"""
65
+
66
+ def __init__(self, text: str = "Button", x: float = 0, y: float = 0,
67
+ width: float = 200, height: float = 50,
68
+ callback: Callable = None):
69
+ super().__init__(x, y, width, height)
70
+ self.text = text
71
+ self.callback = callback
72
+
73
+ # Визуальные настройки
74
+ self.normal_color = (0.2, 0.3, 0.5, 1.0)
75
+ self.hover_color = (0.3, 0.4, 0.6, 1.0)
76
+ self.press_color = (0.1, 0.2, 0.4, 1.0)
77
+ self.text_color = (1.0, 1.0, 1.0, 1.0)
78
+ self.border_radius = 8.0
79
+ self.font_size = 20
80
+
81
+ self._was_pressed = False
82
+
83
+ def update(self, dt: float, mouse_x: float, mouse_y: float,
84
+ mouse_pressed: bool, mouse_just_pressed: bool):
85
+ super().update(dt, mouse_x, mouse_y, mouse_pressed, mouse_just_pressed)
86
+
87
+ if not self.enabled:
88
+ return
89
+
90
+ # Обработка нажатия
91
+ if self.is_hovered:
92
+ if mouse_just_pressed:
93
+ self.is_pressed = True
94
+ self._was_pressed = True
95
+ elif mouse_pressed and self._was_pressed:
96
+ self.is_pressed = True
97
+ else:
98
+ if self._was_pressed and not mouse_pressed:
99
+ # Клик!
100
+ if self.callback:
101
+ self.callback()
102
+ self.is_pressed = False
103
+ self._was_pressed = False
104
+ else:
105
+ self.is_pressed = False
106
+ self._was_pressed = False
107
+
108
+ def render(self):
109
+ if not self.visible:
110
+ return
111
+
112
+ # Определяем цвет
113
+ if self.is_pressed:
114
+ color = self.press_color
115
+ elif self.is_hovered:
116
+ color = self.hover_color
117
+ else:
118
+ color = self.normal_color
119
+
120
+ # Рисуем фон
121
+ self._draw_rounded_rect(self.x, self.y, self.width, self.height,
122
+ self.border_radius, color)
123
+
124
+ # Рендерим текст
125
+ text_renderer = TextRenderer()
126
+ if text_renderer.default_font:
127
+ text_width, text_height = text_renderer.measure_text(
128
+ self.text, self.font_size / 48.0
129
+ )
130
+
131
+ text_x = self.x + (self.width - text_width) / 2
132
+ text_y = self.y + (self.height - text_height) / 2
133
+
134
+ text_renderer.render_text(
135
+ self.text, text_x, text_y,
136
+ self.font_size / 48.0, self.text_color
137
+ )
138
+
139
+ super().render()
140
+
141
+ def _draw_rounded_rect(self, x, y, w, h, r, color):
142
+ """Рисует закруглённый прямоугольник"""
143
+ glEnable(GL_BLEND)
144
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
145
+
146
+ # Простой прямоугольник (закругления можно добавить через шейдер)
147
+ glColor4f(*color)
148
+ glBegin(GL_QUADS)
149
+ glVertex2f(x, y)
150
+ glVertex2f(x + w, y)
151
+ glVertex2f(x + w, y + h)
152
+ glVertex2f(x, y + h)
153
+ glEnd()
154
+
155
+ glDisable(GL_BLEND)
156
+
157
+
158
+ class Panel(UIElement):
159
+ """Панель (контейнер)"""
160
+
161
+ def __init__(self, x: float = 0, y: float = 0,
162
+ width: float = 400, height: float = 300):
163
+ super().__init__(x, y, width, height)
164
+ self.background_color = (0.1, 0.1, 0.15, 0.9)
165
+ self.border_color = (0.3, 0.3, 0.4, 1.0)
166
+ self.border_width = 2.0
167
+ self.padding = 10.0
168
+ self.title = ""
169
+
170
+ def render(self):
171
+ if not self.visible:
172
+ return
173
+
174
+ # Фон
175
+ glEnable(GL_BLEND)
176
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
177
+
178
+ glColor4f(*self.background_color)
179
+ glBegin(GL_QUADS)
180
+ glVertex2f(self.x, self.y)
181
+ glVertex2f(self.x + self.width, self.y)
182
+ glVertex2f(self.x + self.width, self.y + self.height)
183
+ glVertex2f(self.x, self.y + self.height)
184
+ glEnd()
185
+
186
+ # Граница
187
+ glColor4f(*self.border_color)
188
+ glLineWidth(self.border_width)
189
+ glBegin(GL_LINE_LOOP)
190
+ glVertex2f(self.x, self.y)
191
+ glVertex2f(self.x + self.width, self.y)
192
+ glVertex2f(self.x + self.width, self.y + self.height)
193
+ glVertex2f(self.x, self.y + self.height)
194
+ glEnd()
195
+ glLineWidth(1.0)
196
+
197
+ glDisable(GL_BLEND)
198
+
199
+ # Заголовок
200
+ if self.title:
201
+ text_renderer = TextRenderer()
202
+ if text_renderer.default_font:
203
+ text_renderer.render_text(
204
+ self.title,
205
+ self.x + self.padding,
206
+ self.y + self.height - 30,
207
+ 0.5, (1.0, 1.0, 1.0, 1.0)
208
+ )
209
+
210
+ super().render()
211
+
212
+
213
+ class Label(UIElement):
214
+ """Текстовая метка"""
215
+
216
+ def __init__(self, text: str = "", x: float = 0, y: float = 0,
217
+ font_size: float = 20):
218
+ super().__init__(x, y, 0, 0)
219
+ self.text = text
220
+ self.font_size = font_size
221
+ self.color = (1.0, 1.0, 1.0, 1.0)
222
+
223
+ def render(self):
224
+ if not self.visible or not self.text:
225
+ return
226
+
227
+ text_renderer = TextRenderer()
228
+ if text_renderer.default_font:
229
+ w, h = text_renderer.render_text(
230
+ self.text, self.x, self.y,
231
+ self.font_size / 48.0, self.color
232
+ )
233
+ self.width = w
234
+ self.height = h
235
+
236
+ super().render()
237
+
238
+
239
+ class Canvas:
240
+ """Корневой Canvas для UI"""
241
+
242
+ def __init__(self, screen_width: int = 1920, screen_height: int = 1080):
243
+ self.elements = []
244
+ self.screen_width = screen_width
245
+ self.screen_height = screen_height
246
+
247
+ # Настройка ортографической проекции для UI
248
+ self._setup_projection()
249
+
250
+ def _setup_projection(self):
251
+ """Настройка проекции для UI"""
252
+ glMatrixMode(GL_PROJECTION)
253
+ glLoadIdentity()
254
+ glOrtho(0, self.screen_width, 0, self.screen_height, -1, 1)
255
+ glMatrixMode(GL_MODELVIEW)
256
+ glLoadIdentity()
257
+
258
+ def add(self, element: UIElement):
259
+ """Добавляет элемент"""
260
+ self.elements.append(element)
261
+
262
+ def remove(self, element: UIElement):
263
+ """Удаляет элемент"""
264
+ if element in self.elements:
265
+ self.elements.remove(element)
266
+
267
+ def update(self, dt: float, mouse_x: float, mouse_y: float,
268
+ mouse_pressed: bool, mouse_just_pressed: bool):
269
+ """Обновляет все элементы"""
270
+ for element in self.elements:
271
+ element.update(dt, mouse_x, mouse_y, mouse_pressed, mouse_just_pressed)
272
+
273
+ def render(self):
274
+ """Рендерит все элементы"""
275
+ # Сохраняем состояние
276
+ glPushAttrib(GL_ALL_ATTRIB_BITS)
277
+ glMatrixMode(GL_PROJECTION)
278
+ glPushMatrix()
279
+ glMatrixMode(GL_MODELVIEW)
280
+ glPushMatrix()
281
+
282
+ # Устанавливаем ортографическую проекцию
283
+ self._setup_projection()
284
+
285
+ # Отключаем тест глубины для UI
286
+ glDisable(GL_DEPTH_TEST)
287
+
288
+ for element in self.elements:
289
+ element.render()
290
+
291
+ # Восстанавливаем состояние
292
+ glEnable(GL_DEPTH_TEST)
293
+ glMatrixMode(GL_PROJECTION)
294
+ glPopMatrix()
295
+ glMatrixMode(GL_MODELVIEW)
296
+ glPopMatrix()
297
+ glPopAttrib()
aether/ui/canvas.py ADDED
@@ -0,0 +1,11 @@
1
+ """UI Canvas"""
2
+ class Canvas:
3
+ def __init__(self):
4
+ self.elements = []
5
+
6
+ def add(self, element):
7
+ self.elements.append(element)
8
+
9
+ def render(self):
10
+ for element in self.elements:
11
+ element.render()
aether/ui/text.py ADDED
@@ -0,0 +1,286 @@
1
+ """Рендеринг текста с использованием FreeType"""
2
+ import numpy as np
3
+ from OpenGL.GL import *
4
+ import os
5
+ from typing import Dict, Tuple
6
+
7
+
8
+ class Character:
9
+ """Глиф символа"""
10
+ __slots__ = ('texture_id', 'size', 'bearing', 'advance')
11
+
12
+ def __init__(self, texture_id: int, size: Tuple[int, int],
13
+ bearing: Tuple[int, int], advance: int):
14
+ self.texture_id = texture_id
15
+ self.size = size
16
+ self.bearing = bearing
17
+ self.advance = advance
18
+
19
+
20
+ class Font:
21
+ """Шрифт"""
22
+
23
+ def __init__(self, path: str, size: int = 48):
24
+ self.characters: Dict[str, Character] = {}
25
+ self.size = size
26
+ self.line_height = 0
27
+
28
+ self._load_font(path, size)
29
+
30
+ def _load_font(self, path: str, size: int):
31
+ """Загружает шрифт"""
32
+ try:
33
+ from PIL import Image, ImageDraw, ImageFont
34
+
35
+ font = ImageFont.truetype(path, size)
36
+
37
+ # Загружаем ASCII символы
38
+ for c in range(32, 127):
39
+ char = chr(c)
40
+ self._load_character(char, font)
41
+
42
+ # Добавляем кириллицу
43
+ for c in range(1040, 1104):
44
+ char = chr(c)
45
+ self._load_character(char, font)
46
+
47
+ # Дополнительные символы
48
+ for char in "—«»…№":
49
+ self._load_character(char, font)
50
+
51
+ # Высота строки
52
+ self.line_height = size + 10
53
+
54
+ print(f"[Font] Loaded: {path} ({size}px, {len(self.characters)} chars)")
55
+
56
+ except ImportError:
57
+ print("[Font] PIL not installed, using default font")
58
+ self._create_default_font()
59
+
60
+ def _load_character(self, char: str, font):
61
+ """Загружает один символ"""
62
+ from PIL import Image
63
+
64
+ # Получаем размеры символа
65
+ bbox = font.getbbox(char)
66
+ width = bbox[2] - bbox[0]
67
+ height = bbox[3] - bbox[1]
68
+
69
+ if width <= 0 or height <= 0:
70
+ width = self.size // 2
71
+ height = self.size
72
+
73
+ # Рендерим символ
74
+ image = Image.new('RGBA', (width + 4, height + 4), (0, 0, 0, 0))
75
+ draw = ImageDraw.Draw(image)
76
+ draw.text((2 - bbox[0], 2 - bbox[1]), char, font=font, fill=(255, 255, 255, 255))
77
+
78
+ # Создаём OpenGL текстуру
79
+ texture_id = glGenTextures(1)
80
+ glBindTexture(GL_TEXTURE_2D, texture_id)
81
+
82
+ image_data = np.array(image, dtype=np.uint8)
83
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height,
84
+ 0, GL_RGBA, GL_UNSIGNED_BYTE, image_data)
85
+
86
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
87
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
88
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
89
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
90
+
91
+ # Сохраняем информацию
92
+ self.characters[char] = Character(
93
+ texture_id,
94
+ (width + 4, height + 4),
95
+ (bbox[0], -bbox[3]),
96
+ width + 4
97
+ )
98
+
99
+ def _create_default_font(self):
100
+ """Создаёт простой встроенный шрифт"""
101
+ for c in range(32, 127):
102
+ char = chr(c)
103
+ texture_id = glGenTextures(1)
104
+
105
+ # Создаём простой символ 16x16
106
+ image = np.zeros((16, 16, 4), dtype=np.uint8)
107
+ image[:, :, 3] = 255
108
+
109
+ glBindTexture(GL_TEXTURE_2D, texture_id)
110
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16,
111
+ 0, GL_RGBA, GL_UNSIGNED_BYTE, image)
112
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
113
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
114
+
115
+ self.characters[char] = Character(texture_id, (16, 16), (0, -16), 8)
116
+
117
+ self.line_height = 16
118
+
119
+
120
+ class TextRenderer:
121
+ """Рендерер текста"""
122
+
123
+ _instance = None
124
+
125
+ def __new__(cls):
126
+ if cls._instance is None:
127
+ cls._instance = super().__new__(cls)
128
+ cls._instance._initialize()
129
+ return cls._instance
130
+
131
+ def _initialize(self):
132
+ """Инициализация рендерера"""
133
+ # VAO и VBO для текста
134
+ self.vao = glGenVertexArrays(1)
135
+ self.vbo = glGenBuffers(1)
136
+
137
+ glBindVertexArray(self.vao)
138
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
139
+
140
+ # Выделяем память (6 вершин * 4 floats на глиф * максимум 256 символов)
141
+ glBufferData(GL_ARRAY_BUFFER, 256 * 6 * 4 * 4, None, GL_DYNAMIC_DRAW)
142
+
143
+ # Позиция (vec2) и UV (vec2)
144
+ glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * 4, ctypes.c_void_p(0))
145
+ glEnableVertexAttribArray(0)
146
+
147
+ glBindVertexArray(0)
148
+
149
+ # Шейдер текста
150
+ self._setup_shader()
151
+
152
+ # Текущий шрифт
153
+ self.default_font = None
154
+
155
+ print("[TextRenderer] Initialized")
156
+
157
+ def _setup_shader(self):
158
+ """Настройка шейдера для текста"""
159
+ vertex_shader = """
160
+ #version 460 core
161
+ layout(location = 0) in vec4 vertex; // xy: pos, zw: uv
162
+
163
+ out vec2 TexCoords;
164
+
165
+ uniform mat4 projection;
166
+
167
+ void main() {
168
+ gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
169
+ TexCoords = vertex.zw;
170
+ }
171
+ """
172
+
173
+ fragment_shader = """
174
+ #version 460 core
175
+ in vec2 TexCoords;
176
+ out vec4 color;
177
+
178
+ uniform sampler2D text;
179
+ uniform vec4 textColor;
180
+
181
+ void main() {
182
+ vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
183
+ color = textColor * sampled;
184
+ }
185
+ """
186
+
187
+ from aether.graphics.shader import Shader
188
+ self.shader = Shader(vertex_shader, fragment_shader)
189
+
190
+ def set_default_font(self, font_path: str, size: int = 48):
191
+ """Устанавливает шрифт по умолчанию"""
192
+ self.default_font = Font(font_path, size)
193
+
194
+ def render_text(self, text: str, x: float, y: float,
195
+ scale: float = 1.0, color: tuple = (1.0, 1.0, 1.0, 1.0),
196
+ font: Font = None, screen_width: int = 1920,
197
+ screen_height: int = 1080) -> Tuple[float, float]:
198
+ """Рендерит текст и возвращает (width, height)"""
199
+ if not text:
200
+ return (0, 0)
201
+
202
+ if font is None:
203
+ font = self.default_font
204
+
205
+ if font is None:
206
+ return (0, 0)
207
+
208
+ # Ортографическая проекция
209
+ projection = np.array([
210
+ [2.0 / screen_width, 0, 0, -1],
211
+ [0, 2.0 / screen_height, 0, -1],
212
+ [0, 0, -1, 0],
213
+ [0, 0, 0, 1]
214
+ ], dtype=np.float32)
215
+
216
+ self.shader.use()
217
+ self.shader.set_mat4("projection", projection)
218
+ self.shader.set_vec4("textColor", color)
219
+
220
+ glActiveTexture(GL_TEXTURE0)
221
+ glBindVertexArray(self.vao)
222
+
223
+ total_width = 0
224
+ total_height = 0
225
+
226
+ for char in text:
227
+ if char not in font.characters:
228
+ continue
229
+
230
+ ch = font.characters[char]
231
+
232
+ xpos = x + ch.bearing[0] * scale
233
+ ypos = y - (ch.size[1] - ch.bearing[1]) * scale
234
+
235
+ w = ch.size[0] * scale
236
+ h = ch.size[1] * scale
237
+
238
+ # 6 вершин на символ (2 треугольника)
239
+ vertices = np.array([
240
+ xpos, ypos + h, 0.0, 0.0,
241
+ xpos, ypos, 0.0, 1.0,
242
+ xpos + w, ypos, 1.0, 1.0,
243
+
244
+ xpos, ypos + h, 0.0, 0.0,
245
+ xpos + w, ypos, 1.0, 1.0,
246
+ xpos + w, ypos + h, 1.0, 0.0,
247
+ ], dtype=np.float32)
248
+
249
+ # Рендерим глиф
250
+ glBindTexture(GL_TEXTURE_2D, ch.texture_id)
251
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
252
+ glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.nbytes, vertices)
253
+ glDrawArrays(GL_TRIANGLES, 0, 6)
254
+
255
+ # Сдвигаем позицию для следующего символа
256
+ x += (ch.advance >> 6) * scale
257
+
258
+ total_width += (ch.advance >> 6) * scale
259
+ total_height = max(total_height, ch.size[1] * scale)
260
+
261
+ glBindVertexArray(0)
262
+ glBindTexture(GL_TEXTURE_2D, 0)
263
+
264
+ return (total_width, total_height)
265
+
266
+ def measure_text(self, text: str, scale: float = 1.0, font: Font = None) -> Tuple[float, float]:
267
+ """Измеряет размер текста без рендеринга"""
268
+ if not text or font is None:
269
+ return (0, 0)
270
+
271
+ if font is None:
272
+ font = self.default_font
273
+
274
+ if font is None:
275
+ return (0, 0)
276
+
277
+ width = 0
278
+ height = 0
279
+
280
+ for char in text:
281
+ if char in font.characters:
282
+ ch = font.characters[char]
283
+ width += (ch.advance >> 6) * scale
284
+ height = max(height, ch.size[1] * scale)
285
+
286
+ return (width, height)
File without changes
aether/utils/logger.py ADDED
@@ -0,0 +1,16 @@
1
+ """Логирование"""
2
+ import logging
3
+
4
+
5
+ def setup_logger(name="Aether", level=logging.INFO):
6
+ logger = logging.getLogger(name)
7
+ logger.setLevel(level)
8
+
9
+ handler = logging.StreamHandler()
10
+ handler.setFormatter(logging.Formatter(
11
+ '[%(asctime)s] [%(levelname)s] %(message)s',
12
+ datefmt='%H:%M:%S'
13
+ ))
14
+ logger.addHandler(handler)
15
+
16
+ return logger
@@ -0,0 +1,31 @@
1
+ """Математические утилиты"""
2
+ import numpy as np
3
+
4
+
5
+ def lerp(a: float, b: float, t: float) -> float:
6
+ return a + (b - a) * t
7
+
8
+
9
+ def clamp(value: float, min_val: float, max_val: float) -> float:
10
+ return max(min_val, min(max_val, value))
11
+
12
+
13
+ def normalize(v: np.ndarray) -> np.ndarray:
14
+ norm = np.linalg.norm(v)
15
+ return v / norm if norm > 0 else v
16
+
17
+
18
+ def look_at(eye: np.ndarray, target: np.ndarray, up: np.ndarray) -> np.ndarray:
19
+ """Создаёт look-at матрицу"""
20
+ f = target - eye
21
+ f = f / np.linalg.norm(f)
22
+ s = np.cross(f, up)
23
+ s = s / np.linalg.norm(s)
24
+ u = np.cross(s, f)
25
+
26
+ m = np.identity(4, dtype=np.float32)
27
+ m[0, :3] = s
28
+ m[1, :3] = u
29
+ m[2, :3] = -f
30
+ m[3, :3] = eye
31
+ return np.linalg.inv(m)