ncca-ngl 0.3.5__py3-none-any.whl → 0.5.1__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 +5 -5
  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 +50 -0
  16. ncca/ngl/vec2.py +59 -302
  17. ncca/ngl/vec2_array.py +79 -28
  18. ncca/ngl/vec3.py +60 -350
  19. ncca/ngl/vec3_array.py +76 -23
  20. ncca/ngl/vec4.py +90 -200
  21. ncca/ngl/vec4_array.py +78 -27
  22. ncca/ngl/vector_base.py +548 -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 +33 -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.5.dist-info → ncca_ngl-0.5.1.dist-info}/METADATA +3 -2
  53. ncca_ngl-0.5.1.dist-info/RECORD +105 -0
  54. ncca/ngl/widgets/transformation_widget.py +0 -299
  55. ncca_ngl-0.3.5.dist-info/RECORD +0 -82
  56. {ncca_ngl-0.3.5.dist-info → ncca_ngl-0.5.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,548 @@
1
+ """
2
+ Base class for all vector types providing common functionality and eliminating code duplication.
3
+
4
+ This module implements a generic base class that provides all common vector operations
5
+ while allowing dimension-specific behavior through abstract methods.
6
+ """
7
+
8
+ import ctypes
9
+ import math
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any, ClassVar, Generic, Self, Tuple, TypeVar
12
+
13
+ import numpy as np
14
+
15
+ from .util import clamp, hash_combine
16
+
17
+ # Note this has been changed so I can use python 3.11
18
+ # as renderman want this. Once >3.11 is used we can get rid and just use
19
+ # class VectorBase[T](ABC):
20
+
21
+ T = TypeVar("T", bound="VectorBase")
22
+
23
+
24
+ class VectorBase(ABC, Generic[T]):
25
+ """
26
+ Base class for all vector types providing common functionality.
27
+
28
+ This class implements all common vector operations using numpy for efficiency
29
+ while maintaining the same API as the original vector classes.
30
+
31
+ Attributes:
32
+ DIMENSION: The dimension of the vector (2, 3, or 4)
33
+ COMPONENT_NAMES: Tuple of component names ('x', 'y', 'z', 'w')
34
+ DEFAULT_VALUES: Tuple of default values for each component
35
+ """
36
+
37
+ # Class attributes to be defined by subclasses
38
+ DIMENSION: ClassVar[int]
39
+ COMPONENT_NAMES: ClassVar[Tuple[str, ...]]
40
+ DEFAULT_VALUES: ClassVar[Tuple[float, ...]]
41
+
42
+ def __init__(self, *args: float, **kwargs: float) -> None:
43
+ """Initialize vector with components."""
44
+ if kwargs:
45
+ self._init_from_kwargs(args, kwargs)
46
+ else:
47
+ self._init_from_args(args)
48
+
49
+ def _init_from_kwargs(self, args: tuple[float, ...], kwargs: dict[str, float]) -> None:
50
+ """Initialize vector from keyword arguments."""
51
+ self._validate_component_count(len(args) + len(kwargs))
52
+
53
+ values = list(self.DEFAULT_VALUES)
54
+ self._set_positional_args(values, args)
55
+ self._set_keyword_args(values, kwargs)
56
+
57
+ self._data = np.array(values, dtype=np.float64)
58
+
59
+ def _init_from_args(self, args: tuple[float, ...]) -> None:
60
+ """Initialize vector from positional arguments only."""
61
+ if not args:
62
+ self._data = np.array(self.DEFAULT_VALUES, dtype=np.float64)
63
+ return
64
+
65
+ self._validate_component_count(len(args))
66
+
67
+ values = list(self.DEFAULT_VALUES)
68
+ self._set_positional_args(values, args)
69
+
70
+ self._data = np.array(values, dtype=np.float64)
71
+
72
+ def _validate_component_count(self, count: int) -> None:
73
+ """Validate the number of components doesn't exceed dimension."""
74
+ if count > self.DIMENSION:
75
+ raise ValueError(f"{self.__class__.__name__} requires at most {self.DIMENSION} components")
76
+
77
+ def _set_positional_args(self, values: list[float], args: tuple[float, ...]) -> None:
78
+ """Set positional argument values."""
79
+ for i, arg in enumerate(args):
80
+ values[i] = float(arg)
81
+
82
+ def _set_keyword_args(self, values: list[float], kwargs: dict[str, float]) -> None:
83
+ """Set keyword argument values."""
84
+ for key, value in kwargs.items():
85
+ if key in self.COMPONENT_NAMES:
86
+ idx = self.COMPONENT_NAMES.index(key)
87
+ values[idx] = float(value)
88
+ else:
89
+ raise ValueError(f"Unknown component name: {key}")
90
+
91
+ @classmethod
92
+ def sizeof(cls) -> int:
93
+ """Return the size of the vector in bytes (for OpenGL compatibility)."""
94
+ return cls.DIMENSION * ctypes.sizeof(ctypes.c_float)
95
+
96
+ def __iter__(self):
97
+ """
98
+ Make the vector class iterable.
99
+
100
+ Yields:
101
+ float: The components of the vector.
102
+ """
103
+ for i in range(self.DIMENSION):
104
+ yield self._data[i]
105
+
106
+ def __getitem__(self, index: int) -> float:
107
+ """
108
+ Get the component of the vector at the given index.
109
+
110
+ Args:
111
+ index (int): The index of the component.
112
+
113
+ Returns:
114
+ float: The value of the component at the given index.
115
+
116
+ Raises:
117
+ IndexError: If the index is out of range.
118
+ """
119
+ if index < 0 or index >= self.DIMENSION:
120
+ valid_indices = ", ".join(str(i) for i in range(self.DIMENSION))
121
+ raise IndexError(f"Index out of range. Valid indices are {valid_indices}.")
122
+ return self._data[index]
123
+
124
+ def __hash__(self) -> int:
125
+ """
126
+ Compute hash for use in sets and dictionaries.
127
+
128
+ Returns:
129
+ int: Hash value for the vector.
130
+ """
131
+ # Use 32-bit float element hashes, then combine
132
+ seed = 0
133
+ for v in self._data:
134
+ # ensure 32-bit float semantics
135
+ h = hash(float(np.float32(v)))
136
+ seed = hash_combine(seed, h)
137
+ return seed
138
+
139
+ def copy(self) -> Self:
140
+ """
141
+ Create a copy of the vector.
142
+
143
+ Returns:
144
+ VectorBase: A new vector instance with the same values.
145
+ """
146
+ return self.__class__(*self._data)
147
+
148
+ def __add__(self, rhs: Self) -> Self:
149
+ """
150
+ Vector addition a+b.
151
+
152
+ Args:
153
+ rhs (VectorBase): The right-hand side vector to add.
154
+
155
+ Returns:
156
+ VectorBase: A new vector that is the result of adding this vector and the rhs vector.
157
+ """
158
+ if not isinstance(rhs, self.__class__):
159
+ raise ValueError(f"Can only add {self.__class__.__name__} to {self.__class__.__name__}")
160
+ result = self.__class__()
161
+ result._data = self._data + rhs._data
162
+ return result
163
+
164
+ def __iadd__(self, rhs: Self) -> Self:
165
+ """
166
+ Vector addition a+=b.
167
+
168
+ Args:
169
+ rhs (VectorBase): The right-hand side vector to add.
170
+
171
+ Returns:
172
+ VectorBase: Returns this vector after adding the rhs vector.
173
+ """
174
+ if not isinstance(rhs, self.__class__):
175
+ raise ValueError(f"Can only add {self.__class__.__name__} to {self.__class__.__name__}")
176
+ self._data += rhs._data
177
+ return self
178
+
179
+ def __sub__(self, rhs: Self) -> Self:
180
+ """
181
+ Vector subtraction a-b.
182
+
183
+ Args:
184
+ rhs (VectorBase): The right-hand side vector to subtract.
185
+
186
+ Returns:
187
+ VectorBase: A new vector that is the result of subtracting this vector and the rhs vector.
188
+ """
189
+ if not isinstance(rhs, self.__class__):
190
+ raise ValueError(f"Can only subtract {self.__class__.__name__} from {self.__class__.__name__}")
191
+ result = self.__class__()
192
+ result._data = self._data - rhs._data
193
+ return result
194
+
195
+ def __isub__(self, rhs: Self) -> Self:
196
+ """
197
+ Vector subtraction a-=b.
198
+
199
+ Args:
200
+ rhs (VectorBase): The right-hand side vector to subtract.
201
+
202
+ Returns:
203
+ VectorBase: Returns this vector after subtracting the rhs vector.
204
+ """
205
+ if not isinstance(rhs, self.__class__):
206
+ raise ValueError(f"Can only subtract {self.__class__.__name__} from {self.__class__.__name__}")
207
+ self._data -= rhs._data
208
+ return self
209
+
210
+ def __eq__(self, rhs: Any) -> bool:
211
+ """
212
+ Vector comparison a==b using numpy.allclose.
213
+
214
+ Args:
215
+ rhs (VectorBase): The right-hand side vector to compare.
216
+
217
+ Returns:
218
+ bool: True if the vectors are close, False otherwise.
219
+ NotImplemented: If the right-hand side is not a VectorBase.
220
+ """
221
+ if not isinstance(rhs, self.__class__):
222
+ return NotImplemented
223
+ return np.allclose(self._data, rhs._data)
224
+
225
+ def __ne__(self, rhs: Any) -> bool:
226
+ """
227
+ Vector comparison a!=b using numpy.allclose.
228
+
229
+ Args:
230
+ rhs (VectorBase): The right-hand side vector to compare.
231
+
232
+ Returns:
233
+ bool: True if the vectors are not close, False otherwise.
234
+ NotImplemented: If the right-hand side is not a VectorBase.
235
+ """
236
+ if not isinstance(rhs, self.__class__):
237
+ return NotImplemented
238
+ return not np.allclose(self._data, rhs._data)
239
+
240
+ def __neg__(self) -> Self:
241
+ """
242
+ Negate a vector -a.
243
+
244
+ Returns:
245
+ VectorBase: The negated vector.
246
+ """
247
+ result = self.__class__()
248
+ result._data = -self._data
249
+ return result
250
+
251
+ def __mul__(self, rhs: float | int) -> Self:
252
+ """
253
+ Piecewise scalar multiplication.
254
+
255
+ Args:
256
+ rhs (float): The scalar to multiply by.
257
+
258
+ Returns:
259
+ VectorBase: A new vector that is the result of multiplying this vector by the scalar.
260
+
261
+ Raises:
262
+ ValueError: If the right-hand side is not a float or int.
263
+ """
264
+ if isinstance(rhs, (float, int)):
265
+ result = self.__class__()
266
+ result._data = self._data * rhs
267
+ return result
268
+ else:
269
+ raise ValueError(f"Can only do piecewise multiplication with a scalar, got {type(rhs)}")
270
+
271
+ def __rmul__(self, rhs: float | int) -> Self:
272
+ """
273
+ Piecewise scalar multiplication (right operand).
274
+
275
+ Args:
276
+ rhs (float): The scalar to multiply by.
277
+
278
+ Returns:
279
+ VectorBase: A new vector that is the result of multiplying this vector by the scalar.
280
+ """
281
+ return self * rhs
282
+
283
+ def __truediv__(self, rhs: float | int | Self) -> Self:
284
+ """
285
+ Piecewise scalar or vector division.
286
+
287
+ Args:
288
+ rhs (Union[float, int, VectorBase]): The scalar or vector to divide by.
289
+
290
+ Returns:
291
+ VectorBase: A new vector that is the result of division.
292
+
293
+ Raises:
294
+ ZeroDivisionError: If division by zero is attempted.
295
+ ValueError: If the right-hand side is not a valid type.
296
+ """
297
+ if isinstance(rhs, (float, int)):
298
+ if rhs == 0.0:
299
+ raise ZeroDivisionError("division by zero")
300
+ result = self.__class__()
301
+ result._data = self._data / rhs
302
+ return result
303
+ elif isinstance(rhs, self.__class__):
304
+ if np.any(np.isclose(rhs._data, 0.0)):
305
+ raise ZeroDivisionError("division by zero")
306
+ result = self.__class__()
307
+ result._data = self._data / rhs._data
308
+ return result
309
+ else:
310
+ raise ValueError(
311
+ f"Can only do piecewise division with a scalar or {self.__class__.__name__}, got {type(rhs)}"
312
+ )
313
+
314
+ def dot(self, rhs: Self) -> float:
315
+ """
316
+ Dot product of two vectors a.b.
317
+
318
+ Args:
319
+ rhs (VectorBase): The right-hand side vector to dot product with.
320
+
321
+ Returns:
322
+ float: The dot product of the two vectors.
323
+ """
324
+ if not isinstance(rhs, self.__class__):
325
+ raise ValueError(f"Can only compute dot product with {self.__class__.__name__}")
326
+ return np.dot(self._data, rhs._data)
327
+
328
+ def length(self) -> float:
329
+ """
330
+ Length of vector.
331
+
332
+ Returns:
333
+ float: The length of the vector.
334
+ """
335
+ return np.linalg.norm(self._data)
336
+
337
+ def length_squared(self) -> float:
338
+ """
339
+ Length of vector squared (sometimes used to avoid the sqrt for performance).
340
+
341
+ Returns:
342
+ float: The length of the vector squared.
343
+ """
344
+ return np.dot(self._data, self._data)
345
+
346
+ def inner(self, rhs: Self) -> float:
347
+ """
348
+ Inner product of two vectors a.b (alias for dot product).
349
+
350
+ Args:
351
+ rhs (VectorBase): The right-hand side vector to inner product with.
352
+
353
+ Returns:
354
+ float: The inner product of the two vectors.
355
+ """
356
+ return self.dot(rhs)
357
+
358
+ def normalize(self) -> Self:
359
+ """
360
+ Normalize the vector to unit length.
361
+
362
+ Returns:
363
+ VectorBase: This vector after normalization.
364
+
365
+ Raises:
366
+ ZeroDivisionError: If the length of the vector is zero.
367
+ """
368
+ vector_length = self.length()
369
+ if math.isclose(vector_length, 0.0):
370
+ raise ZeroDivisionError(
371
+ f"{self.__class__.__name__}.normalize: length is zero, most likely calling normalize on a zero vector"
372
+ )
373
+ self._data /= vector_length
374
+ return self
375
+
376
+ def null(self) -> None:
377
+ """Set the vector to zero."""
378
+ self._data[:] = 0.0
379
+
380
+ def clamp(self, low: float, high: float) -> None:
381
+ """
382
+ Clamp the vector to a range.
383
+
384
+ Args:
385
+ low (float): The low end of the range.
386
+ high (float): The high end of the range.
387
+ """
388
+ for i in range(self.DIMENSION):
389
+ self._data[i] = clamp(self._data[i], low, high)
390
+
391
+ def to_list(self) -> list:
392
+ """
393
+ Convert vector to list.
394
+
395
+ Returns:
396
+ list: List of vector components.
397
+ """
398
+ return self._data.tolist()
399
+
400
+ def to_numpy(self) -> np.ndarray:
401
+ """
402
+ Convert vector to numpy array.
403
+
404
+ Returns:
405
+ np.ndarray: Copy of the vector as numpy array.
406
+ """
407
+ return self._data.copy()
408
+
409
+ def to_tuple(self) -> tuple:
410
+ """
411
+ Convert vector to tuple.
412
+
413
+ Returns:
414
+ tuple: Tuple of vector components.
415
+ """
416
+ return tuple(self._data)
417
+
418
+ # Abstract methods that must be implemented by subclasses
419
+ @abstractmethod
420
+ def cross(self, rhs: Self) -> Self | float:
421
+ """
422
+ Cross product of two vectors a x b.
423
+
424
+ Args:
425
+ rhs (VectorBase): The right-hand side vector to cross product with.
426
+
427
+ Returns:
428
+ Union[VectorBase, float]: The cross product (scalar for 2D, vector for 3D/4D).
429
+ """
430
+ pass # pragma: no cover
431
+
432
+ @abstractmethod
433
+ def reflect(self, n: Self) -> Self:
434
+ """
435
+ Reflect a vector about a normal.
436
+
437
+ Args:
438
+ n (VectorBase): The normal to reflect about.
439
+
440
+ Returns:
441
+ VectorBase: A new vector that is the result of reflecting this vector about the normal.
442
+ """
443
+ pass # pragma: no cover
444
+
445
+ @abstractmethod
446
+ def outer(self, rhs: Self) -> Any:
447
+ """
448
+ Outer product of two vectors a x b.
449
+
450
+ Args:
451
+ rhs (VectorBase): The right-hand side vector to outer product with.
452
+
453
+ Returns:
454
+ Any: A matrix that is the result of the outer product.
455
+ """
456
+ pass # pragma: no cover
457
+
458
+ @abstractmethod
459
+ def __matmul__(self, rhs: Any) -> Self:
460
+ """
461
+ Matrix multiplication Vector @ Matrix.
462
+
463
+ Args:
464
+ rhs (Any): The matrix to multiply by.
465
+
466
+ Returns:
467
+ VectorBase: A new vector that is the result of multiplying this vector by the matrix.
468
+ """
469
+ pass # pragma: no cover
470
+
471
+ @abstractmethod
472
+ def set(self, *args: float) -> None:
473
+ """
474
+ Set the components of the vector.
475
+
476
+ Args:
477
+ *args: Component values to set.
478
+
479
+ Raises:
480
+ ValueError: If wrong number of arguments is provided or they are not floats.
481
+ """
482
+ pass # pragma: no cover
483
+
484
+ @abstractmethod
485
+ def __repr__(self) -> str:
486
+ """Object representation for debugging."""
487
+ pass # pragma: no cover
488
+
489
+ @abstractmethod
490
+ def __str__(self) -> str:
491
+ """String representation of the vector."""
492
+ pass # pragma: no cover
493
+
494
+ def __getattr__(self, name: str):
495
+ """
496
+ Handle access to non-existent attributes.
497
+
498
+ Args:
499
+ name (str): The name of the attribute.
500
+
501
+ Raises:
502
+ AttributeError: If the attribute does not exist.
503
+ """
504
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
505
+
506
+ def __setattr__(self, name: str, value: Any) -> None:
507
+ """
508
+ Control attribute setting to prevent non-component attributes.
509
+
510
+ Args:
511
+ name: The attribute name to set
512
+ value: The value to set
513
+
514
+ Raises:
515
+ AttributeError: If trying to set non-component attributes
516
+ """
517
+ # Allow setting internal _data
518
+ if name == "_data":
519
+ super().__setattr__(name, value)
520
+ # Allow setting component properties (x, y, z, w) during initialization
521
+ elif name in self.COMPONENT_NAMES:
522
+ super().__setattr__(name, value)
523
+ else:
524
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
525
+
526
+
527
+ def _create_properties(cls: type) -> None:
528
+ """
529
+ Dynamically add properties for vector components.
530
+
531
+ Args:
532
+ cls: The vector class to add properties to.
533
+ """
534
+
535
+ def make_property(index: int):
536
+ def getter(self):
537
+ return self._data[index]
538
+
539
+ def setter(self, value):
540
+ if not isinstance(value, (int, float, np.float32)):
541
+ raise ValueError("need float or int")
542
+ self._data[index] = float(value)
543
+
544
+ return property(getter, setter)
545
+
546
+ # Add properties for each component (x, y, z, w)
547
+ for i, attr in enumerate(cls.COMPONENT_NAMES):
548
+ setattr(cls, attr, make_property(i))
@@ -0,0 +1,20 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("ncca-ngl") # pragma: no cover
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0" # pragma: no cover
7
+
8
+ __author__ = "Jon Macey jmacey@bournemouth.ac.uk"
9
+ __license__ = "MIT"
10
+
11
+ from .pipeline_factory import PipelineFactory, PipelineType
12
+ from .webgpu_constants import NGLToWebGPU
13
+ from .webgpu_widget import WebGPUWidget
14
+
15
+ __all__ = [
16
+ "WebGPUWidget",
17
+ "NGLToWebGPU",
18
+ "PipelineFactory",
19
+ "PipelineType",
20
+ ]