ncca-ngl 0.3.4__py3-none-any.whl → 0.5.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.
Files changed (56) hide show
  1. ncca/ngl/PrimData/pack_arrays.py +2 -3
  2. ncca/ngl/__init__.py +3 -4
  3. ncca/ngl/base_mesh.py +28 -20
  4. ncca/ngl/image.py +1 -3
  5. ncca/ngl/mat2.py +79 -53
  6. ncca/ngl/mat3.py +104 -185
  7. ncca/ngl/mat4.py +144 -309
  8. ncca/ngl/prim_data.py +42 -36
  9. ncca/ngl/primitives.py +2 -2
  10. ncca/ngl/pyside_event_handling_mixin.py +0 -108
  11. ncca/ngl/quaternion.py +69 -36
  12. ncca/ngl/shader.py +0 -116
  13. ncca/ngl/shader_program.py +94 -117
  14. ncca/ngl/texture.py +5 -2
  15. ncca/ngl/util.py +7 -0
  16. ncca/ngl/vec2.py +58 -292
  17. ncca/ngl/vec2_array.py +79 -28
  18. ncca/ngl/vec3.py +59 -340
  19. ncca/ngl/vec3_array.py +76 -23
  20. ncca/ngl/vec4.py +90 -190
  21. ncca/ngl/vec4_array.py +78 -27
  22. ncca/ngl/vector_base.py +542 -0
  23. ncca/ngl/webgpu/__init__.py +20 -0
  24. ncca/ngl/webgpu/__main__.py +640 -0
  25. ncca/ngl/webgpu/__main__.py.backup +640 -0
  26. ncca/ngl/webgpu/base_webgpu_pipeline.py +354 -0
  27. ncca/ngl/webgpu/custom_shader_pipeline.py +288 -0
  28. ncca/ngl/webgpu/instanced_geometry_pipeline.py +594 -0
  29. ncca/ngl/webgpu/line_pipeline.py +405 -0
  30. ncca/ngl/webgpu/pipeline_factory.py +190 -0
  31. ncca/ngl/webgpu/pipeline_shaders.py +497 -0
  32. ncca/ngl/webgpu/point_list_pipeline.py +349 -0
  33. ncca/ngl/webgpu/point_pipeline.py +336 -0
  34. ncca/ngl/webgpu/triangle_pipeline.py +419 -0
  35. ncca/ngl/webgpu/webgpu_constants.py +31 -0
  36. ncca/ngl/webgpu/webgpu_widget.py +322 -0
  37. ncca/ngl/webgpu/wip/REFACTORING_SUMMARY.md +82 -0
  38. ncca/ngl/webgpu/wip/UNIFIED_SYSTEM.md +314 -0
  39. ncca/ngl/webgpu/wip/buffer_manager.py +396 -0
  40. ncca/ngl/webgpu/wip/pipeline_config.py +463 -0
  41. ncca/ngl/webgpu/wip/shader_constants.py +328 -0
  42. ncca/ngl/webgpu/wip/shader_templates.py +563 -0
  43. ncca/ngl/webgpu/wip/unified_examples.py +390 -0
  44. ncca/ngl/webgpu/wip/unified_factory.py +449 -0
  45. ncca/ngl/webgpu/wip/unified_pipeline.py +469 -0
  46. ncca/ngl/widgets/__init__.py +18 -2
  47. ncca/ngl/widgets/__main__.py +2 -1
  48. ncca/ngl/widgets/lookatwidget.py +2 -1
  49. ncca/ngl/widgets/mat4widget.py +2 -2
  50. ncca/ngl/widgets/vec2widget.py +1 -1
  51. ncca/ngl/widgets/vec3widget.py +1 -0
  52. {ncca_ngl-0.3.4.dist-info → ncca_ngl-0.5.0.dist-info}/METADATA +3 -2
  53. ncca_ngl-0.5.0.dist-info/RECORD +105 -0
  54. ncca/ngl/widgets/transformation_widget.py +0 -299
  55. ncca_ngl-0.3.4.dist-info/RECORD +0 -82
  56. {ncca_ngl-0.3.4.dist-info → ncca_ngl-0.5.0.dist-info}/WHEEL +0 -0
