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
@@ -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]