Miscellaneous Classes

Auto-generated API reference. For a guided introduction to the vector array classes see the Vector Arrays tutorial.

Random

Static class for generating random numbers and vectors.

Source code in ncca/ngl/random.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
class Random:
    """Static class for generating random numbers and vectors."""

    _float_generators: dict[str, Callable[[], float]] = {
        "RandomFloat": lambda: random.uniform(-1.0, 1.0),
        "RandomPositiveFloat": lambda: random.uniform(0.0, 1.0),
    }

    _int_generators: dict[str, Callable[[], int]] = {}

    @staticmethod
    def set_seed() -> None:
        """Set the seed using std::time(NULL)."""
        random.seed(int(time.time()))

    @staticmethod
    def set_seed_value(value: int) -> None:
        """Set the seed using a param value.

        Args:
            value (int): the seed value
        """
        random.seed(value)

    @staticmethod
    def get_float_from_generator_name(name: str) -> float:
        """Gets a pre-generated float value for a genetator.

        Args:
            name (str): the name of the generator to use for the number

        Returns:
            a random number created by the generator or 0 if the generator is not found
        """
        if name in Random._float_generators:
            return Random._float_generators[name]()
        return 0.0

    @staticmethod
    def get_int_from_generator_name(name: str) -> int:
        """Gets a pre-generated int value for a genetator.

        Args:
            name (str): the name of the generator to use for the number

        Returns:
            a random number created by the generator or 0 if the generator is not found
        """
        if name in Random._int_generators:
            return Random._int_generators[name]()
        return 0

    @staticmethod
    def add_int_generator(name: str, generator: Callable[[], int]) -> None:
        """Add a generator to the int generators.

        Args:
            name (str): the name of the generator to use for the number
            generator: the generator to add, should be a callable function
        """
        Random._int_generators[name] = generator

    @staticmethod
    def add_float_generator(name: str, generator: Callable[[], float]) -> None:
        """Add a generator to the float generators.

        Args:
            name (str): the name of the generator to use for the number
            generator: the generator to add, should be a callable function
        """
        Random._float_generators[name] = generator

    @staticmethod
    def get_random_vec4() -> Vec4:
        """Get a random vector with componets ranged from +/- 1."""
        gen = Random._float_generators["RandomFloat"]
        return Vec4(gen(), gen(), gen(), 0.0)

    @staticmethod
    def get_random_colour4() -> Vec4:
        """Get a random colour with components ranged from 0-1."""
        gen = Random._float_generators["RandomPositiveFloat"]
        return Vec4(gen(), gen(), gen(), 1.0)

    @staticmethod
    def get_random_colour3() -> Vec3:
        """Get a random colour with components ranged from 0-1."""
        gen = Random._float_generators["RandomPositiveFloat"]
        return Vec3(gen(), gen(), gen())

    @staticmethod
    def get_random_normalized_vec4() -> Vec4:
        """Get a random vector with componets ranged from +/- 1 and Normalized."""
        gen = Random._float_generators["RandomFloat"]
        v = Vec4(gen(), gen(), gen(), 0.0)
        v = v.normalized()
        return v

    @staticmethod
    def get_random_vec3() -> Vec3:
        """Get a random vector with componets ranged from +/- 1."""
        gen = Random._float_generators["RandomFloat"]
        return Vec3(gen(), gen(), gen())

    @staticmethod
    def get_random_normalized_vec3() -> Vec3:
        """Get a random vector with componets ranged from +/- 1 and Normalized."""
        gen = Random._float_generators["RandomFloat"]
        v = Vec3(gen(), gen(), gen())
        v = v.normalized()
        return v

    @staticmethod
    def get_random_vec2() -> Vec2:
        """Get a random vector with componets ranged from +/- 1."""
        gen = Random._float_generators["RandomFloat"]
        return Vec2(gen(), gen())

    @staticmethod
    def get_random_normalized_vec2() -> Vec2:
        """Get a random vector with componets ranged from +/- 1 and Normalized."""
        gen = Random._float_generators["RandomFloat"]
        v = Vec2(gen(), gen())
        v = v.normalized()
        return v

    @staticmethod
    def get_random_point(
        x_range: float = 1.0, y_range: float = 1.0, z_range: float = 1.0
    ) -> Vec3:
        """Get a random point in 3D space defaults to +/- 1 else user defined range.

        Args:
            x_range (float): the +/-x range
            y_range (float): the +/-y range
            z_range (float): the +/-z range

        Returns:
            a random point
        """
        gen = Random._float_generators["RandomFloat"]
        return Vec3(gen() * x_range, gen() * y_range, gen() * z_range)

    @staticmethod
    def random_number(mult: float = 1.0) -> float:
        """A replacement for the old RandomNumber func, a convinience function.

        Args:
            mult (float): an optional multiplyer for the output

        Returns:
            (uniform_random(-1-0-+1) * mult)
        """
        gen = Random._float_generators["RandomFloat"]
        return gen() * mult

    @staticmethod
    def random_positive_number(mult: float = 1.0) -> float:
        """A replacement for the old ReandomPosNum, a convinience function.

        Args:
            mult (float): an optional multiplyer for the output

        Returns:
            (uniform_random(0-1) * mult)
        """
        gen = Random._float_generators["RandomPositiveFloat"]
        return gen() * mult

add_float_generator(name, generator) staticmethod

Add a generator to the float generators.

Parameters:
  • name (str) –

    the name of the generator to use for the number

  • generator (Callable[[], float]) –

    the generator to add, should be a callable function

Source code in ncca/ngl/random.py
74
75
76
77
78
79
80
81
82
@staticmethod
def add_float_generator(name: str, generator: Callable[[], float]) -> None:
    """Add a generator to the float generators.

    Args:
        name (str): the name of the generator to use for the number
        generator: the generator to add, should be a callable function
    """
    Random._float_generators[name] = generator

add_int_generator(name, generator) staticmethod

Add a generator to the int generators.

Parameters:
  • name (str) –

    the name of the generator to use for the number

  • generator (Callable[[], int]) –

    the generator to add, should be a callable function

Source code in ncca/ngl/random.py
64
65
66
67
68
69
70
71
72
@staticmethod
def add_int_generator(name: str, generator: Callable[[], int]) -> None:
    """Add a generator to the int generators.

    Args:
        name (str): the name of the generator to use for the number
        generator: the generator to add, should be a callable function
    """
    Random._int_generators[name] = generator

get_float_from_generator_name(name) staticmethod

Gets a pre-generated float value for a genetator.

Parameters:
  • name (str) –

    the name of the generator to use for the number

Returns:
  • float

    a random number created by the generator or 0 if the generator is not found

Source code in ncca/ngl/random.py
36
37
38
39
40
41
42
43
44
45
46
47
48
@staticmethod
def get_float_from_generator_name(name: str) -> float:
    """Gets a pre-generated float value for a genetator.

    Args:
        name (str): the name of the generator to use for the number

    Returns:
        a random number created by the generator or 0 if the generator is not found
    """
    if name in Random._float_generators:
        return Random._float_generators[name]()
    return 0.0

get_int_from_generator_name(name) staticmethod

Gets a pre-generated int value for a genetator.

Parameters:
  • name (str) –

    the name of the generator to use for the number

Returns:
  • int

    a random number created by the generator or 0 if the generator is not found

Source code in ncca/ngl/random.py
50
51
52
53
54
55
56
57
58
59
60
61
62
@staticmethod
def get_int_from_generator_name(name: str) -> int:
    """Gets a pre-generated int value for a genetator.

    Args:
        name (str): the name of the generator to use for the number

    Returns:
        a random number created by the generator or 0 if the generator is not found
    """
    if name in Random._int_generators:
        return Random._int_generators[name]()
    return 0

get_random_colour3() staticmethod

Get a random colour with components ranged from 0-1.

Source code in ncca/ngl/random.py
 96
 97
 98
 99
100
@staticmethod
def get_random_colour3() -> Vec3:
    """Get a random colour with components ranged from 0-1."""
    gen = Random._float_generators["RandomPositiveFloat"]
    return Vec3(gen(), gen(), gen())

get_random_colour4() staticmethod

Get a random colour with components ranged from 0-1.

Source code in ncca/ngl/random.py
90
91
92
93
94
@staticmethod
def get_random_colour4() -> Vec4:
    """Get a random colour with components ranged from 0-1."""
    gen = Random._float_generators["RandomPositiveFloat"]
    return Vec4(gen(), gen(), gen(), 1.0)

get_random_normalized_vec2() staticmethod

Get a random vector with componets ranged from +/- 1 and Normalized.

Source code in ncca/ngl/random.py
130
131
132
133
134
135
136
@staticmethod
def get_random_normalized_vec2() -> Vec2:
    """Get a random vector with componets ranged from +/- 1 and Normalized."""
    gen = Random._float_generators["RandomFloat"]
    v = Vec2(gen(), gen())
    v = v.normalized()
    return v