ncca/ngl/prim_data.py CHANGED
@@ -1,13 +1,15 @@
1
- import enum
1
+ from enum import Enum
2
2
  from pathlib import Path
3
- from typing import Union
4
3
 
5
4
  import numpy as np
6
5
 
7
6
  from .vec3 import Vec3
8
7
 
8
+ RAD_POS = "Radius must be positive"
9
+ NON_NEG = "Height must be non-negative"
9
10
 
10
- class Prims(enum.Enum):
11
+
12
+ class Prims(Enum):
11
13
  """Enum for the default primitives that can be loaded."""
12
14
 
13
15
  BUDDHA = "buddah"
@@ -346,64 +348,43 @@ class PrimData:
346
348
  return np.array(data, dtype=np.float32)
347
349
 
348
350
  @staticmethod
349
- def capsule(radius: float, height: float, precision: int) -> np.ndarray:
350
- """
351
- Creates a capsule primitive.
352
- The capsule is aligned along the y-axis.
353
- It is composed of a cylinder and two hemispherical caps.
354
- based on code from here https://code.google.com/p/rgine/source/browse/trunk/RGine/opengl/src/RGLShapes.cpp
355
- and adapted
356
- """
357
- if radius <= 0.0:
358
- raise ValueError("Radius must be positive")
359
- if height < 0.0:
360
- raise ValueError("Height must be non-negative")
361
- if precision < 4:
362
- precision = 4
363
-
364
- data = []
365
- h = height / 2.0
366
- ang = np.pi / precision
367
-
368
- # Cylinder sides
351
+ def _add_cylinder_sides(data: list, radius: float, h: float, ang: float, precision: int):
352
+ """Generates cylinder side geometry."""
369
353
  for i in range(2 * precision):
370
354
  c = radius * np.cos(ang * i)
371
355
  c1 = radius * np.cos(ang * (i + 1))
372
356
  s = radius * np.sin(ang * i)
373
357
  s1 = radius * np.sin(ang * (i + 1))
374
-
375
358
  # normals for cylinder sides
376
359
  nc = np.cos(ang * i)
377
360
  ns = np.sin(ang * i)
378
361
  nc1 = np.cos(ang * (i + 1))
379
362
  ns1 = np.sin(ang * (i + 1))
380
-
381
363
  # side top
382
364
  data.extend([c1, h, s1, nc1, 0.0, ns1, 0.0, 0.0])
383
365
  data.extend([c, h, s, nc, 0.0, ns, 0.0, 0.0])
384
366
  data.extend([c, -h, s, nc, 0.0, ns, 0.0, 0.0])
385
-
386
367
  # side bot
387
368
  data.extend([c, -h, s, nc, 0.0, ns, 0.0, 0.0])
388
369
  data.extend([c1, -h, s1, nc1, 0.0, ns1, 0.0, 0.0])
389
370
  data.extend([c1, h, s1, nc1, 0.0, ns1, 0.0, 0.0])
390
- # Hemispherical caps
371
+
372
+ @staticmethod
373
+ def _add_hemispherical_caps(data: list, radius: float, h: float, ang: float, precision: int):
374
+ """Generates hemispherical cap geometry."""
391
375
  for i in range(2 * precision):
392
376
  # longitude
393
377
  s = -np.sin(ang * i)
394
378
  s1 = -np.sin(ang * (i + 1))
395
379
  c = np.cos(ang * i)
396
380
  c1 = np.cos(ang * (i + 1))
397
-
398
381
  for j in range(precision + 1):
399
382
  o = h if j < precision / 2 else -h
400
-
401
383
  # latitude
402
384
  sb = radius * np.sin(ang * j)
403
385
  sb1 = radius * np.sin(ang * (j + 1))
404
386
  cb = radius * np.cos(ang * j)
405
387
  cb1 = radius * np.cos(ang * (j + 1))
406
-
407
388
  if j != precision - 1:
408
389
  nx, ny, nz = sb * c, cb, sb * s
409
390
  data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
