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.
- ncca/ngl/PrimData/Primitives.npz +0 -0
- ncca/ngl/PrimData/pack_arrays.py +20 -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 +816 -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 +346 -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 +106 -0
- ncca/ngl/vec3.py +401 -0
- ncca/ngl/vec3_array.py +110 -0
- ncca/ngl/vec4.py +229 -0
- ncca/ngl/vec4_array.py +106 -0
- {ncca_ngl-0.1.1.dist-info → ncca_ngl-0.1.2.dist-info}/METADATA +10 -9
- ncca_ngl-0.1.2.dist-info/RECORD +51 -0
- {ncca_ngl-0.1.1.dist-info → ncca_ngl-0.1.2.dist-info}/WHEEL +2 -1
- ncca_ngl-0.1.2.dist-info/top_level.txt +1 -0
- ncca_ngl-0.1.1.dist-info/RECORD +0 -4
- {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
|
ncca/ngl/vao_factory.py
ADDED
|
@@ -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))
|