ncca-ngl 0.1.1__py3-none-any.whl → 0.1.4__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.
- ncca/ngl/.ruff_cache/.gitignore +2 -0
- ncca/ngl/.ruff_cache/0.13.0/10564494386971134025 +0 -0
- ncca/ngl/.ruff_cache/0.13.0/7783445477288392980 +0 -0
- ncca/ngl/.ruff_cache/CACHEDIR.TAG +1 -0
- ncca/ngl/PrimData/Primitives.npz +0 -0
- ncca/ngl/PrimData/buddah.npy +0 -0
- ncca/ngl/PrimData/bunny.npy +0 -0
- ncca/ngl/PrimData/cube.npy +0 -0
- ncca/ngl/PrimData/dodecahedron.npy +0 -0
- ncca/ngl/PrimData/dragon.npy +0 -0
- ncca/ngl/PrimData/football.npy +0 -0
- ncca/ngl/PrimData/icosahedron.npy +0 -0
- ncca/ngl/PrimData/octahedron.npy +0 -0
- ncca/ngl/PrimData/pack_arrays.py +20 -0
- ncca/ngl/PrimData/teapot.npy +0 -0
- ncca/ngl/PrimData/tetrahedron.npy +0 -0
- ncca/ngl/PrimData/troll.npy +0 -0
- ncca/ngl/__init__.py +100 -0
- ncca/ngl/abstract_vao.py +85 -0
- ncca/ngl/base_mesh.py +170 -0
- ncca/ngl/base_mesh.pyi +11 -0
- ncca/ngl/bbox.py +224 -0
- ncca/ngl/bezier_curve.py +75 -0
- ncca/ngl/first_person_camera.py +174 -0
- ncca/ngl/image.py +94 -0
- ncca/ngl/log.py +44 -0
- ncca/ngl/mat2.py +128 -0
- ncca/ngl/mat3.py +466 -0
- ncca/ngl/mat4.py +456 -0
- ncca/ngl/multi_buffer_vao.py +49 -0
- ncca/ngl/obj.py +416 -0
- ncca/ngl/plane.py +47 -0
- ncca/ngl/primitives.py +706 -0
- ncca/ngl/pyside_event_handling_mixin.py +318 -0
- ncca/ngl/quaternion.py +112 -0
- ncca/ngl/random.py +167 -0
- ncca/ngl/shader.py +229 -0
- ncca/ngl/shader_lib.py +536 -0
- ncca/ngl/shader_program.py +785 -0
- ncca/ngl/shaders/checker_fragment.glsl +35 -0
- ncca/ngl/shaders/checker_vertex.glsl +19 -0
- ncca/ngl/shaders/colour_fragment.glsl +8 -0
- ncca/ngl/shaders/colour_vertex.glsl +11 -0
- ncca/ngl/shaders/diffuse_fragment.glsl +21 -0
- ncca/ngl/shaders/diffuse_vertex.glsl +24 -0
- ncca/ngl/shaders/text_fragment.glsl +10 -0
- ncca/ngl/shaders/text_geometry.glsl +53 -0
- ncca/ngl/shaders/text_vertex.glsl +18 -0
- ncca/ngl/simple_index_vao.py +65 -0
- ncca/ngl/simple_vao.py +42 -0
- ncca/ngl/text.py +342 -0
- ncca/ngl/texture.py +75 -0
- ncca/ngl/transform.py +95 -0
- ncca/ngl/util.py +128 -0
- ncca/ngl/vao_factory.py +34 -0
- ncca/ngl/vec2.py +350 -0
- ncca/ngl/vec2_array.py +124 -0
- ncca/ngl/vec3.py +401 -0
- ncca/ngl/vec3_array.py +128 -0
- ncca/ngl/vec4.py +229 -0
- ncca/ngl/vec4_array.py +124 -0
- ncca_ngl-0.1.4.dist-info/METADATA +22 -0
- ncca_ngl-0.1.4.dist-info/RECORD +64 -0
- ncca_ngl-0.1.4.dist-info/WHEEL +4 -0
- ncca_ngl-0.1.1.dist-info/METADATA +0 -23
- ncca_ngl-0.1.1.dist-info/RECORD +0 -4
- ncca_ngl-0.1.1.dist-info/WHEEL +0 -4
- ncca_ngl-0.1.1.dist-info/licenses/LICENSE.txt +0 -7
ncca/ngl/bezier_curve.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from .vec3 import Vec3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BezierCurve:
|
|
5
|
+
"""A Bezier curve class."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self, control_points: list[Vec3] = None, knots: list[float] = None
|
|
9
|
+
) -> None:
|
|
10
|
+
self._cp = control_points if control_points is not None else []
|
|
11
|
+
self._knots = knots if knots is not None else []
|
|
12
|
+
self._degree = 0
|
|
13
|
+
self._order = 0
|
|
14
|
+
self._num_cp = 0
|
|
15
|
+
self._num_knots = 0
|
|
16
|
+
if self._cp:
|
|
17
|
+
self._num_cp = len(self._cp)
|
|
18
|
+
self._degree = self._num_cp
|
|
19
|
+
self._order = self._degree + 1
|
|
20
|
+
if not self._knots:
|
|
21
|
+
self.create_knots()
|
|
22
|
+
self._num_knots = len(self._knots)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def control_points(self) -> list[Vec3]:
|
|
26
|
+
return self._cp
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def knots(self) -> list[float]:
|
|
30
|
+
return self._knots
|
|
31
|
+
|
|
32
|
+
def add_point(self, x: float | Vec3, y: float = None, z: float = None) -> None:
|
|
33
|
+
if isinstance(x, Vec3):
|
|
34
|
+
self._cp.append(x)
|
|
35
|
+
else:
|
|
36
|
+
self._cp.append(Vec3(x, y, z))
|
|
37
|
+
self._num_cp += 1
|
|
38
|
+
self._degree = self._num_cp
|
|
39
|
+
self._order = self._degree + 1
|
|
40
|
+
self.create_knots()
|
|
41
|
+
|
|
42
|
+
def add_knot(self, k: float) -> None:
|
|
43
|
+
self._knots.append(k)
|
|
44
|
+
self._num_knots = len(self._knots)
|
|
45
|
+
|
|
46
|
+
def create_knots(self) -> None:
|
|
47
|
+
self._num_knots = self._num_cp + self._order
|
|
48
|
+
self._knots = [0.0] * (self._num_knots // 2) + [1.0] * (
|
|
49
|
+
self._num_knots - (self._num_knots // 2)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def get_point_on_curve(self, u: float) -> Vec3:
|
|
53
|
+
p = Vec3()
|
|
54
|
+
for i in range(self._num_cp):
|
|
55
|
+
val = self.cox_de_boor(u, i, self._degree, self._knots)
|
|
56
|
+
if val > 0.001:
|
|
57
|
+
p += self._cp[i] * val
|
|
58
|
+
return p
|
|
59
|
+
|
|
60
|
+
def cox_de_boor(self, u: float, i: int, k: int, knots: list[float]) -> float:
|
|
61
|
+
if k == 1:
|
|
62
|
+
return 1.0 if knots[i] <= u <= knots[i + 1] else 0.0
|
|
63
|
+
|
|
64
|
+
den1 = knots[i + k - 1] - knots[i]
|
|
65
|
+
den2 = knots[i + k] - knots[i + 1]
|
|
66
|
+
|
|
67
|
+
eq1 = 0.0
|
|
68
|
+
if den1 > 0:
|
|
69
|
+
eq1 = ((u - knots[i]) / den1) * self.cox_de_boor(u, i, k - 1, knots)
|
|
70
|
+
|
|
71
|
+
eq2 = 0.0
|
|
72
|
+
if den2 > 0:
|
|
73
|
+
eq2 = ((knots[i + k] - u) / den2) * self.cox_de_boor(u, i + 1, k - 1, knots)
|
|
74
|
+
|
|
75
|
+
return eq1 + eq2
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from .mat4 import Mat4
|
|
4
|
+
from .util import perspective
|
|
5
|
+
from .vec3 import Vec3
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FirstPersonCamera:
|
|
9
|
+
"""
|
|
10
|
+
A class representing a first-person camera.
|
|
11
|
+
|
|
12
|
+
This class provides functionality for a first-person camera, including movement,
|
|
13
|
+
rotation, and projection matrix calculation.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
eye (Vec3): The position of the camera.
|
|
17
|
+
look (Vec3): The point the camera is looking at.
|
|
18
|
+
world_up (Vec3): The world's up vector.
|
|
19
|
+
front (Vec3): The front direction vector of the camera.
|
|
20
|
+
up (Vec3): The up direction vector of the camera.
|
|
21
|
+
right (Vec3): The right direction vector of the camera.
|
|
22
|
+
yaw (float): The yaw angle of the camera.
|
|
23
|
+
pitch (float): The pitch angle of the camera.
|
|
24
|
+
speed (float): The movement speed of the camera.
|
|
25
|
+
sensitivity (float): The mouse sensitivity.
|
|
26
|
+
zoom (float): The zoom level of the camera.
|
|
27
|
+
near (float): The near clipping plane.
|
|
28
|
+
far (float): The far clipping plane.
|
|
29
|
+
aspect (float): The aspect ratio.
|
|
30
|
+
fov (float): The field of view.
|
|
31
|
+
projection (Mat4): The projection matrix.
|
|
32
|
+
view (Mat4): The view matrix.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, eye: Vec3, look: Vec3, up: Vec3, fov: float) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Initialize the FirstPersonCamera.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
eye (Vec3): The position of the camera.
|
|
41
|
+
look (Vec3): The point the camera is looking at.
|
|
42
|
+
up (Vec3): The world's up vector.
|
|
43
|
+
fov (float): The field of view.
|
|
44
|
+
"""
|
|
45
|
+
self.eye: Vec3 = eye
|
|
46
|
+
self.look: Vec3 = look
|
|
47
|
+
self.world_up: Vec3 = up
|
|
48
|
+
self.front: Vec3 = Vec3()
|
|
49
|
+
self.up: Vec3 = Vec3()
|
|
50
|
+
self.right: Vec3 = Vec3()
|
|
51
|
+
self.yaw: float = -90.0
|
|
52
|
+
self.pitch: float = 0.0
|
|
53
|
+
self.speed: float = 2.5
|
|
54
|
+
self.sensitivity: float = 0.1
|
|
55
|
+
self.zoom: float = 45.0
|
|
56
|
+
self.near: float = 0.1
|
|
57
|
+
self.far: float = 100.0
|
|
58
|
+
self.aspect: float = 1.2
|
|
59
|
+
self.fov: float = fov
|
|
60
|
+
self._update_camera_vectors()
|
|
61
|
+
self.projection: Mat4 = self.set_projection(
|
|
62
|
+
self.fov, self.aspect, self.near, self.far
|
|
63
|
+
)
|
|
64
|
+
from .util import look_at
|
|
65
|
+
|
|
66
|
+
self.view: Mat4 = look_at(self.eye, self.eye + self.front, self.up)
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
return f"Camera {self.eye} {self.look} {self.world_up} {self.fov}"
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return f"Camera {self.eye} {self.look} {self.world_up} {self.fov}"
|
|
73
|
+
|
|
74
|
+
def process_mouse_movement(
|
|
75
|
+
self, diffx: float, diffy: float, _constrain_pitch: bool = True
|
|
76
|
+
) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Process mouse movement to update the camera's direction vectors.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
diffx (float): The difference in the x-coordinate of the mouse movement.
|
|
82
|
+
diffy (float): The difference in the y-coordinate of the mouse movement.
|
|
83
|
+
_constrain_pitch (bool, optional): Whether to constrain the pitch angle. Defaults to True.
|
|
84
|
+
"""
|
|
85
|
+
diffx *= self.sensitivity
|
|
86
|
+
diffy *= self.sensitivity
|
|
87
|
+
|
|
88
|
+
self.yaw += diffx
|
|
89
|
+
self.pitch += diffy
|
|
90
|
+
|
|
91
|
+
# Make sure that when pitch is out of bounds, screen doesn't get flipped
|
|
92
|
+
if _constrain_pitch:
|
|
93
|
+
if self.pitch > 89.0:
|
|
94
|
+
self.pitch = 89.0
|
|
95
|
+
if self.pitch < -89.0:
|
|
96
|
+
self.pitch = -89.0
|
|
97
|
+
|
|
98
|
+
self._update_camera_vectors()
|
|
99
|
+
|
|
100
|
+
def _update_camera_vectors(self) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Update the camera's direction vectors based on the current yaw and pitch angles.
|
|
103
|
+
"""
|
|
104
|
+
pitch = math.radians(self.pitch)
|
|
105
|
+
yaw = math.radians(self.yaw)
|
|
106
|
+
self.front.x = math.cos(yaw) * math.cos(pitch)
|
|
107
|
+
self.front.y = math.sin(pitch)
|
|
108
|
+
self.front.z = math.sin(yaw) * math.cos(pitch)
|
|
109
|
+
self.front.normalize()
|
|
110
|
+
# Also re-calculate the Right and Up vector
|
|
111
|
+
self.right = self.front.cross(self.world_up)
|
|
112
|
+
self.up = self.right.cross(self.front)
|
|
113
|
+
# normalize as fast movement can cause issues
|
|
114
|
+
self.right.normalize()
|
|
115
|
+
self.front.normalize()
|
|
116
|
+
from .util import look_at
|
|
117
|
+
|
|
118
|
+
self.view = look_at(self.eye, self.eye + self.front, self.up)
|
|
119
|
+
|
|
120
|
+
def set_projection(
|
|
121
|
+
self, fov: float, aspect: float, near: float, far: float
|
|
122
|
+
) -> Mat4:
|
|
123
|
+
"""
|
|
124
|
+
Set the projection matrix for the camera.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
fov (float): The field of view.
|
|
128
|
+
aspect (float): The aspect ratio.
|
|
129
|
+
near (float): The near clipping plane.
|
|
130
|
+
far (float): The far clipping plane.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Mat4: The projection matrix.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
return perspective(fov, aspect, near, far)
|
|
137
|
+
|
|
138
|
+
def move(self, x: float, y: float, delta: float) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Move the camera based on input directions.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
x (float): The movement in the x-direction.
|
|
144
|
+
y (float): The movement in the y-direction.
|
|
145
|
+
delta (float): The amount to move the camera.
|
|
146
|
+
"""
|
|
147
|
+
velocity = self.speed * delta
|
|
148
|
+
self.eye += self.front * velocity * x
|
|
149
|
+
self.eye += self.right * velocity * y
|
|
150
|
+
self._update_camera_vectors()
|
|
151
|
+
|
|
152
|
+
def get_vp(self) -> Mat4:
|
|
153
|
+
"""
|
|
154
|
+
Get the view-projection matrix.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Mat4: The view-projection matrix.
|
|
158
|
+
"""
|
|
159
|
+
return self.projection @ self.view
|
|
160
|
+
|
|
161
|
+
def process_mouse_scroll(self, y_offset: float) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Process mouse scroll events.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
_yoffset (float): The scroll offset.
|
|
167
|
+
"""
|
|
168
|
+
if self.zoom >= 1.0 and self.zoom <= 45.0:
|
|
169
|
+
self.zoom -= y_offset
|
|
170
|
+
if self.zoom <= 1.0:
|
|
171
|
+
self.zoom = 1.0
|
|
172
|
+
if self.zoom >= 45.0:
|
|
173
|
+
self.zoom = 45.0
|
|
174
|
+
self.projection = perspective(self.zoom, self.aspect, self.near, self.far)
|
ncca/ngl/image.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from PIL import Image as PILImage
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ImageModes(Enum):
|
|
13
|
+
RGB = "RGB"
|
|
14
|
+
RGBA = "RGBA"
|
|
15
|
+
GRAY = "L"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Image:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
filename: str = None,
|
|
22
|
+
width: int = 0,
|
|
23
|
+
height: int = 0,
|
|
24
|
+
mode: ImageModes = None,
|
|
25
|
+
):
|
|
26
|
+
if filename:
|
|
27
|
+
self.load(filename)
|
|
28
|
+
logger.debug(f"Creating Image from file {filename} ")
|
|
29
|
+
else:
|
|
30
|
+
self._width = width
|
|
31
|
+
self._height = height
|
|
32
|
+
self._mode = mode
|
|
33
|
+
if mode:
|
|
34
|
+
if mode == ImageModes.GRAY:
|
|
35
|
+
self._data = np.zeros((height, width), dtype=np.uint8)
|
|
36
|
+
else:
|
|
37
|
+
self._data = np.zeros(
|
|
38
|
+
(height, width, len(mode.value)), dtype=np.uint8
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
self._data = None
|
|
42
|
+
|
|
43
|
+
def set_pixel(self, x, y, r, g, b, a=255):
|
|
44
|
+
if x < 0 or x >= self._width or y < 0 or y >= self._height:
|
|
45
|
+
raise ValueError("Pixel coordinates out of bounds")
|
|
46
|
+
if self._mode == ImageModes.RGBA:
|
|
47
|
+
self._data[y, x] = [r, g, b, a]
|
|
48
|
+
else:
|
|
49
|
+
self._data[y, x] = [r, g, b]
|
|
50
|
+
|
|
51
|
+
def load(self, filename: str) -> bool:
|
|
52
|
+
try:
|
|
53
|
+
with PILImage.open(filename) as img:
|
|
54
|
+
self._width = img.width
|
|
55
|
+
self._height = img.height
|
|
56
|
+
try:
|
|
57
|
+
self._mode = ImageModes(img.mode)
|
|
58
|
+
except ValueError:
|
|
59
|
+
logger.warning(f"Image mode {img.mode} not supported, converting")
|
|
60
|
+
if img.mode == "I;16":
|
|
61
|
+
img = img.convert("L")
|
|
62
|
+
else:
|
|
63
|
+
img = img.convert("RGB")
|
|
64
|
+
self._mode = ImageModes(img.mode)
|
|
65
|
+
|
|
66
|
+
self._data = np.array(img)
|
|
67
|
+
return True
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Error loading image {filename}: {e}")
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def save(self, filename: str) -> bool:
|
|
73
|
+
try:
|
|
74
|
+
img = PILImage.fromarray(self._data).convert(self._mode.value)
|
|
75
|
+
img.save(filename)
|
|
76
|
+
return True
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Error saving image {filename}: {e}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def width(self) -> int:
|
|
83
|
+
return self._width
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def height(self) -> int:
|
|
87
|
+
return self._height
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def mode(self) -> ImageModes:
|
|
91
|
+
return self._mode
|
|
92
|
+
|
|
93
|
+
def get_pixels(self) -> np.ndarray:
|
|
94
|
+
return self._data
|
ncca/ngl/log.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ColoredFormatter(logging.Formatter):
|
|
6
|
+
COLORS = {
|
|
7
|
+
logging.DEBUG: "\033[37m", # White
|
|
8
|
+
logging.INFO: "\033[36m", # Cyan
|
|
9
|
+
logging.WARNING: "\033[33m", # Yellow
|
|
10
|
+
logging.ERROR: "\033[31m", # Red
|
|
11
|
+
logging.CRITICAL: "\033[41m", # Red background
|
|
12
|
+
}
|
|
13
|
+
RESET = "\033[0m"
|
|
14
|
+
|
|
15
|
+
def format(self, record):
|
|
16
|
+
log_message = super().format(record)
|
|
17
|
+
return f"{self.COLORS.get(record.levelno, '')}{log_message}{self.RESET}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def setup_logger():
|
|
21
|
+
logger = logging.getLogger("ngl")
|
|
22
|
+
if not logger.handlers:
|
|
23
|
+
logger.setLevel(logging.DEBUG)
|
|
24
|
+
file_handler = logging.FileHandler("NGLDebug.log", mode="w")
|
|
25
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
26
|
+
|
|
27
|
+
file_formatter = logging.Formatter(
|
|
28
|
+
"%(asctime)s - %(levelname)s - %(message)s",
|
|
29
|
+
datefmt="%H:%M:%S",
|
|
30
|
+
)
|
|
31
|
+
console_formatter = ColoredFormatter(
|
|
32
|
+
"%(asctime)s - %(levelname)s - %(message)s",
|
|
33
|
+
datefmt="%H:%M:%S",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
file_handler.setFormatter(file_formatter)
|
|
37
|
+
console_handler.setFormatter(console_formatter)
|
|
38
|
+
|
|
39
|
+
logger.addHandler(file_handler)
|
|
40
|
+
logger.addHandler(console_handler)
|
|
41
|
+
return logger
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
logger = setup_logger()
|
ncca/ngl/mat2.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
from .vec2 import Vec2
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Mat2Error(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_identity = [[1.0, 0.0], [0.0, 1.0]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Mat2:
|
|
14
|
+
__slots__ = ["m"]
|
|
15
|
+
|
|
16
|
+
def __init__(self, m=None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize a 2x2 matrix.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
m (list): A 2D list representing the matrix.
|
|
22
|
+
If not provided, an identity matrix is created.
|
|
23
|
+
"""
|
|
24
|
+
if m is None:
|
|
25
|
+
self.m = copy.deepcopy(_identity)
|
|
26
|
+
elif isinstance(m, list) and len(m) == 4 and not isinstance(m[0], list):
|
|
27
|
+
self.m = [m[0:2], m[2:4]]
|
|
28
|
+
else:
|
|
29
|
+
self.m = m
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_list(cls, m: list[float]):
|
|
33
|
+
"""
|
|
34
|
+
Initialize a 2x2 matrix from a flat list.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
m (list[float]): A flat list representing the matrix.
|
|
38
|
+
"""
|
|
39
|
+
return cls([m[0:2], m[2:4]])
|
|
40
|
+
|
|
41
|
+
def get_matrix(self) -> list[float]:
|
|
42
|
+
"""
|
|
43
|
+
Get the current matrix representation as a flat list in column-major order.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
list[float]: A flat list of floats.
|
|
47
|
+
"""
|
|
48
|
+
return [item for sublist in zip(*self.m) for item in sublist]
|
|
49
|
+
|
|
50
|
+
def to_numpy(self):
|
|
51
|
+
"""
|
|
52
|
+
Convert the current matrix to a NumPy array.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
np.ndarray: The matrix as a NumPy array.
|
|
56
|
+
"""
|
|
57
|
+
import numpy as np
|
|
58
|
+
|
|
59
|
+
return np.array(self.get_matrix()).reshape([2, 2])
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def identity(cls) -> "Mat2":
|
|
63
|
+
"""
|
|
64
|
+
Create an identity matrix.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Mat2: A new identity Mat2 object.
|
|
68
|
+
"""
|
|
69
|
+
ret = cls()
|
|
70
|
+
ret.m = copy.deepcopy(_identity)
|
|
71
|
+
return ret
|
|
72
|
+
|
|
73
|
+
def __matmul__(self, rhs):
|
|
74
|
+
"""
|
|
75
|
+
Matrix multiplication or vector transformation with a 2D matrix.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
rhs (Mat2 | Vec2): The right-hand side operand.
|
|
79
|
+
If Mat2, perform matrix multiplication.
|
|
80
|
+
If Vec2, transform the vec
|
|
81
|
+
r by the matrix.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Mat2: Resulting matrix from matrix multiplication.
|
|
85
|
+
Vec2: Transformed vector.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If rhs is neither a Mat2 nor Vec2 object.
|
|
89
|
+
"""
|
|
90
|
+
if isinstance(rhs, Mat2):
|
|
91
|
+
return self._mat_mul(rhs)
|
|
92
|
+
elif isinstance(rhs, Vec2):
|
|
93
|
+
return Vec2(
|
|
94
|
+
rhs.x * self.m[0][0] + rhs.y * self.m[0][1],
|
|
95
|
+
rhs.x * self.m[1][0] + rhs.y * self.m[1][1],
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError(f"Can only multiply by Mat2 or Vec2, not {type(rhs)}")
|
|
99
|
+
|
|
100
|
+
def _mat_mul(self, other):
|
|
101
|
+
"""
|
|
102
|
+
Internal method to perform matrix multiplication.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
other (Mat2): The right-hand side matrix.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Mat2: Result of matrix multiplication.
|
|
109
|
+
"""
|
|
110
|
+
ret = Mat2()
|
|
111
|
+
for i in range(2):
|
|
112
|
+
for j in range(2):
|
|
113
|
+
ret.m[i][j] = sum(self.m[i][k] * other.m[k][j] for k in range(2))
|
|
114
|
+
return ret
|
|
115
|
+
|
|
116
|
+
def __str__(self) -> str:
|
|
117
|
+
"""
|
|
118
|
+
String representation of the matrix.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
str: The string representation.
|
|
122
|
+
"""
|
|
123
|
+
return f"Mat2({self.m[0]}, {self.m[1]})"
|
|
124
|
+
|
|
125
|
+
def to_list(self):
|
|
126
|
+
"convert matrix to list in column-major order"
|
|
127
|
+
# flatten to single array
|
|
128
|
+
return [item for sublist in zip(*self.m) for item in sublist]
|