@@ -411,7 +392,6 @@ class PrimData:
411
392
  data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
412
393
  nx, ny, nz = sb1 * c1, cb1, sb1 * s1
413
394
  data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
414
-
415
395
  if j != 0:
416
396
  nx, ny, nz = sb * c, cb, sb * s
417
397
  data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
@@ -420,6 +400,32 @@ class PrimData:
420
400
  nx, ny, nz = sb * c1, cb, sb * s1
421
401
  data.extend([nx, ny + o, nz, nx, ny, nz, 0.0, 0.0])
422
402
 
403
+ @staticmethod
404
+ def capsule(radius: float, height: float, precision: int) -> np.ndarray:
405
+ """
406
+ Creates a capsule primitive.
407
+ The capsule is aligned along the y-axis.
408
+ It is composed of a cylinder and two hemispherical caps.
409
+ based on code from here https://code.google.com/p/rgine/source/browse/trunk/RGine/opengl/src/RGLShapes.cpp
410
+ and adapted
411
+ """
412
+ if radius <= 0.0:
413
+ raise ValueError(RAD_POS)
414
+ if height < 0.0:
415
+ raise ValueError(NON_NEG)
416
+ if precision < 4:
417
+ precision = 4
418
+
419
+ data = []
420
+ h = height / 2.0
421
+ ang = np.pi / precision
422
+
423
+ # Cylinder sides
424
+ PrimData._add_cylinder_sides(data, radius, h, ang, precision)
425
+
426
+ # Hemispherical caps
427
+ PrimData._add_hemispherical_caps(data, radius, h, ang, precision)
428
+
423
429
  return np.array(data, dtype=np.float32)
424
430
 
425
431
  @staticmethod
@@ -430,9 +436,9 @@ class PrimData:
430
436
  This method generates the cylinder walls, but not the top and bottom caps.
431
437
  """
432
438
  if radius <= 0.0:
433
- raise ValueError("Radius must be positive")
439
+ raise ValueError(RAD_POS)
434
440
  if height < 0.0:
435
- raise ValueError("Height must be non-negative")
441
+ raise ValueError(NON_NEG)
436
442
  if slices < 3:
437
443
  slices = 3
438
444
  if stacks < 1:
@@ -486,7 +492,7 @@ class PrimData:
486
492
  slices: The number of slices to divide the disk into.
487
493
  """
488
494
  if radius <= 0.0:
489
- raise ValueError("Radius must be positive")
495
+ raise ValueError(RAD_POS)
490
496
  if slices < 3:
491
497
  slices = 3
492
498
 
@@ -540,7 +546,7 @@ class PrimData:
540
546
  rings: The number of rings for the torus.
541
547
  """
542
548
  if minor_radius <= 0 or major_radius <= 0:
543
- raise ValueError("Radii must be positive")
549
+ raise ValueError(RAD_POS)
544
550
  if sides < 3 or rings < 3:
545
551
  raise ValueError("Sides and rings must be at least 3")
546
552
 
@@ -602,7 +608,7 @@ class PrimData:
602
608
  return np.array(data, dtype=np.float32)
603
609
 
604
610
  @staticmethod
605
- def primitive(name: Union[str, enum]) -> np.ndarray:
611
+ def primitive(name: str | Enum) -> np.ndarray:
606
612
  prim_folder = Path(__file__).parent / "PrimData"
607
613
  prims = np.load(prim_folder / "Primitives.npz")
608
614
  if isinstance(name, Prims):
ncca/ngl/primitives.py CHANGED
@@ -6,7 +6,7 @@ We need to create the data first which is stored in a map as part of the class,
6
6
  which will generate a pipeline for this object and draw into the current context.
7
7
  """
8
8
 
9
- from typing import Dict, Union
9
+ from typing import Dict
10
10
 
11
11
  import numpy as np
12
12
  import OpenGL.GL as gl
@@ -104,7 +104,7 @@ class Primitives:
104
104
  cls._loaded = True
105
105
 
106
106
  @classmethod