get_random_normalized_vec3() staticmethod

Get a random vector with componets ranged from +/- 1 and Normalized.

Source code in ncca/ngl/random.py
116
117
118
119
120
121
122
@staticmethod
def get_random_normalized_vec3() -> Vec3:
    """Get a random vector with componets ranged from +/- 1 and Normalized."""
    gen = Random._float_generators["RandomFloat"]
    v = Vec3(gen(), gen(), gen())
    v = v.normalized()
    return v

get_random_normalized_vec4() staticmethod

Get a random vector with componets ranged from +/- 1 and Normalized.

Source code in ncca/ngl/random.py
102
103
104
105
106
107
108
@staticmethod
def get_random_normalized_vec4() -> Vec4:
    """Get a random vector with componets ranged from +/- 1 and Normalized."""
    gen = Random._float_generators["RandomFloat"]
    v = Vec4(gen(), gen(), gen(), 0.0)
    v = v.normalized()
    return v

get_random_point(x_range=1.0, y_range=1.0, z_range=1.0) staticmethod

Get a random point in 3D space defaults to +/- 1 else user defined range.

Parameters:
  • x_range (float, default: 1.0 ) –

    the +/-x range

  • y_range (float, default: 1.0 ) –

    the +/-y range

  • z_range (float, default: 1.0 ) –

    the +/-z range

Returns:
  • Vec3

    a random point

Source code in ncca/ngl/random.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
@staticmethod
def get_random_point(
    x_range: float = 1.0, y_range: float = 1.0, z_range: float = 1.0
) -> Vec3:
    """Get a random point in 3D space defaults to +/- 1 else user defined range.

    Args:
        x_range (float): the +/-x range
        y_range (float): the +/-y range
        z_range (float): the +/-z range

    Returns:
        a random point
    """
    gen = Random._float_generators["RandomFloat"]
    return Vec3(gen() * x_range, gen() * y_range, gen() * z_range)

get_random_vec2() staticmethod

Get a random vector with componets ranged from +/- 1.

Source code in ncca/ngl/random.py
124
125
126
127
128
@staticmethod
def get_random_vec2() -> Vec2:
    """Get a random vector with componets ranged from +/- 1."""
    gen = Random._float_generators["RandomFloat"]
    return Vec2(gen(), gen())

get_random_vec3() staticmethod

Get a random vector with componets ranged from +/- 1.

Source code in ncca/ngl/random.py
110
111
112
113
114
@staticmethod
def get_random_vec3() -> Vec3:
    """Get a random vector with componets ranged from +/- 1."""
    gen = Random._float_generators["RandomFloat"]
    return Vec3(gen(), gen(), gen())

get_random_vec4() staticmethod

Get a random vector with componets ranged from +/- 1.

Source code in ncca/ngl/random.py
84
85
86
87
88
@staticmethod
def get_random_vec4() -> Vec4:
    """Get a random vector with componets ranged from +/- 1."""
    gen = Random._float_generators["RandomFloat"]
    return Vec4(gen(), gen(), gen(), 0.0)

random_number(mult=1.0) staticmethod

A replacement for the old RandomNumber func, a convinience function.

Parameters:
  • mult (float, default: 1.0 ) –

    an optional multiplyer for the output

Returns:
  • float

    (uniform_random(-1-0-+1) * mult)

Source code in ncca/ngl/random.py
155
156
157
158
159
160
161
162
163
164
165
166
@staticmethod
def random_number(mult: float = 1.0) -> float:
    """A replacement for the old RandomNumber func, a convinience function.

    Args:
        mult (float): an optional multiplyer for the output

    Returns:
        (uniform_random(-1-0-+1) * mult)
    """
    gen = Random._float_generators["RandomFloat"]
    return gen() * mult

random_positive_number(mult=1.0) staticmethod

A replacement for the old ReandomPosNum, a convinience function.

Parameters:
  • mult (float, default: 1.0 ) –

    an optional multiplyer for the output

Returns:
  • float

    (uniform_random(0-1) * mult)

Source code in ncca/ngl/random.py
168
169
170
171
172
173
174
175
176
177
178
179
@staticmethod
def random_positive_number(mult: float = 1.0) -> float:
    """A replacement for the old ReandomPosNum, a convinience function.

    Args:
        mult (float): an optional multiplyer for the output

    Returns:
        (uniform_random(0-1) * mult)
    """
    gen = Random._float_generators["RandomPositiveFloat"]
    return gen() * mult

set_seed() staticmethod

Set the seed using std::time(NULL).

Source code in ncca/ngl/random.py
22
23
24
25
@staticmethod
def set_seed() -> None:
    """Set the seed using std::time(NULL)."""
    random.seed(int(time.time()))

set_seed_value(value) staticmethod

Set the seed using a param value.

Parameters:
  • value (int) –

    the seed value

Source code in ncca/ngl/random.py
27
28
29
30
31
32
33
34
@staticmethod
def set_seed_value(value: int) -> None:
    """Set the seed using a param value.

    Args:
        value (int): the seed value
    """
    random.seed(value)

PySideEventHandlingMixin

Mixin class providing standard event handling for PyNGL applications.

This mixin provides common functionality for: - Mouse-based camera control (rotation with left button, translation with right button) - Keyboard shortcuts (wireframe/solid mode, reset, escape) - Mouse wheel zooming

Classes using this mixin should call setup_event_handling() in their init method.

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
class PySideEventHandlingMixin:
    """Mixin class providing standard event handling for PyNGL applications.

    This mixin provides common functionality for:
    - Mouse-based camera control (rotation with left button, translation with right button)
    - Keyboard shortcuts (wireframe/solid mode, reset, escape)
    - Mouse wheel zooming

    Classes using this mixin should call setup_event_handling() in their __init__ method.
    """

    # Default sensitivity values
    DEFAULT_ROTATION_SENSITIVITY = 0.5
    DEFAULT_TRANSLATION_SENSITIVITY = 0.01
    DEFAULT_ZOOM_SENSITIVITY = 0.1

    def setup_event_handling(
        self,
        rotation_sensitivity: float = DEFAULT_ROTATION_SENSITIVITY,
        translation_sensitivity: float = DEFAULT_TRANSLATION_SENSITIVITY,
        zoom_sensitivity: float = DEFAULT_ZOOM_SENSITIVITY,
        initial_position: Vec3 = None,
        handle_key_shortcuts: bool = True,
    ) -> None:
        """Initialize event handling attributes.

        Args:
            rotation_sensitivity: Mouse sensitivity for rotation (default: 0.5)
            translation_sensitivity: Mouse sensitivity for translation (default: 0.01)
            zoom_sensitivity: Mouse wheel sensitivity for zooming (default: 0.1)
            initial_position: Initial model position (default: Vec3(0,0,0))
            handle_key_shortcuts: Whether the mixin handles Escape, W, S and Space
                (default: True). Pass False when the application owns its own
                keyboard, and every key press is passed on to the parent instead.
        """
        # Mouse control state
        self.rotate: bool = False
        self.translate: bool = False

        # Whether the mixin's own keyboard shortcuts are live
        self.handle_key_shortcuts: bool = handle_key_shortcuts

        # Set by the W and S shortcuts so that a paintGL which sets the polygon
        # mode itself has something to read, see keyPressEvent. Only defaulted
        # if the application has not already made it its own, as several do.
        if not hasattr(self, "wireframe"):
            self.wireframe: bool = False

        # Rotation state
        self.spin_x_face: int = 0
        self.spin_y_face: int = 0

        # Mouse position tracking for rotation
        self.original_x_rotation: float = 0.0
        self.original_y_rotation: float = 0.0

        # Mouse position tracking for translation
        self.original_x_pos: float = 0.0
        self.original_y_pos: float = 0.0

        # Model position and sensitivity settings
        self.model_position: Vec3 = initial_position or Vec3(0, 0, 0)
        self.rotation_sensitivity: float = rotation_sensitivity
        self.translation_sensitivity: float = translation_sensitivity
        self.zoom_sensitivity: float = zoom_sensitivity

        self.INCREMENT = self.translation_sensitivity
        self.ZOOM = self.zoom_sensitivity

    def reset_camera(self) -> None:
        """Reset camera rotation and model position to defaults."""
        self.spin_x_face = 0
        self.spin_y_face = 0
        self.model_position.set(0, 0, 0)

    def close_target(self) -> object:
        """The thing the Escape shortcut should close.

        The mixin is used two ways round. Most applications mix it into a
        QOpenGLWindow, which is a QWindow and is already the top level thing, so
        closing it is closing the application. A few mix it into a QOpenGLWidget
        sitting inside a QMainWindow alongside a control panel, and there
        closing the widget only hides the viewport and leaves the rest of the
        window behind, which is never what Escape is meant to do.

        QWidget has window() to walk up to the top level; QWindow has no such
        method at all, so this cannot simply be self.window().

        Returns:
            The top level window if there is one to walk up to, else self.
        """
        window = getattr(self, "window", None)
        return window() if callable(window) else self

    def keyPressEvent(self, event: QKeyEvent) -> None:
        """Handle keyboard press events with common shortcuts.

        Shortcuts:
        - Escape: Close the window this is in
        - W: Switch to wireframe mode
        - S: Switch to solid fill mode
        - Space: Reset camera rotation and position

        All of these are off when setup_event_handling was given
        handle_key_shortcuts=False, in which case every key is passed on
        untouched for the parent to deal with.

        Args:
            event: The QKeyEvent object
        """
        if not getattr(self, "handle_key_shortcuts", True):
            super().keyPressEvent(event)
            return

        key = event.key()

        if key == Qt.Key_Escape:
            self.close_target().close()
        elif key == Qt.Key_W:
            self.wireframe = True
            gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
        elif key == Qt.Key_S:
            self.wireframe = False
            gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
        elif key == Qt.Key_Space:
            self.reset_camera()
        else:
            # Let subclasses handle other keys
            super().keyPressEvent(event)
            return

        self.update()

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        """Handle mouse movement for camera control.

        - Left button: Rotate the scene
        - Right button: Translate (pan) the scene

        Args:
            event: The QMouseEvent object
        """
        position = event.position()

        # Handle rotation with left mouse button
        if self.rotate and event.buttons() == Qt.LeftButton:
            diff_x = position.x() - self.original_x_rotation
            diff_y = position.y() - self.original_y_rotation

            self.spin_x_face += int(self.rotation_sensitivity * diff_y)
            self.spin_y_face += int(self.rotation_sensitivity * diff_x)

            self.original_x_rotation = position.x()
            self.original_y_rotation = position.y()

            self.update()

        # Handle translation with right mouse button
        elif self.translate and event.buttons() == Qt.RightButton:
            diff_x = int(position.x() - self.original_x_pos)
            diff_y = int(position.y() - self.original_y_pos)

            self.original_x_pos = position.x()
            self.original_y_pos = position.y()

            self.model_position.x += self.translation_sensitivity * diff_x
            self.model_position.y -= self.translation_sensitivity * diff_y

            self.update()

    def mousePressEvent(self, event: QMouseEvent) -> None:
        """Handle mouse button press events to initiate rotation or translation.

        - Left button: Start rotation mode
        - Right button: Start translation mode

        Args:
            event: The QMouseEvent object
        """
        position = event.position()

        if event.button() == Qt.LeftButton:
            self.original_x_rotation = position.x()
            self.original_y_rotation = position.y()
            self.rotate = True

        elif event.button() == Qt.RightButton:
            self.original_x_pos = position.x()
            self.original_y_pos = position.y()
            self.translate = True

    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
        """Handle mouse button release events to stop rotation or translation.

        Args:
            event: The QMouseEvent object
        """
        if event.button() == Qt.LeftButton:
            self.rotate = False
        elif event.button() == Qt.RightButton:
            self.translate = False

    def wheelEvent(self, event: QWheelEvent) -> None:
        """Handle mouse wheel events for zooming.

        Zooming is performed by adjusting the Z coordinate of the model position.

        Args:
            event: The QWheelEvent object
        """
        angle_delta = event.angleDelta()

        # Handle both x and y wheel movement (some mice/trackpads use different axes)
        delta = angle_delta.y() if angle_delta.y() != 0 else angle_delta.x()

        if delta > 0:
            self.model_position.z += self.zoom_sensitivity
        elif delta < 0:
            self.model_position.z -= self.zoom_sensitivity

        self.update()

