ncca-ngl 0.1.1__py3-none-any.whl → 0.1.2__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 (52) hide show
  1. ncca/ngl/PrimData/Primitives.npz +0 -0
  2. ncca/ngl/PrimData/pack_arrays.py +20 -0
  3. ncca/ngl/__init__.py +100 -0
  4. ncca/ngl/abstract_vao.py +85 -0
  5. ncca/ngl/base_mesh.py +170 -0
  6. ncca/ngl/base_mesh.pyi +11 -0
  7. ncca/ngl/bbox.py +224 -0
  8. ncca/ngl/bezier_curve.py +75 -0
  9. ncca/ngl/first_person_camera.py +174 -0
  10. ncca/ngl/image.py +94 -0
  11. ncca/ngl/log.py +44 -0
  12. ncca/ngl/mat2.py +128 -0
  13. ncca/ngl/mat3.py +466 -0
  14. ncca/ngl/mat4.py +456 -0
  15. ncca/ngl/multi_buffer_vao.py +49 -0
  16. ncca/ngl/obj.py +416 -0
  17. ncca/ngl/plane.py +47 -0
  18. ncca/ngl/primitives.py +706 -0
  19. ncca/ngl/pyside_event_handling_mixin.py +318 -0
  20. ncca/ngl/quaternion.py +112 -0
  21. ncca/ngl/random.py +167 -0
  22. ncca/ngl/shader.py +229 -0
  23. ncca/ngl/shader_lib.py +536 -0
  24. ncca/ngl/shader_program.py +816 -0
  25. ncca/ngl/shaders/checker_fragment.glsl +35 -0
  26. ncca/ngl/shaders/checker_vertex.glsl +19 -0
  27. ncca/ngl/shaders/colour_fragment.glsl +8 -0
  28. ncca/ngl/shaders/colour_vertex.glsl +11 -0
  29. ncca/ngl/shaders/diffuse_fragment.glsl +21 -0
  30. ncca/ngl/shaders/diffuse_vertex.glsl +24 -0
  31. ncca/ngl/shaders/text_fragment.glsl +10 -0
  32. ncca/ngl/shaders/text_geometry.glsl +53 -0
  33. ncca/ngl/shaders/text_vertex.glsl +18 -0
  34. ncca/ngl/simple_index_vao.py +65 -0
  35. ncca/ngl/simple_vao.py +42 -0
  36. ncca/ngl/text.py +346 -0
  37. ncca/ngl/texture.py +75 -0
  38. ncca/ngl/transform.py +95 -0
  39. ncca/ngl/util.py +128 -0
  40. ncca/ngl/vao_factory.py +34 -0
  41. ncca/ngl/vec2.py +350 -0
  42. ncca/ngl/vec2_array.py +106 -0
  43. ncca/ngl/vec3.py +401 -0
  44. ncca/ngl/vec3_array.py +110 -0
  45. ncca/ngl/vec4.py +229 -0
  46. ncca/ngl/vec4_array.py +106 -0
  47. {ncca_ngl-0.1.1.dist-info → ncca_ngl-0.1.2.dist-info}/METADATA +10 -9
  48. ncca_ngl-0.1.2.dist-info/RECORD +51 -0
  49. {ncca_ngl-0.1.1.dist-info → ncca_ngl-0.1.2.dist-info}/WHEEL +2 -1
  50. ncca_ngl-0.1.2.dist-info/top_level.txt +1 -0
  51. ncca_ngl-0.1.1.dist-info/RECORD +0 -4
  52. {ncca_ngl-0.1.1.dist-info → ncca_ngl-0.1.2.dist-info}/licenses/LICENSE.txt +0 -0
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}"
ncca/ngl/util.py ADDED
@@ -0,0 +1,128 @@
1
+ """Utility math module, contains various useful functions for 3D.
2
+
3
+ Most of these functions are based on functions found in other libraries such as GLM, NGL or GLU
4
+ """
5
+
6
+ import math
7
+
8
+ from .mat4 import Mat4
9
+
10
+
11
+ def clamp(num, low, high):
12
+ "clamp to range min and max will throw ValueError is low>=high"
13
+ if low > high or low == high:
14
+ raise ValueError
15
+ return max(min(num, high), low)
16
+
17
+
18
+ def look_at(eye, look, up):
19
+ """
20
+ Calculate 4x4 matrix for camera lookAt
21
+ """
22
+
23
+ n = look - eye
24
+ u = up
25
+ v = n.cross(u)
26
+ u = v.cross(n)
27
+ n.normalize()
28
+ v.normalize()
29
+ u.normalize()
30
+
31
+ result = Mat4.identity()
32
+ result.m[0][0] = v.x
33
+ result.m[1][0] = v.y
34
+ result.m[2][0] = v.z
35
+ result.m[0][1] = u.x
36
+ result.m[1][1] = u.y
37
+ result.m[2][1] = u.z
38
+ result.m[0][2] = -n.x
39
+ result.m[1][2] = -n.y
40
+ result.m[2][2] = -n.z
41
+ result.m[3][0] = -eye.dot(v)
42
+ result.m[3][1] = -eye.dot(u)
43
+ result.m[3][2] = eye.dot(n)
44
+ return result
45
+
46
+
47
+ def perspective(fov, aspect, near, far):
48
+ m = Mat4.zero() # as per glm
49
+ _range = math.tan(math.radians(fov / 2.0)) * near
50
+ left = -_range * aspect
51
+ right = _range * aspect
52
+ bottom = -_range
53
+ top = _range
54
+ m.m[0][0] = (2.0 * near) / (right - left)
55
+ m.m[1][1] = (2.0 * near) / (top - bottom)
56
+ m.m[2][2] = -(far + near) / (far - near)
57
+ m.m[2][3] = -1.0
58
+ m.m[3][2] = -(2.0 * far * near) / (far - near)
59
+ return m
60
+
61
+
62
+ def ortho(left, right, bottom, top, near, far):
63
+ """Create an orthographic projection matrix."""
64
+ m = Mat4.identity()
65
+ m.m[0][0] = 2.0 / (right - left)
66
+ m.m[1][1] = 2.0 / (top - bottom)
67
+ m.m[2][2] = -2.0 / (far - near)
68
+ m.m[3][0] = -(right + left) / (right - left)
69
+ m.m[3][1] = -(top + bottom) / (top - bottom)
70
+ m.m[3][2] = -(far + near) / (far - near)
71
+ return m
72
+
73
+
74
+ # Mat4 result(1.0f);
75
+ # result.m_00= 2.0f / (_right - _left);
76
+ # result.m_11= 2.0f / (_top - _bottom);
77
+ # result.m_22= - 2.0f / (_zFar - _zNear);
78
+ # result.m_30= - (_right + _left) / (_right - _left);
79
+ # result.m_31= - (_top + _bottom) / (_top - _bottom);
80
+ # result.m_32= - (_zFar + _zNear) / (_zFar - _zNear);
81
+ # return result;
82
+
83
+
84
+ def frustum(left, right, bottom, top, near, far):
85
+ """Create a frustum projection matrix."""
86
+ m = Mat4.zero()
87
+ m.m[0][0] = (2.0 * near) / (right - left)
88
+ m.m[1][1] = (2.0 * near) / (top - bottom)
89
+ m.m[2][0] = (right + left) / (right - left)
90
+ m.m[2][1] = (top + bottom) / (top - bottom)
91
+ m.m[2][2] = -(far + near) / (far - near)
92
+ m.m[2][3] = -1.0
93
+ m.m[3][2] = -(2.0 * far * near) / (far - near)
94
+ return m
95
+
96
+
97
+ def lerp(a, b, t):
98
+ return a + (b - a) * t
99
+
100
+
101
+ def calc_normal(p1, p2, p3):
102
+ """
103
+ Calculates the normal of a triangle defined by three points.
104
+
105
+ This is a Python implementation of the NGL C++ Util::calcNormal function.
106
+ It uses the vector cross product method for clarity and leverages the py-ngl library.
107
+ The order of the cross product is chosen to match the output of the C++ version.
108
+
109
+ Args:
110
+ p1: The first vertex of the triangle.
111
+ p2: The second vertex of the triangle.
112
+ p3: The third vertex of the triangle.
113
+
114
+ Returns:
115
+ The normalized normal vector of the triangle.
116
+ """
117
+ # Two vectors on the plane of the triangle
118
+ v1 = p3 - p1
119
+ v2 = p2 - p1
120
+
121
+ # The cross product gives the normal vector.
122
+ # The order (v1 x v2) is used to match the C++ implementation's result.
123
+ normal = v1.cross(v2)
124
+
125
+ # Normalize the result to get a unit length normal
126
+ normal.normalize()
127
+
128
+ return normal
@@ -0,0 +1,34 @@
1
+ import enum
2
+
3
+ from .multi_buffer_vao import MultiBufferVAO
4
+ from .simple_index_vao import SimpleIndexVAO
5
+ from .simple_vao import SimpleVAO
6
+ from .log import logger
7
+
8
+
9
+ class VAOType(enum.Enum):
10
+ SIMPLE = "simpleVAO"
11
+ MULTI_BUFFER = "multiBufferVAO"
12
+ SIMPLE_INDEX = "simpleIndexVAO"
13
+
14
+
15
+ class VAOFactory:
16
+ _creators = {}
17
+
18
+ @staticmethod
19
+ def register_vao_creator(name, creator_func):
20
+ VAOFactory._creators[name] = creator_func
21
+
22
+ @staticmethod
23
+ def create_vao(name, mode):
24
+ creator = VAOFactory._creators.get(name)
25
+ if not creator:
26
+ logger.warning(f"VAO type '{name}' not found.")
27
+ raise ValueError(name)
28
+ return creator(mode)
29
+
30
+
31
+ # pre-register the default VAO types
32
+ VAOFactory.register_vao_creator(VAOType.SIMPLE, SimpleVAO)
33
+ VAOFactory.register_vao_creator(VAOType.MULTI_BUFFER, MultiBufferVAO)
34
+ VAOFactory.register_vao_creator(VAOType.SIMPLE_INDEX, SimpleIndexVAO)
ncca/ngl/vec2.py ADDED
@@ -0,0 +1,350 @@
1
+ """
2
+ Simple float only Vec2 class for 3D graphics, very similar to the pyngl ones
3
+ """
4
+
5
+ import ctypes
6
+ import math
7
+
8
+ from .util import clamp
9
+
10
+
11
+ class Vec2:
12
+ """
13
+ A simple 3D vector class for 3D graphics, I use slots to fix the attributes to x,y,z
14
+ Attributes:
15
+ x (float): The x-coordinate of the vector.
16
+ y (float): The y-coordinate of the vector.
17
+ """
18
+
19
+ __slots__ = ["_x", "_y"] # fix the attributes to x,y,z
20
+
21
+ def __init__(self, x=0.0, y=0.0):
22
+ """
23
+ Initializes a new instance of the Vec2 class.
24
+
25
+ Args:
26
+ x (float, optional): The x-coordinate of the vector. Defaults to 0.0.
27
+ y (float, optional): The y-coordinate of the vector. Defaults to 0.0.
28
+ """
29
+ self._x = x # x component of vector : float
30
+ self._y = y # y component of vector : float
31
+
32
+ @classmethod
33
+ def sizeof(cls):
34
+ return 2 * ctypes.sizeof(ctypes.c_float)
35
+
36
+ def __iter__(self):
37
+ """
38
+ Make the Vec2 class iterable.
39
+ Yields:
40
+ float: The x and y components of the vector.
41
+ """
42
+ yield self.x
43
+ yield self.y
44
+
45
+ def clone(self) -> "Vec2":
46
+ """
47
+ Create a copy of the vector.
48
+ Returns:
49
+ Vec2: A new instance of Vec2 with the same x and y values.
50
+ """
51
+ return Vec2(self.x, self.y)
52
+
53
+ def __getitem__(self, index):
54
+ """
55
+ Get the component of the vector at the given index.
56
+ Args:
57
+ index (int): The index of the component (0 for x, 1 for y, 2 for z).
58
+ Returns:
59
+ float: The value of the component at the given index.
60
+ Raises:
61
+ IndexError: If the index is out of range.
62
+ """
63
+ components = [self.x, self.y]
64
+ try:
65
+ return components[index]
66
+ except IndexError:
67
+ raise IndexError("Index out of range. Valid indices are 0, 1,")
68
+
69
+ def _validate_and_set(self, v, name):
70
+ """
71
+ check if v is a float or int
72
+ Args:
73
+ v (number): The value to check.
74
+ Raises:
75
+ ValueError: If v is not a float or int.
76
+ """
77
+ if not isinstance(v, (int, float)):
78
+ raise ValueError("need float or int")
79
+ else:
80
+ setattr(self, name, v)
81
+
82
+ def __add__(self, rhs):
83
+ """
84
+ vector addition a+b
85
+
86
+ Args:
87
+ rhs (Vec2): The right-hand side vector to add.
88
+ Returns:
89
+ Vec2: A new vector that is the result of adding this vector and the rhs vector.
90
+ """
91
+ r = Vec2()
92
+ r.x = self.x + rhs.x
93
+ r.y = self.y + rhs.y
94
+ return r
95
+
96
+ def __iadd__(self, rhs):
97
+ """
98
+ vector addition a+=b
99
+
100
+ Args:
101
+ rhs (Vec2): The right-hand side vector to add.
102
+ Returns:
103
+ Vec2: returns this vector after adding the rhs vector.
104
+ """
105
+ self.x += rhs.x
106
+ self.y += rhs.y
107
+ return self
108
+
109
+ def __sub__(self, rhs):
110
+ """
111
+ vector subtraction a-b
112
+
113
+ Args:
114
+ rhs (Vec2): The right-hand side vector to add.
115
+ Returns:
116
+ Vec2: A new vector that is the result of subtracting this vector and the rhs vector.
117
+ """
118
+ r = Vec2()
119
+ r.x = self.x - rhs.x
120
+ r.y = self.y - rhs.y
121
+ return r
122
+
123
+ def __isub__(self, rhs):
124
+ """
125
+ vector subtraction a-=b
126
+
127
+ Args:
128
+ rhs (Vec2): The right-hand side vector to add.
129
+ Returns:
130
+ Vec2: returns this vector after subtracting the rhs vector.
131
+ """
132
+ self.x -= rhs.x
133
+ self.y -= rhs.y
134
+ return self
135
+
136
+ def __eq__(self, rhs):
137
+ """
138
+ vector comparison a==b using math.isclose not we only compare to 6 decimal places
139
+ Args:
140
+ rhs (Vec2): The right-hand side vector to compare.
141
+ Returns:
142
+ bool: True if the vectors are close, False otherwise.
143
+ NotImplemented: If the right-hand side is not a Vec2.
144
+ """
145
+
146
+ if not isinstance(rhs, Vec2):
147
+ return NotImplemented
148
+ return math.isclose(self.x, rhs.x) and math.isclose(self.y, rhs.y)
149
+
150
+ def __neq__(self, rhs):
151
+ """
152
+ vector comparison a!=b using math.isclose not we only compare to 6 decimal places
153
+ Args:
154
+ rhs (Vec2): The right-hand side vector to compare.
155
+ Returns:
156
+ bool: True if the vectors are not close, False otherwise.
157
+ NotImplemented: If the right-hand side is not a Vec2.
158
+ """
159
+
160
+ if not isinstance(rhs, Vec2):
161
+ return NotImplemented
162
+ return not (math.isclose(self.x, rhs.x) and math.isclose(self.y, rhs.y))
163
+
164
+ def __neg__(self):
165
+ """
166
+ negate a vector -a
167
+ """
168
+ self.x = -self.x
169
+ self.y = -self.y
170
+ return self
171
+
172
+ def set(self, x, y):
173
+ """
174
+ set the x,y,z values of the vector
175
+ Args:
176
+ x (float): The x-coordinate of the vector.
177
+ y (float): The y-coordinate of the vector.
178
+ Raises :
179
+ ValueError: if x,y are not float
180
+ """
181
+ try:
182
+ self.x = float(x)
183
+ self.y = float(y)
184
+ except ValueError:
185
+ raise ValueError(f"Vec2.set {x=} {y=} all need to be float")
186
+
187
+ def dot(self, rhs):
188
+ """
189
+ dot product of two vectors a.b
190
+ Args:
191
+ rhs (Vec2): The right-hand side vector to dot product with.
192
+ """
193
+ return self.x * rhs.x + self.y * rhs.y
194
+
195
+ def length(self):
196
+ """
197
+ length of vector
198
+ Returns:
199
+ float: The length of the vector.
200
+ """
201
+ return math.sqrt(self.x**2 + self.y**2)
202
+
203
+ def length_squared(self):
204
+ """
205
+ length of vector squared sometimes used to avoid the sqrt for performance
206
+ Returns:
207
+ float: The length of the vector squared
208
+ """
209
+ return self.x**2 + self.y**2
210
+
211
+ def inner(self, rhs):
212
+ """
213
+ inner product of two vectors a.b
214
+ Args:
215
+ rhs (Vec2): The right-hand side vector to inner product with.
216
+ Returns:
217
+ float: The inner product of the two vectors.
218
+ """
219
+ return (self.x * rhs.x) + (self.y * rhs.y)
220
+
221
+ def null(self):
222
+ """
223
+ set the vector to zero
224
+ """
225
+ self.x = 0.0
226
+ self.y = 0.0
227
+
228
+ def cross(self, rhs):
229
+ """
230
+ cross product of two vectors a x b
231
+ Args:
232
+ rhs (Vec2): The right-hand side vector to cross product with.
233
+ Returns:
234
+ float : 2D cross product or perpendicular dot product.
235
+ """
236
+ return self.x * rhs.y - self.y * rhs.x
237
+
238
+ def normalize(self):
239
+ """
240
+ normalize the vector to unit length
241
+ Returns:
242
+ Vec2: A new vector that is the result of normalizing this vector.
243
+ Raises:
244
+ ZeroDivisionError: If the length of the vector is zero.
245
+ """
246
+ vector_length = self.length()
247
+ try:
248
+ self.x /= vector_length
249
+ self.y /= vector_length
250
+ except ZeroDivisionError:
251
+ raise ZeroDivisionError(
252
+ f"Vec2.normalize {vector_length} length is zero most likely calling normalize on a zero vector"
253
+ )
254
+ return self
255
+
256
+ def reflect(self, n):
257
+ """
258
+ reflect a vector about a normal
259
+ Args:
260
+ n (Vec2): The normal to reflect about.
261
+ Returns:
262
+ Vec2: A new vector that is the result of reflecting this vector about the normal.
263
+ """
264
+ d = self.dot(n)
265
+ # I - 2.0 * dot(N, I) * N
266
+ return Vec2(self.x - 2.0 * d * n.x, self.y - 2.0 * d * n.y)
267
+
268
+ def clamp(self, low, high):
269
+ """
270
+ clamp the vector to a range
271
+ Args:
272
+ low (float): The low end of the range.
273
+ high (float): The high end of the range.
274
+
275
+ """
276
+ self.x = clamp(self.x, low, high)
277
+ self.y = clamp(self.y, low, high)
278
+
279
+ def __repr__(self):
280
+ "object representation for debugging"
281
+ return f"Vec2 [{self.x},{self.y}]"
282
+
283
+ def __truediv__(self, rhs):
284
+ if isinstance(rhs, (float, int)):
285
+ return Vec2(self.x / rhs, self.y / rhs)
286
+ elif isinstance(rhs, Vec2):
287
+ return Vec2(self.x / rhs.x, self.y / rhs.y)
288
+ else:
289
+ raise ValueError(f"can only do piecewise division with a scalar {rhs=}")
290
+
291
+ def __str__(self):
292
+ "object representation for debugging"
293
+ return f"[{self.x},{self.y}]"
294
+
295
+ def __mul__(self, rhs):
296
+ """
297
+ piecewise scalar multiplication
298
+ Args:
299
+ rhs (float): The scalar to multiply by.
300
+ Returns:
301
+ Vec2: A new vector that is the result of multiplying this vector by the scalar.
302
+ Raises:
303
+ ValueError: If the right-hand side is not a float.
304
+ """
305
+ if isinstance(rhs, (float, int)):
306
+ return Vec2(self.x * rhs, self.y * rhs)
307
+ else:
308
+ raise ValueError(f"can only do piecewise multiplication with a scalar {rhs=}")
309
+
310
+ def __rmul__(self, rhs):
311
+ """
312
+ piecewise scalar multiplication
313
+ Args:
314
+ rhs (float): The scalar to multiply by.
315
+ Returns:
316
+ Vec2: A new vector that is the result of multiplying this vector by the scalar.
317
+ Raises:
318
+ ValueError: If the right-hand side is not a float.
319
+ """
320
+ return self * rhs
321
+
322
+ def __matmul__(self, rhs):
323
+ """
324
+ "Vec2 @ Mat2 matrix multiplication"
325
+ Args:
326
+ rhs (Mat2): The matrix to multiply by.
327
+ Returns:
328
+ Vec2: A new vector that is the result of multiplying this vector by the matrix.
329
+ """
330
+ return Vec2(
331
+ self.x * rhs.m[0][0] + self.y * rhs.m[1][0] + self.z * rhs.m[2][0],
332
+ self.x * rhs.m[0][1] + self.y * rhs.m[1][1] + self.z * rhs.m[2][1],
333
+ self.x * rhs.m[0][2] + self.y * rhs.m[1][2] + self.z * rhs.m[2][2],
334
+ )
335
+
336
+
337
+ # Helper function to create properties
338
+ def _create_property(attr_name):
339
+ def getter(self):
340
+ return getattr(self, f"_{attr_name}")
341
+
342
+ def setter(self, value):
343
+ self._validate_and_set(value, f"_{attr_name}")
344
+
345
+ return property(getter, setter)
346
+
347
+
348
+ # Dynamically add properties for x, y
349
+ for attr in ["x", "y"]:
350
+ setattr(Vec2, attr, _create_property(attr))