107
- def draw(cls, name: Union[str, Prims]) -> None:
107
+ def draw(cls, name: str | Prims) -> None:
108
108
  """
109
109
  Draws the specified primitive.
110
110
 
@@ -22,22 +22,6 @@ from PySide6.QtCore import Qt
22
22
  from .vec3 import Vec3
23
23
 
24
24
 
25
- class EventHandlingTarget(Protocol):
26
- """
27
- Protocol defining the interface that classes using EventHandlingMixin must implement.
28
-
29
- This ensures that the mixin has access to the necessary methods and attributes.
30
- """
31
-
32
- def update(self) -> None:
33
- """Trigger a redraw of the window."""
34
- ...
35
-
36
- def close(self) -> None:
37
- """Close the window."""
38
- ...
39
-
40
-
41
25
  class PySideEventHandlingMixin:
42
26
  """
43
27
  Mixin class providing standard event handling for PyNGL applications.
@@ -96,32 +80,12 @@ class PySideEventHandlingMixin:
96
80
  self.INCREMENT = self.translation_sensitivity
97
81
  self.ZOOM = self.zoom_sensitivity
98
82
 
99
- # def sync_legacy_attributes(self) -> None:
100
- # """
101
- # Synchronize legacy attribute names with new ones.
102
- # Call this if you modify the legacy attributes directly.
103
- # """
104
- # self.spin_x_face = self.spinXFace
105
- # self.spin_y_face = self.spinYFace
106
- # self.model_position = self.modelPos
107
- # self.original_x_rotation = self.origX
108
- # self.original_y_rotation = self.origY
109
- # self.original_x_pos = self.origXPos
110
- # self.original_y_pos = self.origYPos
111
- # self.translation_sensitivity = self.INCREMENT
112
- # self.zoom_sensitivity = self.ZOOM
113
-
114
83
  def reset_camera(self) -> None:
115
84
  """Reset camera rotation and model position to defaults."""
116
85
  self.spin_x_face = 0
117
86
  self.spin_y_face = 0
118
87
  self.model_position.set(0, 0, 0)
119
88
 
120
- # # Sync legacy attributes
121
- # self.spinXFace = 0
122
- # self.spinYFace = 0
123
- # self.modelPos.set(0, 0, 0)
124
-
125
89
  def keyPressEvent(self, event) -> None:
126
90
  """
127
91
  Handle keyboard press events with common shortcuts.
@@ -175,12 +139,6 @@ class PySideEventHandlingMixin:
175
139
  self.original_x_rotation = position.x()
176
140
  self.original_y_rotation = position.y()
177
141
 
178
- # # Sync legacy attributes
179
- # self.spinXFace = self.spin_x_face
180
- # self.spinYFace = self.spin_y_face
181
- # self.origX = self.original_x_rotation
182
- # self.origY = self.original_y_rotation
183
-
184
142
  self.update()
185
143
 
186
144
  # Handle translation with right mouse button
@@ -194,11 +152,6 @@ class PySideEventHandlingMixin:
194
152
  self.model_position.x += self.translation_sensitivity * diff_x
195
153
  self.model_position.y -= self.translation_sensitivity * diff_y
196
154
 
197
- # # Sync legacy attributes
198
- # self.origXPos = self.original_x_pos
199
- # self.origYPos = self.original_y_pos
200
- # self.modelPos = self.model_position
201
-
202
155
  self.update()
203
156
 
204
157
  def mousePressEvent(self, event) -> None:
@@ -218,19 +171,11 @@ class PySideEventHandlingMixin:
218
171
  self.original_y_rotation = position.y()
219
172
  self.rotate = True
220
173
 
221
- # # Sync legacy attributes
222
- # self.origX = self.original_x_rotation
223
- # self.origY = self.original_y_rotation
224
-
225
174
  elif event.button() == Qt.RightButton:
226
175
  self.original_x_pos = position.x()
227
176
  self.original_y_pos = position.y()
228
177
  self.translate = True
229
178
 
230
- # # Sync legacy attributes
231
- # self.origXPos = self.original_x_pos
232
- # self.origYPos = self.original_y_pos
233
-
234
179
  def mouseReleaseEvent(self, event) -> None:
235
180
  """
236
181
  Handle mouse button release events to stop rotation or translation.