close_target()

The thing the Escape shortcut should close.

The mixin is used two ways round. Most applications mix it into a QOpenGLWindow, which is a QWindow and is already the top level thing, so closing it is closing the application. A few mix it into a QOpenGLWidget sitting inside a QMainWindow alongside a control panel, and there closing the widget only hides the viewport and leaves the rest of the window behind, which is never what Escape is meant to do.

QWidget has window() to walk up to the top level; QWindow has no such method at all, so this cannot simply be self.window().

Returns:
  • object

    The top level window if there is one to walk up to, else self.

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def close_target(self) -> object:
    """The thing the Escape shortcut should close.

    The mixin is used two ways round. Most applications mix it into a
    QOpenGLWindow, which is a QWindow and is already the top level thing, so
    closing it is closing the application. A few mix it into a QOpenGLWidget
    sitting inside a QMainWindow alongside a control panel, and there
    closing the widget only hides the viewport and leaves the rest of the
    window behind, which is never what Escape is meant to do.

    QWidget has window() to walk up to the top level; QWindow has no such
    method at all, so this cannot simply be self.window().

    Returns:
        The top level window if there is one to walk up to, else self.
    """
    window = getattr(self, "window", None)
    return window() if callable(window) else self

keyPressEvent(event)

Handle keyboard press events with common shortcuts.

Shortcuts: - Escape: Close the window this is in - W: Switch to wireframe mode - S: Switch to solid fill mode - Space: Reset camera rotation and position

All of these are off when setup_event_handling was given handle_key_shortcuts=False, in which case every key is passed on untouched for the parent to deal with.

Parameters:
  • event (QKeyEvent) –

    The QKeyEvent object

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def keyPressEvent(self, event: QKeyEvent) -> None:
    """Handle keyboard press events with common shortcuts.

    Shortcuts:
    - Escape: Close the window this is in
    - W: Switch to wireframe mode
    - S: Switch to solid fill mode
    - Space: Reset camera rotation and position

    All of these are off when setup_event_handling was given
    handle_key_shortcuts=False, in which case every key is passed on
    untouched for the parent to deal with.

    Args:
        event: The QKeyEvent object
    """
    if not getattr(self, "handle_key_shortcuts", True):
        super().keyPressEvent(event)
        return

    key = event.key()

    if key == Qt.Key_Escape:
        self.close_target().close()
    elif key == Qt.Key_W:
        self.wireframe = True
        gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
    elif key == Qt.Key_S:
        self.wireframe = False
        gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
    elif key == Qt.Key_Space:
        self.reset_camera()
    else:
        # Let subclasses handle other keys
        super().keyPressEvent(event)
        return

    self.update()

mouseMoveEvent(event)

Handle mouse movement for camera control.

  • Left button: Rotate the scene
  • Right button: Translate (pan) the scene
Parameters:
  • event (QMouseEvent) –

    The QMouseEvent object

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def mouseMoveEvent(self, event: QMouseEvent) -> None:
    """Handle mouse movement for camera control.

    - Left button: Rotate the scene
    - Right button: Translate (pan) the scene

    Args:
        event: The QMouseEvent object
    """
    position = event.position()

    # Handle rotation with left mouse button
    if self.rotate and event.buttons() == Qt.LeftButton:
        diff_x = position.x() - self.original_x_rotation
        diff_y = position.y() - self.original_y_rotation

        self.spin_x_face += int(self.rotation_sensitivity * diff_y)
        self.spin_y_face += int(self.rotation_sensitivity * diff_x)

        self.original_x_rotation = position.x()
        self.original_y_rotation = position.y()

        self.update()

    # Handle translation with right mouse button
    elif self.translate and event.buttons() == Qt.RightButton:
        diff_x = int(position.x() - self.original_x_pos)
        diff_y = int(position.y() - self.original_y_pos)

        self.original_x_pos = position.x()
        self.original_y_pos = position.y()

        self.model_position.x += self.translation_sensitivity * diff_x
        self.model_position.y -= self.translation_sensitivity * diff_y

        self.update()

mousePressEvent(event)

Handle mouse button press events to initiate rotation or translation.

  • Left button: Start rotation mode
  • Right button: Start translation mode
Parameters:
  • event (QMouseEvent) –

    The QMouseEvent object

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def mousePressEvent(self, event: QMouseEvent) -> None:
    """Handle mouse button press events to initiate rotation or translation.

    - Left button: Start rotation mode
    - Right button: Start translation mode

    Args:
        event: The QMouseEvent object
    """
    position = event.position()

    if event.button() == Qt.LeftButton:
        self.original_x_rotation = position.x()
        self.original_y_rotation = position.y()
        self.rotate = True

    elif event.button() == Qt.RightButton:
        self.original_x_pos = position.x()
        self.original_y_pos = position.y()
        self.translate = True

mouseReleaseEvent(event)

Handle mouse button release events to stop rotation or translation.

Parameters:
  • event (QMouseEvent) –

    The QMouseEvent object

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
214
215
216
217
218
219
220
221
222
223
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
    """Handle mouse button release events to stop rotation or translation.

    Args:
        event: The QMouseEvent object
    """
    if event.button() == Qt.LeftButton:
        self.rotate = False
    elif event.button() == Qt.RightButton:
        self.translate = False

reset_camera()

Reset camera rotation and model position to defaults.

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
92
93
94
95
96
def reset_camera(self) -> None:
    """Reset camera rotation and model position to defaults."""
    self.spin_x_face = 0
    self.spin_y_face = 0
    self.model_position.set(0, 0, 0)

