ncca-ngl 0.1.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.
@@ -0,0 +1,65 @@
1
+ import numpy as np
2
+ import OpenGL.GL as gl
3
+
4
+ from .abstract_vao import AbstractVAO, VertexData
5
+ from .log import logger
6
+
7
+
8
+ class IndexVertexData(VertexData):
9
+ def __init__(self, data, size, indices, index_type, mode=gl.GL_STATIC_DRAW):
10
+ super().__init__(data, size, mode)
11
+ gl.GL_to_numpy_type = {
12
+ gl.GL_UNSIGNED_INT: np.uint32,
13
+ gl.GL_UNSIGNED_SHORT: np.uint16,
14
+ gl.GL_UNSIGNED_BYTE: np.uint8,
15
+ }
16
+ numpy_dtype = gl.GL_to_numpy_type.get(index_type)
17
+ if numpy_dtype is None:
18
+ logger.error("SimpleIndexVAO: Unsupported index type")
19
+ raise TypeError(f"Unsupported index type: {index_type}")
20
+
21
+ self.indices = np.array(indices, dtype=numpy_dtype)
22
+ self.index_type = index_type
23
+
24
+
25
+ class SimpleIndexVAO(AbstractVAO):
26
+ def __init__(self, mode=gl.GL_TRIANGLES):
27
+ super().__init__(mode)
28
+ self.buffer = gl.glGenBuffers(1)
29
+ self.idx_buffer = gl.glGenBuffers(1)
30
+ self.index_type = gl.GL_UNSIGNED_INT
31
+
32
+ def draw(self):
33
+ if self.bound and self.allocated:
34
+ gl.glDrawElements(self.mode, self.indices_count, self.index_type, None)
35
+ else:
36
+ logger.error("SimpleIndexVAO not bound or not allocated")
37
+
38
+ def set_data(self, data):
39
+ if not isinstance(data, IndexVertexData):
40
+ logger.error("SimpleIndexVAO: Unsupported index type")
41
+ raise TypeError("data must be of type IndexVertexData")
42
+
43
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer)
44
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, data.data.nbytes, data.data, data.mode)
45
+
46
+ gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.idx_buffer)
47
+ gl.glBufferData(
48
+ gl.GL_ELEMENT_ARRAY_BUFFER, data.indices.nbytes, data.indices, data.mode
49
+ )
50
+
51
+ self.allocated = True
52
+ self.indices_count = len(data.indices)
53
+ self.index_type = data.index_type
54
+
55
+ def remove_vao(self):
56
+ gl.glDeleteBuffers(1, [self.buffer])
57
+ gl.glDeleteBuffers(1, [self.idx_buffer])
58
+ gl.glDeleteVertexArrays(1, [self.id])
59
+
60
+ def get_buffer_id(self, index=0):
61
+ return self.buffer
62
+
63
+ def map_buffer(self, index=0, access_mode=gl.GL_READ_WRITE):
64
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer)
65
+ return gl.glMapBuffer(gl.GL_ARRAY_BUFFER, access_mode)
ncca/ngl/simple_vao.py ADDED
@@ -0,0 +1,42 @@
1
+ import OpenGL.GL as gl
2
+
3
+ from .abstract_vao import AbstractVAO, VertexData
4
+ from .log import logger
5
+
6
+
7
+ class SimpleVAO(AbstractVAO):
8
+ def __init__(self, mode=gl.GL_TRIANGLES):
9
+ super().__init__(mode)
10
+ self.buffer = gl.glGenBuffers(1)
11
+
12
+ def draw(self):
13
+ if self.bound and self.allocated:
14
+ gl.glDrawArrays(self.mode, 0, self.indices_count)
15
+ else:
16
+ logger.error("SimpleVAO not bound or not allocated")
17
+
18
+ def set_data(self, data):
19
+ if not isinstance(data, VertexData):
20
+ logger.error("SimpleVAO: Invalid data type")
21
+ raise TypeError("data must be of type VertexData")
22
+ if not self.bound:
23
+ logger.error("SimpleVAO not bound")
24
+ raise RuntimeError("SimpleVAO not bound")
25
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer)
26
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, data.data.nbytes, data.data, data.mode)
27
+ self.allocated = True
28
+ self.indices_count = data.size
29
+
30
+ def num_indices(self):
31
+ return self.indices_count
32
+
33
+ def remove_vao(self):
34
+ gl.glDeleteBuffers(1, [self.buffer])
35
+ gl.glDeleteVertexArrays(1, [self.id])
36
+
37
+ def get_buffer_id(self, index=0):
38
+ return self.buffer
39
+
40
+ def map_buffer(self, index=0, access_mode=gl.GL_READ_WRITE):
41
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer)
42
+ return gl.glMapBuffer(gl.GL_ARRAY_BUFFER, access_mode)
ncca/ngl/text.py ADDED
@@ -0,0 +1,346 @@
1
+ """
2
+ A module for rendering text in OpenGL using pre-rendered font atlases.
3
+
4
+ This implementation uses the 'freetype-py' library to rasterize characters
5
+ from a given font file into a single texture atlas. This atlas is then
6
+ used by a set of shaders (vertex, geometry, and fragment) to render
7
+ text efficiently.
8
+
9
+ The process is as follows:
10
+ 1. FontAtlas class:
11
+ - Loads a font file.
12
+ - Renders glyphs for a range of characters (ASCII ' ' to '~').
13
+ - Packs these glyphs into a single large texture atlas in memory.
14
+ - Calculates and stores metadata for each glyph (size, bearing,
15
+ advance, and UV coordinates within the atlas).
16
+ - Generates a single OpenGL texture for the atlas.
17
+
18
+ 2. _Text class (exported as Text):
19
+ - Manages multiple fonts by creating and storing FontAtlas objects.
20
+ - Provides a `render_dynamic_text` method to draw text strings.
21
+ - `_build_instances`: For a given string, this method generates a
22
+ list of vertex attributes for each character. Each character is
23
+ represented as a single point with attributes for position, UVs,
24
+ and size.
25
+ - `render_dynamic_text`: This method sends the generated instance
26
+ data to the GPU and draws it using GL_POINTS.
27
+
28
+ 3. Shaders:
29
+ - Vertex Shader: A simple pass-through shader that sends point
30
+ data to the geometry shader.
31
+ - Geometry Shader: Receives points and generates a textured quad
32
+ for each character on the fly.
33
+ - Fragment Shader: Samples the font atlas texture to color the
34
+ quad, effectively drawing the character.
35
+ """
36
+
37
+ from typing import Any, Dict, List
38
+
39
+ import freetype
40
+ import numpy as np
41
+ import OpenGL.GL as gl
42
+
43
+ from .log import logger
44
+ from .shader_lib import DefaultShader, ShaderLib
45
+ from .simple_vao import VertexData
46
+ from .vao_factory import VAOFactory, VAOType
47
+ from .vec3 import Vec3
48
+
49
+
50
+ class FontAtlas:
51
+ """
52
+ Manages the creation of a font texture atlas for efficient text rendering.
53
+
54
+ This class uses FreeType to render glyphs for a specified font and packs them
55
+ into a single texture. It also stores metadata for each glyph.
56
+ """
57
+
58
+ def __init__(self, font_path: str, font_size: int = 48, debug: bool = False):
59
+ """
60
+ Initializes the FontAtlas.
61
+
62
+ Args:
63
+ font_path: The file path to the font (e.g., a .ttf file).
64
+ font_size: The font size in pixels to be used for rendering the atlas.
65
+ debug: If True, saves the generated atlas as a PNG for debugging.
66
+ """
67
+ try:
68
+ self.face = freetype.Face(font_path)
69
+ self.face.set_pixel_sizes(0, font_size)
70
+ self.font_size: int = font_size
71
+ self.glyphs: Dict[str, Dict[str, Any]] = {}
72
+ self.texture: int = 0
73
+ self.atlas_w: int = 0
74
+ self.atlas_h: int = 0
75
+ self.atlas: np.ndarray | None = None
76
+ self.build_atlas(debug)
77
+
78
+ except freetype.FT_Exception as e:
79
+ logger.error(f"{font_path} could not be loaded {e}")
80
+
81
+ def __str__(self) -> str:
82
+ """Returns a string representation of the FontAtlas."""
83
+ return f"TextureID: {self.texture}, FontSize: {self.font_size}"
84
+
85
+ def build_atlas(self, debug: bool = False) -> None:
86
+ """
87
+ Renders characters and packs them into a texture atlas.
88
+
89
+ This method iterates through ASCII characters 32-126, renders each one
90
+ using FreeType, and arranges them in a single large numpy array which
91
+ will later be used to create an OpenGL texture.
92
+
93
+ Args:
94
+ debug: If True, saves the generated atlas as 'debug_atlas.png'.
95
+ """
96
+ padding = 2 # Padding between glyphs in the atlas
97
+ atlas_w = 1024 # Fixed width for the atlas texture
98
+ x, y, row_h = 0, 0, 0
99
+ bitmaps_data = []
100
+
101
+ # Iterate through printable ASCII characters
102
+ for charcode in range(ord(" "), ord("~")):
103
+ self.face.load_char(chr(charcode), freetype.FT_LOAD_RENDER)
104
+ bmp = self.face.glyph.bitmap
105
+ w, h = bmp.width, bmp.rows
106
+
107
+ # Move to the next row if the current glyph doesn't fit
108
+ if x + w + padding > atlas_w:
109
+ x = 0
110
+ y += row_h + padding
111
+ row_h = 0
112
+
113
+ # Copy bitmap data as the buffer is overwritten for each glyph
114
+ if w > 0 and h > 0:
115
+ buffer_copy = np.array(bmp.buffer, dtype=np.ubyte).reshape(h, w)
116
+ bitmaps_data.append((buffer_copy, x, y))
117
+
118
+ # Store glyph metadata
119
+ self.glyphs[chr(charcode)] = {
120
+ "size": (w, h),
121
+ "bearing": (self.face.glyph.bitmap_left, self.face.glyph.bitmap_top),
122
+ "advance": self.face.glyph.advance.x >> 6, # Advance is in 1/64 pixels
123
+ "uv": (x, y, x + w, y + h), # UVs in pixel coordinates
124
+ }
125
+ x += w + padding
126
+ row_h = max(row_h, h)
127
+
128
+ atlas_h = y + row_h + padding
129
+ self.atlas_w, self.atlas_h = atlas_w, atlas_h
130
+ atlas = np.zeros((atlas_h, atlas_w), dtype=np.ubyte)
131
+
132
+ # Blit all the individual glyph bitmaps onto the atlas
133
+ for arr, dest_x, dest_y in bitmaps_data:
134
+ h, w = arr.shape
135
+ atlas[dest_y : dest_y + h, dest_x : dest_x + w] = arr
136
+
137
+ self.atlas = atlas
138
+ if debug:
139
+ from PIL import Image
140
+
141
+ img = Image.fromarray(self.atlas, mode="L")
142
+ img.save("debug_atlas.png")
143
+ print(f"Saved debug_atlas.png, size: {self.atlas.shape}")
144
+
145
+ def generate_texture(self) -> None:
146
+ """Generates and configures the OpenGL texture for the font atlas."""
147
+ if self.atlas is None:
148
+ return
149
+ tex = gl.glGenTextures(1)
150
+ gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
151
+
152
+ gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
153
+ # Create a single-channel RED texture from our numpy atlas
154
+ gl.glTexImage2D(
155
+ gl.GL_TEXTURE_2D,
156
+ 0,
157
+ gl.GL_RED,
158
+ self.atlas_w,
159
+ self.atlas_h,
160
+ 0,
161
+ gl.GL_RED,
162
+ gl.GL_UNSIGNED_BYTE,
163
+ self.atlas,
164
+ )
165
+
166
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
167
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
168
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
169
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
170
+
171
+ # Use texture swizzling to use the RED channel as ALPHA.
172
+ # This allows us to color the font using a uniform in the shader,
173
+ # while using the glyph's intensity for transparency.
174
+ # We set the texture's RGB channels to 1.0, and the A channel to the
175
+ # value from the RED channel of the source.
176
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_SWIZZLE_R, gl.GL_ONE)
177
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_SWIZZLE_G, gl.GL_ONE)
178
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_SWIZZLE_B, gl.GL_ONE)
179
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_SWIZZLE_A, gl.GL_RED)
180
+
181
+ self.texture = tex
182
+
183
+
184
+ class _Text:
185
+ """
186
+ Main class for managing and rendering text.
187
+
188
+ This class acts as a controller, loading fonts and providing methods
189
+ to render text strings to the screen. It is designed to be used as a
190
+ singleton instance.
191
+ """
192
+
193
+ def __init__(self) -> None:
194
+ """Initializes the Text renderer."""
195
+ self._fonts: Dict[str, FontAtlas] = {}
196
+ self._static_text: List[Any] = [] # Reserved for future use
197
+
198
+ def add_font(self, name: str, font_file: str, size: int) -> None:
199
+ """
200
+ Loads a font and makes it available for rendering.
201
+
202
+ Args:
203
+ name: A unique name to identify this font (e.g., "main_font").
204
+ font_file: The path to the font file.
205
+ size: The font size in pixels.
206
+ """
207
+ if not hasattr(self, "vao"):
208
+ self.vao = VAOFactory.create_vao(VAOType.SIMPLE, gl.GL_POINTS)
209
+ font = FontAtlas(font_file, size)
210
+ font.generate_texture()
211
+ print(f"Font '{name}' added with texture ID: {font.texture}")
212
+ self._fonts[name] = font
213
+
214
+ def set_screen_size(self, w: int, h: int) -> None:
215
+ """
216
+ Sets the screen dimensions for the text shader.
217
+
218
+ This should be called whenever the window is resized.
219
+
220
+ Args:
221
+ w: The width of the screen in pixels.
222
+ h: The height of the screen in pixels.
223
+ """
224
+ ShaderLib.use(DefaultShader.TEXT)
225
+ ShaderLib.set_uniform("textureID", 0)
226
+ ShaderLib.set_uniform("screenSize", w, h)
227
+ ShaderLib.set_uniform("fontSize", 1.0)
228
+ ShaderLib.set_uniform("textColor", 1.0, 1.0, 1.0, 1.0)
229
+
230
+ def render_text(
231
+ self, font: str, x: int, y: int, text: str, colour: Vec3 = Vec3(1, 1, 1)
232
+ ) -> None:
233
+ """
234
+ Renders a string of text to the screen.
235
+
236
+ Args:
237
+ font: The name of the font to use (previously added with add_font).
238
+ x: The x-coordinate of the starting position (baseline).
239
+ y: The y-coordinate of the starting position (baseline).
240
+ text: The string of text to render.
241
+ colour: The color of the text as a Vec4.
242
+ """
243
+ render_data = self._build_instances(font, text, x, y)
244
+ if not render_data:
245
+ return
246
+
247
+ buffer_data = np.array(render_data, dtype=np.float32)
248
+ atlas = self._fonts[font]
249
+
250
+ # Enable blending for transparency
251
+ gl.glEnable(gl.GL_BLEND)
252
+ gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
253
+
254
+ # Ensure text is rendered filled and restore state afterwards.
255
+ polygon_mode = gl.glGetIntegerv(gl.GL_POLYGON_MODE)[0]
256
+ if polygon_mode != gl.GL_FILL:
257
+ gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
258
+
259
+ # Disable depth testing to ensure text is always drawn on top
260
+ depth_test_enabled = gl.glIsEnabled(gl.GL_DEPTH_TEST)
261
+ if depth_test_enabled:
262
+ gl.glDisable(gl.GL_DEPTH_TEST)
263
+
264
+ with self.vao as vao:
265
+ data = VertexData(data=buffer_data, size=buffer_data.nbytes)
266
+ stride = 32 # 8 floats * 4 bytes
267
+ vao.set_data(data)
268
+ # Vertex Attributes:
269
+ # 0: vec2 a_position (screen position of the glyph)
270
+ # 1: vec4 a_uvRect (u0, v0, u1, v1)
271
+ # 2: vec2 a_size (width, height of the glyph quad)
272
+ vao.set_vertex_attribute_pointer(0, 2, gl.GL_FLOAT, stride, 0)
273
+ vao.set_vertex_attribute_pointer(1, 4, gl.GL_FLOAT, stride, 8)
274
+ vao.set_vertex_attribute_pointer(2, 2, gl.GL_FLOAT, stride, 24)
275
+
276
+ gl.glActiveTexture(gl.GL_TEXTURE0)
277
+ gl.glBindTexture(gl.GL_TEXTURE_2D, atlas.texture)
278
+ ShaderLib.use(DefaultShader.TEXT)
279
+ ShaderLib.set_uniform("textColor", colour.x, colour.y, colour.z, 1.0)
280
+ # We are drawing one point per character
281
+ vao.set_num_indices(len(render_data) // 8)
282
+ vao.draw()
283
+
284
+ # Restore OpenGL state
285
+ gl.glDisable(gl.GL_BLEND)
286
+ if depth_test_enabled:
287
+ gl.glEnable(gl.GL_DEPTH_TEST)
288
+ if polygon_mode != gl.GL_FILL:
289
+ gl.glPolygonMode(gl.GL_FRONT_AND_BACK, polygon_mode)
290
+
291
+ def _build_instances(
292
+ self, font: str, text: str, start_x: int, start_y: int
293
+ ) -> List[float]:
294
+ """
295
+ Generates vertex attribute data for each character in a string.
296
+
297
+ This data is sent to the GPU as a single buffer. The geometry shader
298
+ then uses this data to construct a quad for each character.
299
+
300
+ Args:
301
+ font: The name of the font to use.
302
+ text: The string to process.
303
+ start_x: The initial x-coordinate for the text baseline.
304
+ start_y: The initial y-coordinate for the text baseline.
305
+
306
+ Returns:
307
+ A list of floats representing the packed vertex data for all characters.
308
+ """
309
+ inst = []
310
+ atlas = self._fonts.get(font)
311
+ if atlas:
312
+ x, y = float(start_x), float(start_y) # Use floats for positioning
313
+
314
+ for ch in text:
315
+ if ch not in atlas.glyphs:
316
+ continue
317
+ g = atlas.glyphs[ch]
318
+ w, h = g["size"]
319
+ adv = g["advance"]
320
+ bearing_x, bearing_y = g["bearing"]
321
+
322
+ # UV coordinates from atlas (in pixels)
323
+ u0_px, v0_px, u1_px, v1_px = g["uv"]
324
+
325
+ # Normalize UVs to the range [0, 1]
326
+ u0 = u0_px / atlas.atlas_w
327
+ v0 = v0_px / atlas.atlas_h
328
+ u1 = u1_px / atlas.atlas_w
329
+ v1 = v1_px / atlas.atlas_h
330
+
331
+ # Calculate the screen position for the top-left corner of the quad.
332
+ # FreeType's origin is at the baseline, with +y going up.
333
+ # Screen coordinates usually have +y going down, so we adjust.
334
+ pos_x = x + bearing_x
335
+ pos_y = y - bearing_y
336
+
337
+ # Each character is defined by 8 floats:
338
+ # pos_x, pos_y, u0, v0, u1, v1, w, h
339
+ inst.extend([pos_x, pos_y, u0, v0, u1, v1, float(w), float(h)])
340
+ # Advance the cursor for the next character
341
+ x += adv
342
+ return inst
343
+
344
+
345
+ # Create a singleton instance of the Text class for global use.
346
+ Text = _Text()
ncca/ngl/texture.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import OpenGL.GL as gl
4
+
5
+ from .image import Image
6
+
7
+
8
+ class Texture:
9
+ """A texture class to load and create OpenGL textures."""
10
+
11
+ def __init__(self, filename: str = None) -> None:
12
+ self._image = Image(filename)
13
+ self._texture_id = 0
14
+ self._multi_texture_id = 0
15
+
16
+ @property
17
+ def width(self) -> int:
18
+ return self._image.width
19
+
20
+ @property
21
+ def height(self) -> int:
22
+ return self._image.height
23
+
24
+ @property
25
+ def format(self) -> int:
26
+ if self._image.mode:
27
+ if self._image.mode.value == "RGB":
28
+ return gl.GL_RGB
29
+ elif self._image.mode.value == "RGBA":
30
+ return gl.GL_RGBA
31
+ elif self._image.mode.value == "L":
32
+ return gl.GL_RED
33
+ return 0
34
+
35
+ @property
36
+ def internal_format(self) -> int:
37
+ if self._image.mode:
38
+ if self._image.mode.value == "RGB":
39
+ return gl.GL_RGB8
40
+ elif self._image.mode.value == "RGBA":
41
+ return gl.GL_RGBA8
42
+ elif self._image.mode.value == "L":
43
+ return gl.GL_R8
44
+ return 0
45
+
46
+ def load_image(self, filename: str) -> bool:
47
+ return self._image.load(filename)
48
+
49
+ def get_pixels(self) -> bytes:
50
+ return self._image.get_pixels().tobytes()
51
+
52
+ def set_texture_gl(self) -> int:
53
+ if self._image.width > 0 and self._image.height > 0:
54
+ self._texture_id = gl.glGenTextures(1)
55
+ gl.glActiveTexture(gl.GL_TEXTURE0 + self._multi_texture_id)
56
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self._texture_id)
57
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
58
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
59
+ gl.glTexImage2D(
60
+ gl.GL_TEXTURE_2D,
61
+ 0,
62
+ self.internal_format,
63
+ self.width,
64
+ self.height,
65
+ 0,
66
+ self.format,
67
+ gl.GL_UNSIGNED_BYTE,
68
+ self.get_pixels(),
69
+ )
70
+ gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
71
+ return self._texture_id
72
+ return 0
73
+
74
+ def set_multi_texture(self, id: int) -> None:
75
+ self._multi_texture_id = id
ncca/ngl/transform.py ADDED
@@ -0,0 +1,95 @@
1
+ """
2
+ Class to represent a transform using translate, rotate and scale,
3
+ """
4
+
5
+ from .mat4 import Mat4
6
+ from .vec3 import Vec3
7
+
8
+
9
+ class TransformRotationOrder(Exception):
10
+ pass
11
+
12
+
13
+ class Transform:
14
+ rot_order = {
15
+ "xyz": "rz@ry@rx",
16
+ "yzx": "rx@rz@ry",
17
+ "zxy": "ry@rx@rz",
18
+ "xzy": "ry@rz@rx",
19
+ "yxz": "rz@rx@ry",
20
+ "zyx": "rx@ry@rz",
21
+ }
22
+
23
+ def __init__(self):
24
+ self.position = Vec3(0.0, 0.0, 0.0)
25
+ self.rotation = Vec3(0.0, 0.0, 0.0)
26
+ self.scale = Vec3(1.0, 1.0, 1.0)
27
+ self.matrix = Mat4()
28
+ self.need_recalc = True
29
+ self.order = "xyz"
30
+
31
+ def _set_value(self, args):
32
+ v = Vec3()
33
+ self.need_recalc = True
34
+ if len(args) == 1: # one argument
35
+ if isinstance(args[0], (list, tuple)):
36
+ v.x = args[0][0]
37
+ v.y = args[0][1]
38
+ v.z = args[0][2]
39
+ else: # try vec types
40
+ v.x = args[0].x
41
+ v.y = args[0].y
42
+ v.z = args[0].z
43
+ return v
44
+ elif len(args) == 3: # 3 as x,y,z
45
+ v.x = float(args[0])
46
+ v.y = float(args[1])
47
+ v.z = float(args[2])
48
+ return v
49
+ else:
50
+ raise ValueError
51
+
52
+ def reset(self):
53
+ self.position = Vec3()
54
+ self.rotation = Vec3()
55
+ self.scale = Vec3(1, 1, 1)
56
+ self.order = "xyz"
57
+ self.need_recalc = True
58
+
59
+ def set_position(self, *args):
60
+ "set position attrib using either x,y,z or vec types"
61
+ self.position = self._set_value(args)
62
+
63
+ def set_rotation(self, *args):
64
+ "set rotation attrib using either x,y,z or vec types"
65
+ self.rotation = self._set_value(args)
66
+
67
+ def set_scale(self, *args):
68
+ "set scale attrib using either x,y,z or vec types"
69
+ self.scale = self._set_value(args)
70
+
71
+ def set_order(self, order):
72
+ "set rotation order from string e.g xyz or zyx"
73
+ if order not in self.rot_order:
74
+ raise TransformRotationOrder
75
+ self.order = order
76
+ self.need_recalc = True
77
+
78
+ def get_matrix(self):
79
+ "return a transform matrix based on rotation order"
80
+ if self.need_recalc is True:
81
+ scale = Mat4.scale(self.scale.x, self.scale.y, self.scale.z)
82
+ rx = Mat4.rotate_x(self.rotation.x) # noqa: F841
83
+ ry = Mat4.rotate_y(self.rotation.y) # noqa: F841
84
+ rz = Mat4.rotate_z(self.rotation.z) # noqa: F841
85
+ rotation_scale = eval(self.rot_order.get(self.order)) @ scale
86
+ self.matrix = rotation_scale
87
+ self.matrix.m[3][0] = self.position.x
88
+ self.matrix.m[3][1] = self.position.y
89
+ self.matrix.m[3][2] = self.position.z
90
+ self.matrix.m[3][3] = 1.0
91
+ self.need_recalc = False
92
+ return self.matrix
93
+
94
+ def __str__(self):
95
+ return f"pos {self.position}\nrot {self.rotation}\nscale {self.scale}"