@@ -262,57 +207,4 @@ class PySideEventHandlingMixin:
262
207
  elif delta < 0:
263
208
  self.model_position.z -= self.zoom_sensitivity
264
209
 
265
- # # Sync legacy attributes
266
- # self.modelPos = self.model_position
267
-
268
210
  self.update()
269
-
270
- def get_camera_state(self) -> dict:
271
- """
272
- Get the current camera state for serialization or debugging.
273
-
274
- Returns:
275
- Dictionary containing current camera state
276
- """
277
- return {
278
- "spin_x_face": self.spin_x_face,
279
- "spin_y_face": self.spin_y_face,
280
- "model_position": [
281
- self.model_position.x,
282
- self.model_position.y,
283
- self.model_position.z,
284
- ],
285
- "rotation_sensitivity": self.rotation_sensitivity,
286
- "translation_sensitivity": self.translation_sensitivity,
287
- "zoom_sensitivity": self.zoom_sensitivity,
288
- }
289
-
290
- def set_camera_state(self, state: dict) -> None:
291
- """
292
- Restore camera state from a dictionary.
293
-
294
- Args:
295
- state: Dictionary containing camera state (from get_camera_state())
296
- """
297
- self.spin_x_face = state.get("spin_x_face", 0)
298
- self.spin_y_face = state.get("spin_y_face", 0)
299
-
300
- pos = state.get("model_position", [0, 0, 0])
301
- # Handle cases where pos might have fewer than 3 elements
302
- x = pos[0] if len(pos) > 0 else 0
303
- y = pos[1] if len(pos) > 1 else 0
304
- z = pos[2] if len(pos) > 2 else 0
305
- self.model_position.set(x, y, z)
306
-
307
- self.rotation_sensitivity = state.get(
308
- "rotation_sensitivity", self.DEFAULT_ROTATION_SENSITIVITY
309
- )
310
- self.translation_sensitivity = state.get(
311
- "translation_sensitivity", self.DEFAULT_TRANSLATION_SENSITIVITY
312
- )
313
- self.zoom_sensitivity = state.get(
314
- "zoom_sensitivity", self.DEFAULT_ZOOM_SENSITIVITY
315
- )
316
-
317
- # # Sync legacy attributes
318
- # self.sync_legacy_attributes()
ncca/ngl/quaternion.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """
2
2
  A simple Quaternion class for use in NCCA Python
3
+ NumPy-based implementation for efficient operations
3
4
  Attributes:
4
5
  s (float): The scalar part of the quaternion.
5
6
  x (float): The x-coordinate of the vector part of the quaternion.
@@ -9,12 +10,14 @@ Attributes:
9
10
 
10
11
  import math
11
12
 
13
+ import numpy as np
14
+
12
15
  from .mat4 import Mat4
13
16
  from .vec3 import Vec3
14
17
 
15
18
 
16
19
  class Quaternion:
17
- __slots__ = ["s", "x", "y", "z"] # fix the attributes to s x,y,z
20
+ __slots__ = ["_data"] # Store as [s, x, y, z]
18
21
 
19
22
  def __init__(self, s: float = 1.0, x: float = 0, y: float = 0, z: float = 0):
20
23
  """
@@ -25,12 +28,8 @@ class Quaternion:
25
28
  x (float): The x-coordinate of the vector part of the quaternion.
26
29
  y (float): The y-coordinate of the vector part of the quaternion.
27
30
  z (float): The z-coordinate of the vector part of the quaternion.
28
-
29
31
  """
30
- self.s = float(s)
31
- self.x = float(x)
32
- self.y = float(y)
33
- self.z = float(z)
32
+ self._data = np.array([float(s), float(x), float(y), float(z)], dtype=np.float64)
34
33
 
35
34
  @staticmethod
36
35
  def from_mat4(mat: "Mat4") -> "Quaternion":
@@ -42,7 +41,6 @@ class Quaternion:
42
41
 
43
42
  Returns:
44
43
  Quaternion: A new Quaternion representing the rotation matrix.
45
-
46
44
  """
47
45
  matrix = mat.get_matrix()