setup_event_handling(rotation_sensitivity=DEFAULT_ROTATION_SENSITIVITY, translation_sensitivity=DEFAULT_TRANSLATION_SENSITIVITY, zoom_sensitivity=DEFAULT_ZOOM_SENSITIVITY, initial_position=None, handle_key_shortcuts=True)

Initialize event handling attributes.

Parameters:
  • rotation_sensitivity (float, default: DEFAULT_ROTATION_SENSITIVITY ) –

    Mouse sensitivity for rotation (default: 0.5)

  • translation_sensitivity (float, default: DEFAULT_TRANSLATION_SENSITIVITY ) –

    Mouse sensitivity for translation (default: 0.01)

  • zoom_sensitivity (float, default: DEFAULT_ZOOM_SENSITIVITY ) –

    Mouse wheel sensitivity for zooming (default: 0.1)

  • initial_position (Vec3, default: None ) –

    Initial model position (default: Vec3(0,0,0))

  • handle_key_shortcuts (bool, default: True ) –

    Whether the mixin handles Escape, W, S and Space (default: True). Pass False when the application owns its own keyboard, and every key press is passed on to the parent instead.

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def setup_event_handling(
    self,
    rotation_sensitivity: float = DEFAULT_ROTATION_SENSITIVITY,
    translation_sensitivity: float = DEFAULT_TRANSLATION_SENSITIVITY,
    zoom_sensitivity: float = DEFAULT_ZOOM_SENSITIVITY,
    initial_position: Vec3 = None,
    handle_key_shortcuts: bool = True,
) -> None:
    """Initialize event handling attributes.

    Args:
        rotation_sensitivity: Mouse sensitivity for rotation (default: 0.5)
        translation_sensitivity: Mouse sensitivity for translation (default: 0.01)
        zoom_sensitivity: Mouse wheel sensitivity for zooming (default: 0.1)
        initial_position: Initial model position (default: Vec3(0,0,0))
        handle_key_shortcuts: Whether the mixin handles Escape, W, S and Space
            (default: True). Pass False when the application owns its own
            keyboard, and every key press is passed on to the parent instead.
    """
    # Mouse control state
    self.rotate: bool = False
    self.translate: bool = False

    # Whether the mixin's own keyboard shortcuts are live
    self.handle_key_shortcuts: bool = handle_key_shortcuts

    # Set by the W and S shortcuts so that a paintGL which sets the polygon
    # mode itself has something to read, see keyPressEvent. Only defaulted
    # if the application has not already made it its own, as several do.
    if not hasattr(self, "wireframe"):
        self.wireframe: bool = False

    # Rotation state
    self.spin_x_face: int = 0
    self.spin_y_face: int = 0

    # Mouse position tracking for rotation
    self.original_x_rotation: float = 0.0
    self.original_y_rotation: float = 0.0

    # Mouse position tracking for translation
    self.original_x_pos: float = 0.0
    self.original_y_pos: float = 0.0

    # Model position and sensitivity settings
    self.model_position: Vec3 = initial_position or Vec3(0, 0, 0)
    self.rotation_sensitivity: float = rotation_sensitivity
    self.translation_sensitivity: float = translation_sensitivity
    self.zoom_sensitivity: float = zoom_sensitivity

    self.INCREMENT = self.translation_sensitivity
    self.ZOOM = self.zoom_sensitivity

wheelEvent(event)

Handle mouse wheel events for zooming.

Zooming is performed by adjusting the Z coordinate of the model position.

Parameters:
  • event (QWheelEvent) –

    The QWheelEvent object

Source code in ncca/ngl/opengl/pyside_event_handling_mixin.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def wheelEvent(self, event: QWheelEvent) -> None:
    """Handle mouse wheel events for zooming.

    Zooming is performed by adjusting the Z coordinate of the model position.

    Args:
        event: The QWheelEvent object
    """
    angle_delta = event.angleDelta()

    # Handle both x and y wheel movement (some mice/trackpads use different axes)
    delta = angle_delta.y() if angle_delta.y() != 0 else angle_delta.x()

    if delta > 0:
        self.model_position.z += self.zoom_sensitivity
    elif delta < 0:
        self.model_position.z -= self.zoom_sensitivity

    self.update()

Vec2Array

A class to hold Vec2 data in contiguous memory for efficient GPU transfer.

Internally uses a numpy array of shape (N, 2) for optimal performance. Mutable container — intentionally not hashable.

Source code in ncca/ngl/vec2_array.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
class Vec2Array:
    """A class to hold Vec2 data in contiguous memory for efficient GPU transfer.

    Internally uses a numpy array of shape (N, 2) for optimal performance.
    Mutable container — intentionally not hashable.
    """

    def __init__(self, values: "Iterable[Vec2] | int | None" = None) -> None:
        """Initializes the Vec2Array.

        Args:
            values (iterable | int, optional): An iterable of Vec2 objects or an integer.
                If an integer, the array is initialized with that many default Vec2s.
                If an iterable, it's initialized with the Vec2s from the iterable.
                Defaults to None (an empty array).
        """
        if values is None:
            # Empty array - start with shape (0, 2)
            self._data = np.zeros((0, 2), dtype=np.float32)
        elif isinstance(values, int):
            # Initialize N default Vec2s (0, 0)
            self._data = np.zeros((values, 2), dtype=np.float32)
        else:
            # Initialize from iterable of Vec2 objects
            vec_list = []
            for v in values:
                if not isinstance(v, Vec2):
                    raise TypeError("All elements must be of type Vec2")
                vec_list.append([v.x, v.y])
            self._data = np.array(vec_list, dtype=np.float32)

    def __getitem__(self, index: int | slice) -> "Vec2 | Vec2Array":
        """Get the Vec2 at the specified index.

        Args:
            index (int | slice): The index or slice of the element(s).

        Returns:
            Vec2: The Vec2 object at the given index.
            Vec2Array: A new Vec2Array if slicing.
        """
        if isinstance(index, slice):
            # Return a new Vec2Array with sliced data
            result = Vec2Array()
            result._data = self._data[index].copy()
            return result
        else:
            # Return a single Vec2
            row = self._data[index]
            return Vec2(row[0], row[1])

    def __setitem__(self, index: int, value: Vec2) -> None:
        """Set the Vec2 at the specified index.

        Args:
            index (int): The index of the element to set.
            value (Vec2): The new Vec2 object.
        """
        if not isinstance(value, Vec2):
            raise TypeError("Only Vec2 objects can be assigned")
        self._data[index] = [value.x, value.y]

    def __len__(self) -> int:
        """Return the number of elements in the array."""
        return len(self._data)

    def __iter__(self) -> "Iterable[Vec2]":
        """Return an iterator that yields Vec2 objects."""
        for i in range(len(self._data)):
            row = self._data[i]
            yield Vec2(row[0], row[1])

    def __eq__(self, other: object) -> bool:
        """Compare two Vec2Array instances for equality.

        Args:
            other: Another Vec2Array instance to compare with.

        Returns:
            bool: True if the arrays contain the same data, False otherwise.
        """
        if not isinstance(other, Vec2Array):
            return NotImplemented
        return np.array_equal(self._data, other._data)

    def append(self, value: Vec2) -> None:
        """Append a Vec2 object to the array.

        Args:
            value (Vec2): The Vec2 object to append.
        """
        if not isinstance(value, Vec2):
            raise TypeError("Only Vec2 objects can be appended")
        new_row = np.array([[value.x, value.y]], dtype=np.float32)
        self._data = np.vstack([self._data, new_row])

    def extend(self, values: "Iterable[Vec2]") -> None:
        """Extend the array with a list of Vec2 objects.

        Args:
            values (list): A list of Vec2 objects to extend.
        """
        if not all(isinstance(v, Vec2) for v in values):
            raise TypeError("All elements must be of type Vec2")

        new_rows = np.array([[v.x, v.y] for v in values], dtype=np.float32)
        if len(self._data) == 0:
            self._data = new_rows
        else:
            self._data = np.vstack([self._data, new_rows])

    def to_list(self) -> list[float]:
        """Convert the array of Vec2 objects to a single flat list of floats.

        Returns:
            list: A list of x, y components concatenated.
        """
        return self._data.flatten().tolist()

    def to_numpy(self) -> np.ndarray:
        """Convert the array of Vec2 objects to a numpy array.

        This is the primary method for GPU data transfer.

        Returns:
            numpy.ndarray: A float32 numpy array of shape (N*2,) for GPU transfer.
        """
        return self._data.flatten().copy()

    def to_tuple(self) -> tuple[float, ...]:
        """Return all components as one flat tuple of floats."""
        return tuple(float(v) for v in self._data.flatten())

    def __repr__(self) -> str:
        """Eval-able representation, e.g. Vec2Array([Vec2(0.0, 0.0)])."""
        vec_list = [Vec2(row[0], row[1]) for row in self._data]
        return f"Vec2Array({vec_list!r})"

    def __str__(self) -> str:
        """Pretty representation as a list of Vec2 values."""
        vec_list = [Vec2(row[0], row[1]) for row in self._data]
        return str(vec_list)

    def sizeof(self) -> int:
        """Return the size of the array in bytes.

        Returns:
            int: The size of the array in bytes.
        """
        return len(self._data) * Vec2.sizeof()

__eq__(other)

Compare two Vec2Array instances for equality.

Parameters:
  • other (object) –

    Another Vec2Array instance to compare with.

Returns:
  • bool( bool ) –

    True if the arrays contain the same data, False otherwise.

Source code in ncca/ngl/vec2_array.py
85
86
87
88
89
90
91
92
93
94
95
96
def __eq__(self, other: object) -> bool:
    """Compare two Vec2Array instances for equality.

    Args:
        other: Another Vec2Array instance to compare with.

    Returns:
        bool: True if the arrays contain the same data, False otherwise.
    """
    if not isinstance(other, Vec2Array):
        return NotImplemented
    return np.array_equal(self._data, other._data)

__getitem__(index)

Get the Vec2 at the specified index.

Parameters:
  • index (int | slice) –

    The index or slice of the element(s).

Returns:
Source code in ncca/ngl/vec2_array.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __getitem__(self, index: int | slice) -> "Vec2 | Vec2Array":
    """Get the Vec2 at the specified index.

    Args:
        index (int | slice): The index or slice of the element(s).

    Returns:
        Vec2: The Vec2 object at the given index.
        Vec2Array: A new Vec2Array if slicing.
    """
    if isinstance(index, slice):
        # Return a new Vec2Array with sliced data
        result = Vec2Array()
        result._data = self._data[index].copy()
        return result
    else:
        # Return a single Vec2
        row = self._data[index]
        return Vec2(row[0], row[1])

__init__(values=None)

Initializes the Vec2Array.

Parameters:
  • values (iterable | int, default: None ) –

    An iterable of Vec2 objects or an integer. If an integer, the array is initialized with that many default Vec2s. If an iterable, it's initialized with the Vec2s from the iterable. Defaults to None (an empty array).

Source code in ncca/ngl/vec2_array.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(self, values: "Iterable[Vec2] | int | None" = None) -> None:
    """Initializes the Vec2Array.

    Args:
        values (iterable | int, optional): An iterable of Vec2 objects or an integer.
            If an integer, the array is initialized with that many default Vec2s.
            If an iterable, it's initialized with the Vec2s from the iterable.
            Defaults to None (an empty array).
    """
    if values is None:
        # Empty array - start with shape (0, 2)
        self._data = np.zeros((0, 2), dtype=np.float32)
    elif isinstance(values, int):
        # Initialize N default Vec2s (0, 0)
        self._data = np.zeros((values, 2), dtype=np.float32)
    else:
        # Initialize from iterable of Vec2 objects
        vec_list = []
        for v in values:
            if not isinstance(v, Vec2):
                raise TypeError("All elements must be of type Vec2")
            vec_list.append([v.x, v.y])
        self._data = np.array(vec_list, dtype=np.float32)

__iter__()

Return an iterator that yields Vec2 objects.

Source code in ncca/ngl/vec2_array.py
79
80
81
82
83
def __iter__(self) -> "Iterable[Vec2]":
    """Return an iterator that yields Vec2 objects."""
    for i in range(len(self._data)):
        row = self._data[i]
        yield Vec2(row[0], row[1])

__len__()

Return the number of elements in the array.

Source code in ncca/ngl/vec2_array.py
75
76
77
def __len__(self) -> int:
    """Return the number of elements in the array."""
    return len(self._data)

__repr__()

Eval-able representation, e.g. Vec2Array([Vec2(0.0, 0.0)]).

Source code in ncca/ngl/vec2_array.py
146
147
148
149
def __repr__(self) -> str:
    """Eval-able representation, e.g. Vec2Array([Vec2(0.0, 0.0)])."""
    vec_list = [Vec2(row[0], row[1]) for row in self._data]
    return f"Vec2Array({vec_list!r})"

__setitem__(index, value)

Set the Vec2 at the specified index.

Parameters:
  • index (int) –

    The index of the element to set.

  • value (Vec2) –

    The new Vec2 object.

Source code in ncca/ngl/vec2_array.py
64
65
66
67
68
69
70
71
72
73
def __setitem__(self, index: int, value: Vec2) -> None:
    """Set the Vec2 at the specified index.

    Args:
        index (int): The index of the element to set.
        value (Vec2): The new Vec2 object.
    """
    if not isinstance(value, Vec2):
        raise TypeError("Only Vec2 objects can be assigned")
    self._data[index] = [value.x, value.y]

__str__()

Pretty representation as a list of Vec2 values.

Source code in ncca/ngl/vec2_array.py
151
152
153
154
def __str__(self) -> str:
    """Pretty representation as a list of Vec2 values."""
    vec_list = [Vec2(row[0], row[1]) for row in self._data]
    return str(vec_list)

append(value)

Append a Vec2 object to the array.

Parameters:
  • value (Vec2) –

    The Vec2 object to append.

Source code in ncca/ngl/vec2_array.py
 98
 99
100
101
102
103
104
105
106
107
def append(self, value: Vec2) -> None:
    """Append a Vec2 object to the array.

    Args:
        value (Vec2): The Vec2 object to append.
    """
    if not isinstance(value, Vec2):
        raise TypeError("Only Vec2 objects can be appended")
    new_row = np.array([[value.x, value.y]], dtype=np.float32)
    self._data = np.vstack([self._data, new_row])

extend(values)

Extend the array with a list of Vec2 objects.

Parameters:
  • values (list) –

    A list of Vec2 objects to extend.

Source code in ncca/ngl/vec2_array.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def extend(self, values: "Iterable[Vec2]") -> None:
    """Extend the array with a list of Vec2 objects.

    Args:
        values (list): A list of Vec2 objects to extend.
    """
    if not all(isinstance(v, Vec2) for v in values):
        raise TypeError("All elements must be of type Vec2")

    new_rows = np.array([[v.x, v.y] for v in values], dtype=np.float32)
    if len(self._data) == 0:
        self._data = new_rows
    else:
        self._data = np.vstack([self._data, new_rows])

sizeof()

Return the size of the array in bytes.

Returns:
  • int( int ) –

    The size of the array in bytes.

Source code in ncca/ngl/vec2_array.py
156
157
158
159
160
161
162
def sizeof(self) -> int:
    """Return the size of the array in bytes.

    Returns:
        int: The size of the array in bytes.
    """
    return len(self._data) * Vec2.sizeof()

to_list()

Convert the array of Vec2 objects to a single flat list of floats.

Returns:
  • list( list[float] ) –

    A list of x, y components concatenated.

Source code in ncca/ngl/vec2_array.py
124
125
126
127
128
129
130
def to_list(self) -> list[float]:
    """Convert the array of Vec2 objects to a single flat list of floats.

    Returns:
        list: A list of x, y components concatenated.
    """
    return self._data.flatten().tolist()

to_numpy()

Convert the array of Vec2 objects to a numpy array.

This is the primary method for GPU data transfer.

Returns:
  • ndarray

    numpy.ndarray: A float32 numpy array of shape (N*2,) for GPU transfer.

Source code in ncca/ngl/vec2_array.py
132
133
134
135
136
137
138
139
140
def to_numpy(self) -> np.ndarray:
    """Convert the array of Vec2 objects to a numpy array.

    This is the primary method for GPU data transfer.

    Returns:
        numpy.ndarray: A float32 numpy array of shape (N*2,) for GPU transfer.
    """
    return self._data.flatten().copy()

to_tuple()

Return all components as one flat tuple of floats.

Source code in ncca/ngl/vec2_array.py
142
143
144
def to_tuple(self) -> tuple[float, ...]:
    """Return all components as one flat tuple of floats."""
    return tuple(float(v) for v in self._data.flatten())

Vec3Array

A class to hold Vec3 data in contiguous memory for efficient GPU transfer.

Internally uses a numpy array of shape (N, 3) for optimal performance. Mutable container — intentionally not hashable.