48
46
  T = 1.0 + matrix[0] + matrix[5] + matrix[10]
@@ -58,14 +56,12 @@ class Quaternion:
58
56
  y = (matrix[4] + matrix[1]) / scale
59
57
  z = (matrix[2] + matrix[8]) / scale
60
58
  s = (matrix[6] - matrix[9]) / scale
61
-
62
59
  elif matrix[5] > matrix[10]:
63
60
  scale = math.sqrt(1.0 + matrix[5] - matrix[0] - matrix[10]) * 2.0
64
61
  x = (matrix[4] + matrix[1]) / scale
65
62
  y = 0.25 * scale
66
63
  z = (matrix[9] + matrix[6]) / scale
67
64
  s = (matrix[8] - matrix[2]) / scale
68
-
69
65
  else:
70
66
  scale = math.sqrt(1.0 + matrix[10] - matrix[0] - matrix[5]) * 2.0
71
67
  x = (matrix[8] + matrix[2]) / scale
@@ -97,38 +93,37 @@ class Quaternion:
97
93
  return Quaternion(s, x, y, z)
98
94
 
99
95
  def __add__(self, rhs):
100
- return Quaternion(self.s + rhs.s, self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
96
+ result = Quaternion()
97
+ result._data = self._data + rhs._data
98
+ return result
101
99
 
102
100
  def __iadd__(self, rhs):
103
- self.s += rhs.s
104
- self.x += rhs.x
105
- self.y += rhs.y
106
- self.z += rhs.z
101
+ self._data += rhs._data
107
102
  return self
108
103
 
109
104
  def __sub__(self, rhs):
110
- return Quaternion(self.s - rhs.s, self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
105
+ result = Quaternion()
106
+ result._data = self._data - rhs._data
107
+ return result
111
108
 
112
109
  def __isub__(self, rhs):
113
- return self.__sub__(rhs)
114
-
115
- # def __mul__(self, rhs):
116
- # return Quaternion(
117
- # self.s * rhs.s - self.x * rhs.x - self.y * rhs.y - self.z * rhs.z,
118
- # self.s * rhs.x + self.x * rhs.s + self.y * rhs.z - self.z * rhs.y,
119
- # self.s * rhs.y - self.x * rhs.z + self.y * rhs.s + self.z * rhs.x,
120
- # self.s * rhs.z + self.x * rhs.y - self.y * rhs.x + self.z * rhs.s,
121
- # )
110
+ self._data -= rhs._data
111
+ return self
122
112
 
123
113
  def __mul__(self, rhs):
124
114
  if isinstance(rhs, Quaternion):
115
+ # Quaternion multiplication
116
+ s1, x1, y1, z1 = self._data
117
+ s2, x2, y2, z2 = rhs._data
118
+
125
119
  return Quaternion(
126
- self.s * rhs.s - self.x * rhs.x - self.y * rhs.y - self.z * rhs.z,
127
- self.s * rhs.x + self.x * rhs.s + self.y * rhs.z - self.z * rhs.y,
128
- self.s * rhs.y - self.x * rhs.z + self.y * rhs.s + self.z * rhs.x,
129
- self.s * rhs.z + self.x * rhs.y - self.y * rhs.x + self.z * rhs.s,
120
+ s1 * s2 - x1 * x2 - y1 * y2 - z1 * z2,
121
+ s1 * x2 + x1 * s2 + y1 * z2 - z1 * y2,
122
+ s1 * y2 - x1 * z2 + y1 * s2 + z1 * x2,
123
+ s1 * z2 + x1 * y2 - y1 * x2 + z1 * s2,
130
124
  )
131
125
  elif isinstance(rhs, Vec3):
126
+ # Quaternion-vector multiplication (rotate vector by quaternion)
132
127
  qw = self.s
133
128
  qx = self.x
134
129
  qy = self.y
@@ -138,13 +133,13 @@ class Quaternion:
138
133
  vy = rhs.y
139
134
  vz = rhs.z
140
135
 
141
- # pq
136
+ # pq (quaternion * pure quaternion from vector)
142
137
  pw = -qx * vx - qy * vy - qz * vz
143
138
  px = qw * vx + qy * vz - qz * vy
144
139
  py = qw * vy - qx * vz + qz * vx
145
140
  pz = qw * vz + qx * vy - qy * vx
146
141
 
147
- # pqp*
142
+ # pqp* (result * conjugate of quaternion)
148
143
  return Vec3(
149
144
  -pw * qx + px * qw - py * qz + pz * qy,
150
145
  -pw * qy + px * qz + py * qw - pz * qx,
@@ -152,12 +147,25 @@ class Quaternion:
152
147
  )
153
148
 
154
149
  def normalize(self):
155
- length = math.sqrt(self.s * self.s + self.x * self.x + self.y * self.y + self.z * self.z)
150
+ """Normalize the quaternion to unit length"""
151
+ length = np.linalg.norm(self._data)
156
152
  if length > 0:
157
- self.s /= length
158
- self.x /= length
159
- self.y /= length
160
- self.z /= length
153
+ self._data /= length
154
+
155
+ def length(self):
156
+ """Return the length/magnitude of the quaternion"""
157
+ return np.linalg.norm(self._data)
158
+
159
+ def conjugate(self):
160
+ """Return the conjugate of the quaternion (s, -x, -y, -z)"""
161
+ result = Quaternion()
162
+ result._data = self._data.copy()
163
+ result._data[1:] *= -1 # Negate x, y, z components
164
+ return result
165
+
166
+ def dot(self, rhs):
167
+ """Dot product of two quaternions"""
168
+ return np.dot(self._data, rhs._data)
161
169
 
162
170
  def __str__(self) -> str:
163
171
  """
@@ -165,9 +173,34 @@ class Quaternion:
165
173
 
166
174
  Returns:
167
175
  str: A string representation of the Quaternion.
168
-
169
176
  """
170
177
  return f"Quaternion({self.s}, [{self.x}, {self.y}, {self.z}])"
171
178
 
172
179
  def __repr__(self) -> str:
173
180
  return f"Quaternion({self.s}, [{self.x}, {self.y}, {self.z}])"
181
+
182
+ def to_numpy(self):
183
+ """Return the quaternion as a numpy array [s, x, y, z]"""
184
+ return self._data.copy()
185
+
186
+ def to_list(self):
187
+ """Return the quaternion as a list [s, x, y, z]"""
188
+ return self._data.tolist()
189
+
190
+
191
+ # Helper function to create properties
192
+ def _create_property(index):
193
+ def getter(self):
194
+ return self._data[index]
195
+
196
+ def setter(self, value):
197
+ if not isinstance(value, (int, float, np.float32)):
198
+ raise ValueError("need float or int")
199
+ self._data[index] = value
200
+
201
+ return property(getter, setter)
202
+
203
+
204
+ # Dynamically add properties for s, x, y, z, w
205
+ for i, attr in enumerate(["s", "x", "y", "z"]):
206
+ setattr(Quaternion, attr, _create_property(i))
ncca/ngl/shader.py CHANGED
@@ -111,119 +111,3 @@ class Shader:
111
111
  """
112
112
  self._source = shader_source
113
113
  gl.glShaderSource(self._id, self._source)
114
-
115
-
116
- # class ShaderProgram:
117
- # def __init__(self, name: str, exit_on_error: bool = True):
118
- # self._name = name
119
- # self._exit_on_error = exit_on_error
120
- # self._id = gl.glCreateProgram()
121
- # self._shaders = []
122
- # self._uniforms = {}
123
-
124
- # def attach_shader(self, shader: Shader):
125
- # gl.glAttachShader(self._id, shader._id)
126
- # self._shaders.append(shader)
127
-
128
- # def link(self) -> bool:
129
- # gl.glLinkProgram(self._id)
130
- # if gl.glGetProgramiv(self._id, gl.GL_LINK_STATUS) != gl.GL_TRUE:
131
- # info = gl.glGetProgramInfoLog(self._id)
132
- # print(f"Error linking program {self._name}: {info}")
133
- # if self._exit_on_error:
134
- # exit()
135
- # return False
136
- # return True
137
-
138
- # def use(self):
139
- # gl.glUseProgram(self._id)
140
-
141
- # def get_id(self) -> int:
142
- # return self._id
143
-
144
- # def get_uniform_location(self, name: str) -> int:
145
- # if name not in self._uniforms:
146
- # self._uniforms[name] = gl.glGetUniformLocation(self._id, name)
147
- # return self._uniforms[name]
148
-
149
- # def set_uniform(self, name: str, *value):
150
- # loc = self.get_uniform_location(name)
151
- # if loc != -1:
152
- # if len(value) == 1:
153
- # if isinstance(value[0], int):
154
- # gl.glUniform1i(loc, value[0])
155
- # elif isinstance(value[0], float):
156
- # gl.glUniform1f(loc, value[0])
157
- # else:
158
- # try:
159
- # val = list(value[0])
160
- # if len(val) == 4:
161
- # gl.glUniformMatrix2fv(loc, 1, gl.GL_TRUE, (ctypes.c_float * 4)(*val))
162
- # elif len(val) == 9:
163
- # gl.glUniformMatrix3fv(loc, 1, gl.GL_TRUE, (ctypes.c_float * 9)(*val))
164
- # elif len(val) == 16:
165
- # gl.glUniformMatrix4fv(loc, 1, gl.GL_TRUE, (ctypes.c_float * 16)(*val))
166
- # except TypeError:
167
- # pass
168
- # elif len(value) == 2:
169
- # gl.glUniform2f(loc, *value)
170
- # elif len(value) == 3:
171
- # gl.glUniform3f(loc, *value)
172
- # elif len(value) == 4:
173
- # gl.glUniform4f(loc, *value)
174
-
175
- # def get_uniform_1f(self, name: str) -> float:
176
- # loc = self.get_uniform_location(name)
177
- # if loc != -1:
178
- # result = (ctypes.c_float * 1)()
179
- # gl.glGetUniformfv(self._id, loc, result)
180
- # return result[0]
181
- # return 0.0
182
-
183
- # def get_uniform_2f(self, name: str) -> list[float]:
184
- # loc = self.get_uniform_location(name)
185
- # if loc != -1:
186
- # result = (ctypes.c_float * 2)()
187
- # gl.glGetUniformfv(self._id, loc, result)
188
- # return list(result)
189
- # return [0.0, 0.0]
190
-
191
- # def get_uniform_3f(self, name: str) -> list[float]:
192
- # loc = self.get_uniform_location(name)
193
- # if loc != -1:
194
- # result = (ctypes.c_float * 3)()
195
- # gl.glGetUniformfv(self._id, loc, result)
196
- # return list(result)
197
- # return [0.0, 0.0, 0.0]
198
-
199
- # def get_uniform_4f(self, name: str) -> list[float]:
200
- # loc = self.get_uniform_location(name)
201
- # if loc != -1:
202
- # result = (ctypes.c_float * 4)()
203
- # gl.glGetUniformfv(self._id, loc, result)
204
- # return list(result)
205
- # return [0.0, 0.0, 0.0, 0.0]
206
-
207
- # def get_uniform_mat2(self, name: str) -> list[float]:
208
- # loc = self.get_uniform_location(name)
209
- # if loc != -1:
210
- # result = (ctypes.c_float * 4)()
211
- # gl.glGetUniformfv(self._id, loc, result)
212
- # return list(result)
213
- # return [0.0] * 4
214
-
215
- # def get_uniform_mat3(self, name: str) -> list[float]:
216
- # loc = self.get_uniform_location(name)
217
- # if loc != -1:
218
- # result = (ctypes.c_float * 9)()
219
- # gl.glGetUniformfv(self._id, loc, result)
220
- # return list(result)
221
- # return [0.0] * 9
222
-
223
- # def get_uniform_mat4(self, name: str) -> list[float]:
224
- # loc = self.get_uniform_location(name)
225
- # if loc != -1:
226
- # result = (ctypes.c_float * 16)()
227
- # gl.glGetUniformfv(self._id, loc, result)
228
- # return list(result)
229
- # return [0.0] * 16