Source code in ncca/ngl/vec3_array.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
class Vec3Array:
    """A class to hold Vec3 data in contiguous memory for efficient GPU transfer.

    Internally uses a numpy array of shape (N, 3) for optimal performance.
    Mutable container — intentionally not hashable.
    """

    def __init__(self, values: "Iterable[Vec3] | int | None" = None) -> None:
        """Initializes the Vec3Array.

        Args:
            values (iterable | int, optional): An iterable of Vec3 objects or an integer.
                If an integer, the array is initialized with that many default Vec3s.
                If an iterable, it's initialized with the Vec3s from the iterable.
                Defaults to None (an empty array).
        """
        if values is None:
            # Empty array - start with shape (0, 3)
            self._data = np.zeros((0, 3), dtype=np.float32)
        elif isinstance(values, int):
            # Initialize N default Vec3s (0, 0, 0)
            self._data = np.zeros((values, 3), dtype=np.float32)
        else:
            # Initialize from iterable of Vec3 objects
            vec_list = []
            for v in values:
                if not isinstance(v, Vec3):
                    raise TypeError("All elements must be of type Vec3")
                vec_list.append([v.x, v.y, v.z])
            self._data = np.array(vec_list, dtype=np.float32)

    def __getitem__(self, index: int | slice) -> "Vec3 | Vec3Array":
        """Get the Vec3 at the specified index.

        Args:
            index (int | slice): The index or slice of the element(s).

        Returns:
            Vec3: The Vec3 object at the given index.
            Vec3Array: A new Vec3Array if slicing.
        """
        if isinstance(index, slice):
            # Return a new Vec3Array with sliced data
            result = Vec3Array()
            result._data = self._data[index].copy()
            return result
        else:
            # Return a single Vec3
            row = self._data[index]
            return Vec3(row[0], row[1], row[2])

    def __setitem__(self, index: int, value: Vec3) -> None:
        """Set the Vec3 at the specified index.

        Args:
            index (int): The index of the element to set.
            value (Vec3): The new Vec3 object.
        """
        if not isinstance(value, Vec3):
            raise TypeError("Only Vec3 objects can be assigned")
        self._data[index] = [value.x, value.y, value.z]

    def __len__(self) -> int:
        """Return the number of elements in the array."""
        return len(self._data)

    def __iter__(self) -> "Iterable[Vec3]":
        """Return an iterator that yields Vec3 objects."""
        for i in range(len(self._data)):
            row = self._data[i]
            yield Vec3(row[0], row[1], row[2])

    def __eq__(self, other: object) -> bool:
        """Compare two Vec3Array instances for equality.

        Args:
            other: Another Vec3Array instance to compare with.

        Returns:
            bool: True if the arrays contain the same data, False otherwise.
        """
        if not isinstance(other, Vec3Array):
            return NotImplemented
        return np.array_equal(self._data, other._data)

    def append(self, value: Vec3) -> None:
        """Append a Vec3 object to the array.

        Args:
            value (Vec3): The Vec3 object to append.
        """
        if not isinstance(value, Vec3):
            raise TypeError("Only Vec3 objects can be appended")
        new_row = np.array([[value.x, value.y, value.z]], dtype=np.float32)
        self._data = np.vstack([self._data, new_row])

    def extend(self, values: "Iterable[Vec3]") -> None:
        """Extend the array by appending elements from the iterable.

        Args:
            values (iterable): An iterable of Vec3 objects to append.

        Raises:
            TypeError: If any element in values is not a Vec3.
        """
        vec_list = []
        for v in values:
            if not isinstance(v, Vec3):
                raise TypeError("All elements must be of type Vec3")
            vec_list.append([v.x, v.y, v.z])

        new_rows = np.array(vec_list, dtype=np.float32)
        if len(self._data) == 0:
            self._data = new_rows
        else:
            self._data = np.vstack([self._data, new_rows])

    def to_list(self) -> list[float]:
        """Convert the array of Vec3 objects to a single flat list of floats.

        Returns:
            list: A list of x, y, z components concatenated.
        """
        return self._data.flatten().tolist()

    def to_numpy(self) -> np.ndarray:
        """Convert the array of Vec3 objects to a numpy array.

        This is the primary method for GPU data transfer.

        Returns:
            numpy.ndarray: A float32 numpy array of shape (N*3,) for GPU transfer.
        """
        return self._data.flatten().copy()

    def to_tuple(self) -> tuple[float, ...]:
        """Return all components as one flat tuple of floats."""
        return tuple(float(v) for v in self._data.flatten())

    def __repr__(self) -> str:
        """Eval-able representation, e.g. Vec3Array([Vec3(0.0, 0.0, 0.0)])."""
        vec_list = [Vec3(row[0], row[1], row[2]) for row in self._data]
        return f"Vec3Array({vec_list!r})"

    def __str__(self) -> str:
        """Pretty representation as a list of Vec3 values."""
        vec_list = [Vec3(row[0], row[1], row[2]) for row in self._data]
        return str(vec_list)

    def sizeof(self) -> int:
        """Return the size of the array in bytes.

        Returns:
            int: The size of the array in bytes.
        """
        return len(self._data) * Vec3.sizeof()

__eq__(other)

Compare two Vec3Array instances for equality.

Parameters:
  • other (object) –

    Another Vec3Array instance to compare with.

Returns:
  • bool( bool ) –

    True if the arrays contain the same data, False otherwise.

Source code in ncca/ngl/vec3_array.py
85
86
87
88
89
90
91
92
93
94
95
96
def __eq__(self, other: object) -> bool:
    """Compare two Vec3Array instances for equality.

    Args:
        other: Another Vec3Array instance to compare with.

    Returns:
        bool: True if the arrays contain the same data, False otherwise.
    """
    if not isinstance(other, Vec3Array):
        return NotImplemented
    return np.array_equal(self._data, other._data)

__getitem__(index)

Get the Vec3 at the specified index.

Parameters:
  • index (int | slice) –

    The index or slice of the element(s).

Returns:
Source code in ncca/ngl/vec3_array.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __getitem__(self, index: int | slice) -> "Vec3 | Vec3Array":
    """Get the Vec3 at the specified index.

    Args:
        index (int | slice): The index or slice of the element(s).

    Returns:
        Vec3: The Vec3 object at the given index.
        Vec3Array: A new Vec3Array if slicing.
    """
    if isinstance(index, slice):
        # Return a new Vec3Array with sliced data
        result = Vec3Array()
        result._data = self._data[index].copy()
        return result
    else:
        # Return a single Vec3
        row = self._data[index]
        return Vec3(row[0], row[1], row[2])

__init__(values=None)

Initializes the Vec3Array.

Parameters:
  • values (iterable | int, default: None ) –

    An iterable of Vec3 objects or an integer. If an integer, the array is initialized with that many default Vec3s. If an iterable, it's initialized with the Vec3s from the iterable. Defaults to None (an empty array).

Source code in ncca/ngl/vec3_array.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(self, values: "Iterable[Vec3] | int | None" = None) -> None:
    """Initializes the Vec3Array.

    Args:
        values (iterable | int, optional): An iterable of Vec3 objects or an integer.
            If an integer, the array is initialized with that many default Vec3s.
            If an iterable, it's initialized with the Vec3s from the iterable.
            Defaults to None (an empty array).
    """
    if values is None:
        # Empty array - start with shape (0, 3)
        self._data = np.zeros((0, 3), dtype=np.float32)
    elif isinstance(values, int):
        # Initialize N default Vec3s (0, 0, 0)
        self._data = np.zeros((values, 3), dtype=np.float32)
    else:
        # Initialize from iterable of Vec3 objects
        vec_list = []
        for v in values:
            if not isinstance(v, Vec3):
                raise TypeError("All elements must be of type Vec3")
            vec_list.append([v.x, v.y, v.z])
        self._data = np.array(vec_list, dtype=np.float32)

__iter__()

Return an iterator that yields Vec3 objects.

Source code in ncca/ngl/vec3_array.py
79
80
81
82
83
def __iter__(self) -> "Iterable[Vec3]":
    """Return an iterator that yields Vec3 objects."""
    for i in range(len(self._data)):
        row = self._data[i]
        yield Vec3(row[0], row[1], row[2])

__len__()

Return the number of elements in the array.

Source code in ncca/ngl/vec3_array.py
75
76
77
def __len__(self) -> int:
    """Return the number of elements in the array."""
    return len(self._data)

__repr__()

Eval-able representation, e.g. Vec3Array([Vec3(0.0, 0.0, 0.0)]).

Source code in ncca/ngl/vec3_array.py
152
153
154
155
def __repr__(self) -> str:
    """Eval-able representation, e.g. Vec3Array([Vec3(0.0, 0.0, 0.0)])."""
    vec_list = [Vec3(row[0], row[1], row[2]) for row in self._data]
    return f"Vec3Array({vec_list!r})"

__setitem__(index, value)

Set the Vec3 at the specified index.

Parameters:
  • index (int) –

    The index of the element to set.

  • value (Vec3) –

    The new Vec3 object.

Source code in ncca/ngl/vec3_array.py
64
65
66
67
68
69
70
71
72
73
def __setitem__(self, index: int, value: Vec3) -> None:
    """Set the Vec3 at the specified index.

    Args:
        index (int): The index of the element to set.
        value (Vec3): The new Vec3 object.
    """
    if not isinstance(value, Vec3):
        raise TypeError("Only Vec3 objects can be assigned")
    self._data[index] = [value.x, value.y, value.z]

__str__()

Pretty representation as a list of Vec3 values.

Source code in ncca/ngl/vec3_array.py
157
158
159
160
def __str__(self) -> str:
    """Pretty representation as a list of Vec3 values."""
    vec_list = [Vec3(row[0], row[1], row[2]) for row in self._data]
    return str(vec_list)

append(value)

Append a Vec3 object to the array.

Parameters:
  • value (Vec3) –

    The Vec3 object to append.

Source code in ncca/ngl/vec3_array.py
 98
 99
100
101
102
103
104
105
106
107
def append(self, value: Vec3) -> None:
    """Append a Vec3 object to the array.

    Args:
        value (Vec3): The Vec3 object to append.
    """
    if not isinstance(value, Vec3):
        raise TypeError("Only Vec3 objects can be appended")
    new_row = np.array([[value.x, value.y, value.z]], dtype=np.float32)
    self._data = np.vstack([self._data, new_row])

extend(values)

Extend the array by appending elements from the iterable.

Parameters:
  • values (iterable) –

    An iterable of Vec3 objects to append.

Raises:
  • TypeError

    If any element in values is not a Vec3.

Source code in ncca/ngl/vec3_array.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def extend(self, values: "Iterable[Vec3]") -> None:
    """Extend the array by appending elements from the iterable.

    Args:
        values (iterable): An iterable of Vec3 objects to append.

    Raises:
        TypeError: If any element in values is not a Vec3.
    """
    vec_list = []
    for v in values:
        if not isinstance(v, Vec3):
            raise TypeError("All elements must be of type Vec3")
        vec_list.append([v.x, v.y, v.z])

    new_rows = np.array(vec_list, dtype=np.float32)
    if len(self._data) == 0:
        self._data = new_rows
    else:
        self._data = np.vstack([self._data, new_rows])

sizeof()

Return the size of the array in bytes.

Returns:
  • int( int ) –

    The size of the array in bytes.

Source code in ncca/ngl/vec3_array.py
162
163
164
165
166
167
168
def sizeof(self) -> int:
    """Return the size of the array in bytes.

    Returns:
        int: The size of the array in bytes.
    """
    return len(self._data) * Vec3.sizeof()

to_list()

Convert the array of Vec3 objects to a single flat list of floats.

Returns:
  • list( list[float] ) –

    A list of x, y, z components concatenated.

Source code in ncca/ngl/vec3_array.py
130
131
132
133
134
135
136
def to_list(self) -> list[float]:
    """Convert the array of Vec3 objects to a single flat list of floats.

    Returns:
        list: A list of x, y, z components concatenated.
    """
    return self._data.flatten().tolist()

to_numpy()

Convert the array of Vec3 objects to a numpy array.

This is the primary method for GPU data transfer.

Returns:
  • ndarray

    numpy.ndarray: A float32 numpy array of shape (N*3,) for GPU transfer.

Source code in ncca/ngl/vec3_array.py
138
139
140
141
142
143
144
145
146
def to_numpy(self) -> np.ndarray:
    """Convert the array of Vec3 objects to a numpy array.

    This is the primary method for GPU data transfer.

    Returns:
        numpy.ndarray: A float32 numpy array of shape (N*3,) for GPU transfer.
    """
    return self._data.flatten().copy()

to_tuple()

Return all components as one flat tuple of floats.

Source code in ncca/ngl/vec3_array.py
148
149
150
def to_tuple(self) -> tuple[float, ...]:
    """Return all components as one flat tuple of floats."""
    return tuple(float(v) for v in self._data.flatten())

Vec4Array

A class to hold Vec4 data in contiguous memory for efficient GPU transfer.

Internally uses a numpy array of shape (N, 4) for optimal performance. Mutable container — intentionally not hashable.

Source code in ncca/ngl/vec4_array.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class Vec4Array:
    """A class to hold Vec4 data in contiguous memory for efficient GPU transfer.

    Internally uses a numpy array of shape (N, 4) for optimal performance.
    Mutable container — intentionally not hashable.
    """

    def __init__(self, values: "Iterable[Vec4] | int | None" = None) -> None:
        """Initializes the Vec4Array.

        Args:
            values (iterable | int, optional): An iterable of Vec4 objects or an integer.
                If an integer, the array is initialized with that many default Vec4s.
                If an iterable, it's initialized with the Vec4s from the iterable.
                Defaults to None (an empty array).
        """
        if values is None:
            # Empty array - start with shape (0, 4)
            self._data = np.zeros((0, 4), dtype=np.float32)
        elif isinstance(values, int):
            # Initialize N default Vec4s (0, 0, 0, 1)
            self._data = np.zeros((values, 4), dtype=np.float32)
            self._data[:, 3] = 1.0  # Set w component to 1.0
        else:
            # Initialize from iterable of Vec4 objects
            vec_list = []
            for v in values:
                if not isinstance(v, Vec4):
                    raise TypeError("All elements must be of type Vec4")
                vec_list.append([v.x, v.y, v.z, v.w])
            self._data = np.array(vec_list, dtype=np.float32)

    def __getitem__(self, index: int | slice) -> "Vec4 | Vec4Array":
        """Get the Vec4 at the specified index.

        Args:
            index (int | slice): The index or slice of the element(s).

        Returns:
            Vec4: The Vec4 object at the given index.
            Vec4Array: A new Vec4Array if slicing.
        """
        if isinstance(index, slice):
            # Return a new Vec4Array with sliced data
            result = Vec4Array()
            result._data = self._data[index].copy()
            return result
        else:
            # Return a single Vec4
            row = self._data[index]
            return Vec4(row[0], row[1], row[2], row[3])

    def __setitem__(self, index: int, value: Vec4) -> None:
        """Set the Vec4 at the specified index.

        Args:
            index (int): The index of the element to set.
            value (Vec4): The new Vec4 object.
        """
        if not isinstance(value, Vec4):
            raise TypeError("Only Vec4 objects can be assigned")
        self._data[index] = [value.x, value.y, value.z, value.w]

    def __len__(self) -> int:
        """Return the number of elements in the array."""
        return len(self._data)

    def __iter__(self) -> "Iterable[Vec4]":
        """Return an iterator that yields Vec4 objects."""
        for i in range(len(self._data)):
            row = self._data[i]
            yield Vec4(row[0], row[1], row[2], row[3])

    def __eq__(self, other: object) -> bool:
        """Compare two Vec4Array instances for equality.

        Args:
            other: Another Vec4Array instance to compare with.

        Returns:
            bool: True if the arrays contain the same data, False otherwise.
        """
        if not isinstance(other, Vec4Array):
            return NotImplemented
        return np.array_equal(self._data, other._data)

    def append(self, value: Vec4) -> None:
        """Append a Vec4 object to the array.

        Args:
            value (Vec4): The Vec4 object to append.
        """
        if not isinstance(value, Vec4):
            raise TypeError("Only Vec4 objects can be appended")
        new_row = np.array([[value.x, value.y, value.z, value.w]], dtype=np.float32)
        self._data = np.vstack([self._data, new_row])

    def extend(self, values: "Iterable[Vec4]") -> None:
        """Extend the array with a list of Vec4 objects.

        Args:
            values (list): A list of Vec4 objects to extend.
        """
        if not all(isinstance(v, Vec4) for v in values):
            raise TypeError("All elements must be of type Vec4")

        new_rows = np.array([[v.x, v.y, v.z, v.w] for v in values], dtype=np.float32)
        if len(self._data) == 0:
            self._data = new_rows
        else:
            self._data = np.vstack([self._data, new_rows])

    def to_list(self) -> list[float]:
        """Convert the array of Vec4 objects to a single flat list of floats.

        Returns:
            list: A list of x, y, z, w components concatenated.
        """
        return self._data.flatten().tolist()

    def to_numpy(self) -> np.ndarray:
        """Convert the array of Vec4 objects to a numpy array.

        This is the primary method for GPU data transfer.

        Returns:
            numpy.ndarray: A float32 numpy array of shape (N*4,) for GPU transfer.
        """
        return self._data.flatten().copy()

    def to_tuple(self) -> tuple[float, ...]:
        """Return all components as one flat tuple of floats."""
        return tuple(float(v) for v in self._data.flatten())

    def __repr__(self) -> str:
        """Eval-able representation, e.g. Vec4Array([Vec4(0.0, 0.0, 0.0, 1.0)])."""
        vec_list = [Vec4(row[0], row[1], row[2], row[3]) for row in self._data]
        return f"Vec4Array({vec_list!r})"

    def __str__(self) -> str:
        """Pretty representation as a list of Vec4 values."""
        vec_list = [Vec4(row[0], row[1], row[2], row[3]) for row in self._data]
        return str(vec_list)

    def sizeof(self) -> int:
        """Return the size of the array in bytes.

        Returns:
            int: The size of the array in bytes.
        """
        return len(self._data) * Vec4.sizeof()

__eq__(other)

Compare two Vec4Array instances for equality.

Parameters:
  • other (object) –

    Another Vec4Array instance to compare with.

Returns:
  • bool( bool ) –

    True if the arrays contain the same data, False otherwise.

Source code in ncca/ngl/vec4_array.py
86
87
88
89
90
91
92
93
94
95
96
97
def __eq__(self, other: object) -> bool:
    """Compare two Vec4Array instances for equality.

    Args:
        other: Another Vec4Array instance to compare with.

    Returns:
        bool: True if the arrays contain the same data, False otherwise.
    """
    if not isinstance(other, Vec4Array):
        return NotImplemented
    return np.array_equal(self._data, other._data)

__getitem__(index)

Get the Vec4 at the specified index.

Parameters:
  • index (int | slice) –

    The index or slice of the element(s).

Returns:
Source code in ncca/ngl/vec4_array.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __getitem__(self, index: int | slice) -> "Vec4 | Vec4Array":
    """Get the Vec4 at the specified index.

    Args:
        index (int | slice): The index or slice of the element(s).

    Returns:
        Vec4: The Vec4 object at the given index.
        Vec4Array: A new Vec4Array if slicing.
    """
    if isinstance(index, slice):
        # Return a new Vec4Array with sliced data
        result = Vec4Array()
        result._data = self._data[index].copy()
        return result
    else:
        # Return a single Vec4
        row = self._data[index]
        return Vec4(row[0], row[1], row[2], row[3])

__init__(values=None)

Initializes the Vec4Array.

Parameters:
  • values (iterable | int, default: None ) –

    An iterable of Vec4 objects or an integer. If an integer, the array is initialized with that many default Vec4s. If an iterable, it's initialized with the Vec4s from the iterable. Defaults to None (an empty array).

Source code in ncca/ngl/vec4_array.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(self, values: "Iterable[Vec4] | int | None" = None) -> None:
    """Initializes the Vec4Array.

    Args:
        values (iterable | int, optional): An iterable of Vec4 objects or an integer.
            If an integer, the array is initialized with that many default Vec4s.
            If an iterable, it's initialized with the Vec4s from the iterable.
            Defaults to None (an empty array).
    """
    if values is None:
        # Empty array - start with shape (0, 4)
        self._data = np.zeros((0, 4), dtype=np.float32)
    elif isinstance(values, int):
        # Initialize N default Vec4s (0, 0, 0, 1)
        self._data = np.zeros((values, 4), dtype=np.float32)
        self._data[:, 3] = 1.0  # Set w component to 1.0
    else:
        # Initialize from iterable of Vec4 objects
        vec_list = []
        for v in values:
            if not isinstance(v, Vec4):
                raise TypeError("All elements must be of type Vec4")
            vec_list.append([v.x, v.y, v.z, v.w])
        self._data = np.array(vec_list, dtype=np.float32)

__iter__()

Return an iterator that yields Vec4 objects.

Source code in ncca/ngl/vec4_array.py
80
81
82
83
84
def __iter__(self) -> "Iterable[Vec4]":
    """Return an iterator that yields Vec4 objects."""
    for i in range(len(self._data)):
        row = self._data[i]
        yield Vec4(row[0], row[1], row[2], row[3])

__len__()

Return the number of elements in the array.

Source code in ncca/ngl/vec4_array.py
76
77
78
def __len__(self) -> int:
    """Return the number of elements in the array."""
    return len(self._data)

__repr__()

Eval-able representation, e.g. Vec4Array([Vec4(0.0, 0.0, 0.0, 1.0)]).

Source code in ncca/ngl/vec4_array.py
147
148
149
150
def __repr__(self) -> str:
    """Eval-able representation, e.g. Vec4Array([Vec4(0.0, 0.0, 0.0, 1.0)])."""
    vec_list = [Vec4(row[0], row[1], row[2], row[3]) for row in self._data]
    return f"Vec4Array({vec_list!r})"

__setitem__(index, value)

Set the Vec4 at the specified index.

Parameters:
  • index (int) –

    The index of the element to set.

  • value (Vec4) –

    The new Vec4 object.

Source code in ncca/ngl/vec4_array.py
65
66
67
68
69
70
71
72
73
74
def __setitem__(self, index: int, value: Vec4) -> None:
    """Set the Vec4 at the specified index.

    Args:
        index (int): The index of the element to set.
        value (Vec4): The new Vec4 object.
    """
    if not isinstance(value, Vec4):
        raise TypeError("Only Vec4 objects can be assigned")
    self._data[index] = [value.x, value.y, value.z, value.w]

__str__()

Pretty representation as a list of Vec4 values.

Source code in ncca/ngl/vec4_array.py
152
153
154
155
def __str__(self) -> str:
    """Pretty representation as a list of Vec4 values."""
    vec_list = [Vec4(row[0], row[1], row[2], row[3]) for row in self._data]
    return str(vec_list)

append(value)

Append a Vec4 object to the array.

Parameters:
  • value (Vec4) –

    The Vec4 object to append.

Source code in ncca/ngl/vec4_array.py
 99
100
101
102
103
104
105
106
107
108
def append(self, value: Vec4) -> None:
    """Append a Vec4 object to the array.

    Args:
        value (Vec4): The Vec4 object to append.
    """
    if not isinstance(value, Vec4):
        raise TypeError("Only Vec4 objects can be appended")
    new_row = np.array([[value.x, value.y, value.z, value.w]], dtype=np.float32)
    self._data = np.vstack([self._data, new_row])

extend(values)

Extend the array with a list of Vec4 objects.

Parameters:
  • values (list) –

    A list of Vec4 objects to extend.

Source code in ncca/ngl/vec4_array.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def extend(self, values: "Iterable[Vec4]") -> None:
    """Extend the array with a list of Vec4 objects.

    Args:
        values (list): A list of Vec4 objects to extend.
    """
    if not all(isinstance(v, Vec4) for v in values):
        raise TypeError("All elements must be of type Vec4")

    new_rows = np.array([[v.x, v.y, v.z, v.w] for v in values], dtype=np.float32)
    if len(self._data) == 0:
        self._data = new_rows
    else:
        self._data = np.vstack([self._data, new_rows])

sizeof()

Return the size of the array in bytes.

Returns:
  • int( int ) –

    The size of the array in bytes.

Source code in ncca/ngl/vec4_array.py
157
158
159
160
161
162
163
def sizeof(self) -> int:
    """Return the size of the array in bytes.

    Returns:
        int: The size of the array in bytes.
    """
    return len(self._data) * Vec4.sizeof()

to_list()

Convert the array of Vec4 objects to a single flat list of floats.

Returns:
  • list( list[float] ) –

    A list of x, y, z, w components concatenated.

Source code in ncca/ngl/vec4_array.py
125
126
127
128
129
130
131
def to_list(self) -> list[float]:
    """Convert the array of Vec4 objects to a single flat list of floats.

    Returns:
        list: A list of x, y, z, w components concatenated.
    """
    return self._data.flatten().tolist()

to_numpy()

Convert the array of Vec4 objects to a numpy array.

This is the primary method for GPU data transfer.

Returns:
  • ndarray

    numpy.ndarray: A float32 numpy array of shape (N*4,) for GPU transfer.

Source code in ncca/ngl/vec4_array.py
133
134
135
136
137
138
139
140
141
def to_numpy(self) -> np.ndarray:
    """Convert the array of Vec4 objects to a numpy array.

    This is the primary method for GPU data transfer.

    Returns:
        numpy.ndarray: A float32 numpy array of shape (N*4,) for GPU transfer.
    """
    return self._data.flatten().copy()

to_tuple()

Return all components as one flat tuple of floats.

Source code in ncca/ngl/vec4_array.py
143
144
145
def to_tuple(self) -> tuple[float, ...]:
    """Return all components as one flat tuple of floats."""
    return tuple(float(v) for v in self._data.flatten())

Logging

ncca.ngl.logger is a ready-made logging.Logger shared by the whole library — it writes coloured output to the console and plain text to NGLDebug.log. Import it and use it directly:

from ncca.ngl import logger

logger.info("shader compiled")

setup_logger builds it, and is only worth calling yourself if you want a second, separately configured logger.

setup_logger

Create (or return) the "ngl" logger with file and coloured console handlers.

Source code in ncca/ngl/log.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def setup_logger() -> logging.Logger:
    """Create (or return) the "ngl" logger with file and coloured console handlers."""
    logger = logging.getLogger("ngl")
    if not logger.handlers:
        logger.setLevel(logging.DEBUG)
        file_handler = logging.FileHandler("NGLDebug.log", mode="w")
        console_handler = logging.StreamHandler(sys.stdout)

        file_formatter = logging.Formatter(
            "%(asctime)s - %(levelname)s - %(message)s",
            datefmt="%H:%M:%S",
        )
        console_formatter = ColoredFormatter(
            "%(asctime)s - %(levelname)s - %(message)s",
            datefmt="%H:%M:%S",
        )

        file_handler.setFormatter(file_formatter)
        console_handler.setFormatter(console_formatter)

        logger.addHandler(file_handler)
        logger.addHandler(console_handler)
    return logger