Math Classes

Auto-generated API reference for the math classes. For guided, example-driven introductions see the tutorials — in particular Vectors, Matrices, Quaternions, and Understanding the Method Names.

Vec2

Bases: VectorBase['Vec2']

A simple 2D vector class for graphics, using numpy for efficient operations.

Attributes:
  • x (float) –

    The x-coordinate of the vector.

  • y (float) –

    The y-coordinate of the vector.

Source code in ncca/ngl/vec2.py
 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
class Vec2(VectorBase["Vec2"]):
    """A simple 2D vector class for graphics, using numpy for efficient operations.

    Attributes:
        x (float): The x-coordinate of the vector.
        y (float): The y-coordinate of the vector.
    """

    DIMENSION = 2
    COMPONENT_NAMES = ("x", "y")
    DEFAULT_VALUES = (0.0, 0.0)

    __slots__ = ["_data"]

    def cross(self, rhs: "Vec2") -> float:
        """Cross product of two vectors a x b (2D version returns scalar).

        Args:
            rhs (Vec2): The right-hand side vector to cross product with.

        Returns:
            float: 2D cross product (perpendicular dot product).
        """
        return self._data[0] * rhs._data[1] - self._data[1] * rhs._data[0]

    def reflected(self, n: "Vec2") -> "Vec2":
        """Return a new vector reflected about a normal.

        Args:
            n (Vec2): The normal to reflect about.

        Returns:
            Vec2: A new vector that is the result of reflecting this vector about the normal.
        """
        d = self.dot(n)
        # I - 2.0 * dot(N, I) * N
        return Vec2(
            self._data[0] - 2.0 * d * n._data[0], self._data[1] - 2.0 * d * n._data[1]
        )

    def outer(self, rhs: "Vec2") -> "Mat2":
        """Outer product of two vectors a x b.

        Args:
            rhs (Vec2): The right-hand side vector to outer product with.

        Returns:
            Mat2: A new 2x2 matrix that is the result of the outer product.
        """
        from .mat2 import Mat2

        result = Mat2()
        result._data = np.outer(self._data, rhs._data).astype(np.float32)
        return result

    def __matmul__(self, rhs: "Mat2") -> "Vec2":
        """Vec2 @ Mat2 matrix multiplication.

        Args:
            rhs (Mat2): The matrix to multiply by.

        Returns:
            Vec2: A new vector that is the result of multiplying this vector by the matrix.
        """
        return Vec2(
            self._data[0] * rhs._data[0, 0] + self._data[1] * rhs._data[1, 0],
            self._data[0] * rhs._data[0, 1] + self._data[1] * rhs._data[1, 1],
        )

    def set(self, *args: float) -> None:
        """Set the x,y values of the vector.

        Args:
            *args: Component values (x, y).

        Raises:
            ValueError: If wrong number of arguments or they are not floats.
        """
        if len(args) != 2:
            raise ValueError(f"Vec2.set requires 2 arguments, got {len(args)}")
        try:
            self._data[0] = float(args[0])
            self._data[1] = float(args[1])
        except ValueError:
            raise ValueError(f"Vec2.set {args=} all need to be float")

__matmul__(rhs)

Vec2 @ Mat2 matrix multiplication.

Parameters:
  • rhs (Mat2) –

    The matrix to multiply by.

Returns:
  • Vec2( Vec2 ) –

    A new vector that is the result of multiplying this vector by the matrix.

Source code in ncca/ngl/vec2.py
71
72
73
74
75
76
77
78
79
80
81
82
83
def __matmul__(self, rhs: "Mat2") -> "Vec2":
    """Vec2 @ Mat2 matrix multiplication.

    Args:
        rhs (Mat2): The matrix to multiply by.

    Returns:
        Vec2: A new vector that is the result of multiplying this vector by the matrix.
    """
    return Vec2(
        self._data[0] * rhs._data[0, 0] + self._data[1] * rhs._data[1, 0],
        self._data[0] * rhs._data[0, 1] + self._data[1] * rhs._data[1, 1],
    )

cross(rhs)

Cross product of two vectors a x b (2D version returns scalar).

Parameters:
  • rhs (Vec2) –

    The right-hand side vector to cross product with.

Returns:
  • float( float ) –

    2D cross product (perpendicular dot product).

Source code in ncca/ngl/vec2.py
30
31
32
33
34
35
36
37
38
39
def cross(self, rhs: "Vec2") -> float:
    """Cross product of two vectors a x b (2D version returns scalar).

    Args:
        rhs (Vec2): The right-hand side vector to cross product with.

    Returns:
        float: 2D cross product (perpendicular dot product).
    """
    return self._data[0] * rhs._data[1] - self._data[1] * rhs._data[0]

outer(rhs)

Outer product of two vectors a x b.

Parameters:
  • rhs (Vec2) –

    The right-hand side vector to outer product with.

Returns:
  • Mat2( Mat2 ) –

    A new 2x2 matrix that is the result of the outer product.

Source code in ncca/ngl/vec2.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def outer(self, rhs: "Vec2") -> "Mat2":
    """Outer product of two vectors a x b.

    Args:
        rhs (Vec2): The right-hand side vector to outer product with.

    Returns:
        Mat2: A new 2x2 matrix that is the result of the outer product.
    """
    from .mat2 import Mat2

    result = Mat2()
    result._data = np.outer(self._data, rhs._data).astype(np.float32)
    return result

reflected(n)

Return a new vector reflected about a normal.

Parameters:
  • n (Vec2) –

    The normal to reflect about.

Returns:
  • Vec2( Vec2 ) –

    A new vector that is the result of reflecting this vector about the normal.

Source code in ncca/ngl/vec2.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def reflected(self, n: "Vec2") -> "Vec2":
    """Return a new vector reflected about a normal.

    Args:
        n (Vec2): The normal to reflect about.

    Returns:
        Vec2: A new vector that is the result of reflecting this vector about the normal.
    """
    d = self.dot(n)
    # I - 2.0 * dot(N, I) * N
    return Vec2(
        self._data[0] - 2.0 * d * n._data[0], self._data[1] - 2.0 * d * n._data[1]
    )

set(*args)

Set the x,y values of the vector.

Parameters:
  • *args (float, default: () ) –

    Component values (x, y).

Raises:
  • ValueError

    If wrong number of arguments or they are not floats.

Source code in ncca/ngl/vec2.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def set(self, *args: float) -> None:
    """Set the x,y values of the vector.

    Args:
        *args: Component values (x, y).

    Raises:
        ValueError: If wrong number of arguments or they are not floats.
    """
    if len(args) != 2:
        raise ValueError(f"Vec2.set requires 2 arguments, got {len(args)}")
    try:
        self._data[0] = float(args[0])
        self._data[1] = float(args[1])
    except ValueError:
        raise ValueError(f"Vec2.set {args=} all need to be float")

Vec3

Bases: VectorBase['Vec3']

A simple 3D vector class for 3D graphics, using numpy for efficient operations.

Attributes:
  • x (float) –

    The x-coordinate of the vector.

  • y (float) –

    The y-coordinate of the vector.

  • z (float) –

    The z-coordinate of the vector.

Source code in ncca/ngl/vec3.py
 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
class Vec3(VectorBase["Vec3"]):
    """A simple 3D vector class for 3D graphics, using numpy for efficient operations.

    Attributes:
        x (float): The x-coordinate of the vector.
        y (float): The y-coordinate of the vector.
        z (float): The z-coordinate of the vector.
    """

    DIMENSION = 3
    COMPONENT_NAMES = ("x", "y", "z")
    DEFAULT_VALUES = (0.0, 0.0, 0.0)

    __slots__ = ["_data"]

    def cross(self, rhs: "Vec3") -> "Vec3":
        """Cross product of two vectors a x b.

        Args:
            rhs (Vec3): The right-hand side vector to cross product with.

        Returns:
            Vec3: A new vector that is the result of the cross product.
        """
        result = Vec3()
        result._data = np.cross(self._data, rhs._data)
        return result

    def reflected(self, n: "Vec3") -> "Vec3":
        """Return a new vector reflected about a normal.

        Args:
            n (Vec3): The normal to reflect about.

        Returns:
            Vec3: A new vector that is the result of reflecting this vector about the normal.
        """
        d = self.dot(n)
        # I - 2.0 * dot(N, I) * N
        result = Vec3()
        result._data = self._data - 2.0 * d * n._data
        return result

    def outer(self, rhs: "Vec3") -> "Mat3":
        """Outer product of two vectors a x b.

        Args:
            rhs (Vec3): The right-hand side vector to outer product with.

        Returns:
            Mat3: A new 3x3 matrix that is the result of the outer product.
        """
        from .mat3 import Mat3

        result = Mat3()
        result._data = np.outer(self._data, rhs._data).astype(np.float32)
        return result

    def __matmul__(self, rhs: "Mat3") -> "Vec3":
        """Vec3 @ Mat3 matrix multiplication.

        Args:
            rhs (Mat3): The matrix to multiply by.

        Returns:
            Vec3: A new vector that is the result of multiplying this vector by the matrix.
        """
        result = Vec3()
        result._data = rhs._data.T @ self._data  # More efficient
        return result

    def set(self, *args: float) -> None:
        """Set the x,y,z values of the vector.

        Args:
            *args: Component values (x, y, z).

        Raises:
            ValueError: If wrong number of arguments or they are not floats.
        """
        if len(args) != 3:
            raise ValueError(f"Vec3.set requires 3 arguments, got {len(args)}")
        try:
            self._data[0] = float(args[0])
            self._data[1] = float(args[1])
            self._data[2] = float(args[2])
        except ValueError:
            raise ValueError(f"Vec3.set {args=} all need to be float")

__matmul__(rhs)

Vec3 @ Mat3 matrix multiplication.

Parameters:
  • rhs (Mat3) –

    The matrix to multiply by.

Returns:
  • Vec3( Vec3 ) –

    A new vector that is the result of multiplying this vector by the matrix.

Source code in ncca/ngl/vec3.py
74
75
76
77
78
79
80
81
82
83
84
85
def __matmul__(self, rhs: "Mat3") -> "Vec3":
    """Vec3 @ Mat3 matrix multiplication.

    Args:
        rhs (Mat3): The matrix to multiply by.

    Returns:
        Vec3: A new vector that is the result of multiplying this vector by the matrix.
    """
    result = Vec3()
    result._data = rhs._data.T @ self._data  # More efficient
    return result

cross(rhs)

Cross product of two vectors a x b.

Parameters:
  • rhs (Vec3) –

    The right-hand side vector to cross product with.

Returns:
  • Vec3( Vec3 ) –

    A new vector that is the result of the cross product.

Source code in ncca/ngl/vec3.py
31
32
33
34
35
36
37
38
39
40
41
42
def cross(self, rhs: "Vec3") -> "Vec3":
    """Cross product of two vectors a x b.

    Args:
        rhs (Vec3): The right-hand side vector to cross product with.

    Returns:
        Vec3: A new vector that is the result of the cross product.
    """
    result = Vec3()
    result._data = np.cross(self._data, rhs._data)
    return result

outer(rhs)

Outer product of two vectors a x b.

Parameters:
  • rhs (Vec3) –

    The right-hand side vector to outer product with.

Returns:
  • Mat3( Mat3 ) –

    A new 3x3 matrix that is the result of the outer product.

Source code in ncca/ngl/vec3.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def outer(self, rhs: "Vec3") -> "Mat3":
    """Outer product of two vectors a x b.

    Args:
        rhs (Vec3): The right-hand side vector to outer product with.

    Returns:
        Mat3: A new 3x3 matrix that is the result of the outer product.
    """
    from .mat3 import Mat3

    result = Mat3()
    result._data = np.outer(self._data, rhs._data).astype(np.float32)
    return result

reflected(n)

Return a new vector reflected about a normal.

Parameters:
  • n (Vec3) –

    The normal to reflect about.

Returns:
  • Vec3( Vec3 ) –

    A new vector that is the result of reflecting this vector about the normal.

Source code in ncca/ngl/vec3.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def reflected(self, n: "Vec3") -> "Vec3":
    """Return a new vector reflected about a normal.

    Args:
        n (Vec3): The normal to reflect about.

    Returns:
        Vec3: A new vector that is the result of reflecting this vector about the normal.
    """
    d = self.dot(n)
    # I - 2.0 * dot(N, I) * N
    result = Vec3()
    result._data = self._data - 2.0 * d * n._data
    return result

set(*args)

Set the x,y,z values of the vector.

Parameters:
  • *args (float, default: () ) –

    Component values (x, y, z).

Raises:
  • ValueError

    If wrong number of arguments or they are not floats.

Source code in ncca/ngl/vec3.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def set(self, *args: float) -> None:
    """Set the x,y,z values of the vector.

    Args:
        *args: Component values (x, y, z).

    Raises:
        ValueError: If wrong number of arguments or they are not floats.
    """
    if len(args) != 3:
        raise ValueError(f"Vec3.set requires 3 arguments, got {len(args)}")
    try:
        self._data[0] = float(args[0])
        self._data[1] = float(args[1])
        self._data[2] = float(args[2])
    except ValueError:
        raise ValueError(f"Vec3.set {args=} all need to be float")

Vec4

Bases: VectorBase['Vec4']

A simple 4D vector class for graphics, using numpy for efficient operations.

Attributes:
  • x (float) –

    The x-coordinate of the vector.

  • y (float) –

    The y-coordinate of the vector.

  • z (float) –

    The z-coordinate of the vector.

  • w (float) –

    The w-coordinate of the vector.

Source code in ncca/ngl/vec4.py
 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
class Vec4(VectorBase["Vec4"]):
    """A simple 4D vector class for graphics, using numpy for efficient operations.

    Attributes:
        x (float): The x-coordinate of the vector.
        y (float): The y-coordinate of the vector.
        z (float): The z-coordinate of the vector.
        w (float): The w-coordinate of the vector.
    """

    DIMENSION = 4
    COMPONENT_NAMES = ("x", "y", "z", "w")
    DEFAULT_VALUES = (0.0, 0.0, 0.0, 1.0)

    __slots__ = ["_data"]

    def cross(self, rhs: "Vec4") -> "Vec4":
        """Cross product of two vectors a x b (4D version uses first 3 components).

        Args:
            rhs (Vec4): The right-hand side vector to cross product with.

        Returns:
            Vec4: A new vector that is the result of the cross product.
        """
        result = Vec4()
        # Cross product only makes sense for 3D vectors, use first 3 components
        result._data[:3] = np.cross(self._data[:3], rhs._data[:3])
        result._data[3] = 0.0
        return result

    def reflected(self, n: "Vec4") -> "Vec4":
        """Return a new vector reflected about a normal.

        Args:
            n (Vec4): The normal to reflect about.

        Returns:
            Vec4: A new vector that is the result of reflecting this vector about the normal.
        """
        d = self.dot(n)
        # I - 2.0 * dot(N, I) * N
        result = Vec4()
        result._data = self._data - 2.0 * d * n._data
        return result

    def outer(self, rhs: "Vec4") -> "Mat4":
        """Outer product of two vectors a x b.

        Args:
            rhs (Vec4): The right-hand side vector to outer product with.

        Returns:
            Mat4: A new 4x4 matrix that is the result of the outer product.
        """
        from .mat4 import Mat4

        result = Mat4()
        result._data = np.outer(self._data, rhs._data).astype(np.float32)
        return result

    def __matmul__(self, rhs: "Mat4") -> "Vec4":
        """Vec4 @ Mat4 matrix multiplication.

        Args:
            rhs (Mat4): The matrix to multiply by.

        Returns:
            Vec4: A new vector that is the result of multiplying this vector by the matrix.
        """
        return Vec4(*(self._data @ rhs._data))

    def set(self, *args: float) -> None:
        """Set the x,y,z,w values of the vector.

        Args:
            *args: Component values (x, y, z, w). w defaults to 1.0 if not provided.

        Raises:
            ValueError: If wrong number of arguments or they are not floats.
        """
        if len(args) == 3:
            # Allow (x, y, z) with default w=1.0 for backward compatibility
            args = args + (1.0,)
        elif len(args) != 4:
            raise ValueError(f"Vec4.set requires 3 or 4 arguments, got {len(args)}")

        try:
            for i in range(4):
                self._data[i] = float(args[i])
        except ValueError:
            raise ValueError(f"Vec4.set {args=} all need to be float")

__matmul__(rhs)

Vec4 @ Mat4 matrix multiplication.

Parameters:
  • rhs (Mat4) –

    The matrix to multiply by.

Returns:
  • Vec4( Vec4 ) –

    A new vector that is the result of multiplying this vector by the matrix.

Source code in ncca/ngl/vec4.py
77
78
79
80
81
82
83
84
85
86
def __matmul__(self, rhs: "Mat4") -> "Vec4":
    """Vec4 @ Mat4 matrix multiplication.

    Args:
        rhs (Mat4): The matrix to multiply by.

    Returns:
        Vec4: A new vector that is the result of multiplying this vector by the matrix.
    """
    return Vec4(*(self._data @ rhs._data))

cross(rhs)

Cross product of two vectors a x b (4D version uses first 3 components).

Parameters:
  • rhs (Vec4) –

    The right-hand side vector to cross product with.

Returns:
  • Vec4( Vec4 ) –

    A new vector that is the result of the cross product.

Source code in ncca/ngl/vec4.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def cross(self, rhs: "Vec4") -> "Vec4":
    """Cross product of two vectors a x b (4D version uses first 3 components).

    Args:
        rhs (Vec4): The right-hand side vector to cross product with.

    Returns:
        Vec4: A new vector that is the result of the cross product.
    """
    result = Vec4()
    # Cross product only makes sense for 3D vectors, use first 3 components
    result._data[:3] = np.cross(self._data[:3], rhs._data[:3])
    result._data[3] = 0.0
    return result

outer(rhs)

Outer product of two vectors a x b.

Parameters:
  • rhs (Vec4) –

    The right-hand side vector to outer product with.

Returns:
  • Mat4( Mat4 ) –

    A new 4x4 matrix that is the result of the outer product.

Source code in ncca/ngl/vec4.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def outer(self, rhs: "Vec4") -> "Mat4":
    """Outer product of two vectors a x b.

    Args:
        rhs (Vec4): The right-hand side vector to outer product with.

    Returns:
        Mat4: A new 4x4 matrix that is the result of the outer product.
    """
    from .mat4 import Mat4

    result = Mat4()
    result._data = np.outer(self._data, rhs._data).astype(np.float32)
    return result

reflected(n)

Return a new vector reflected about a normal.

Parameters:
  • n (Vec4) –

    The normal to reflect about.

Returns:
  • Vec4( Vec4 ) –

    A new vector that is the result of reflecting this vector about the normal.

Source code in ncca/ngl/vec4.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def reflected(self, n: "Vec4") -> "Vec4":
    """Return a new vector reflected about a normal.

    Args:
        n (Vec4): The normal to reflect about.

    Returns:
        Vec4: A new vector that is the result of reflecting this vector about the normal.
    """
    d = self.dot(n)
    # I - 2.0 * dot(N, I) * N
    result = Vec4()
    result._data = self._data - 2.0 * d * n._data
    return result

set(*args)

Set the x,y,z,w values of the vector.

Parameters:
  • *args (float, default: () ) –

    Component values (x, y, z, w). w defaults to 1.0 if not provided.

Raises:
  • ValueError

    If wrong number of arguments or they are not floats.

Source code in ncca/ngl/vec4.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def set(self, *args: float) -> None:
    """Set the x,y,z,w values of the vector.

    Args:
        *args: Component values (x, y, z, w). w defaults to 1.0 if not provided.

    Raises:
        ValueError: If wrong number of arguments or they are not floats.
    """
    if len(args) == 3:
        # Allow (x, y, z) with default w=1.0 for backward compatibility
        args = args + (1.0,)
    elif len(args) != 4:
        raise ValueError(f"Vec4.set requires 3 or 4 arguments, got {len(args)}")

    try:
        for i in range(4):
            self._data[i] = float(args[i])
    except ValueError:
        raise ValueError(f"Vec4.set {args=} all need to be float")

Mat2

Bases: MatrixBase

A 2x2 matrix for 2D transforms.

Source code in ncca/ngl/mat2.py
 6
 7
 8
 9
10
11
12
13
14
class Mat2(MatrixBase):
    """A 2x2 matrix for 2D transforms."""

    SIZE = 2

    def _vec_type(self) -> type:
        from .vec2 import Vec2

        return Vec2

Mat3

Bases: MatrixBase

A 3x3 matrix for basic affine transforms.

Source code in ncca/ngl/mat3.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
class Mat3(MatrixBase):
    """A 3x3 matrix for basic affine transforms."""

    SIZE = 3

    def _vec_type(self) -> type:
        from .vec3 import Vec3

        return Vec3

    @classmethod
    def scale(cls, x: float, y: float, z: float) -> "Mat3":
        """Return a scale matrix with the diagonal set to (x, y, z)."""
        a = cls()
        a._data[0, 0] = x
        a._data[1, 1] = y
        a._data[2, 2] = z
        return a

    @classmethod
    def rotate_x(cls, angle: float) -> "Mat3":
        """Return a rotation matrix around the X axis by angle degrees."""
        a = cls()
        beta = math.radians(angle)
        sr = math.sin(beta)
        cr = math.cos(beta)
        a._data[1, 1] = cr
        a._data[1, 2] = sr
        a._data[2, 1] = -sr
        a._data[2, 2] = cr
        return a

    @classmethod
    def rotate_y(cls, angle: float) -> "Mat3":
        """Return a rotation matrix around the Y axis by angle degrees."""
        a = cls()
        beta = math.radians(angle)
        sr = math.sin(beta)
        cr = math.cos(beta)
        a._data[0, 0] = cr
        a._data[0, 2] = -sr
        a._data[2, 0] = sr
        a._data[2, 2] = cr
        return a

    @classmethod
    def rotate_z(cls, angle: float) -> "Mat3":
        """Return a rotation matrix around the Z axis by angle degrees."""
        a = cls()
        beta = math.radians(angle)
        sr = math.sin(beta)
        cr = math.cos(beta)
        a._data[0, 0] = cr
        a._data[0, 1] = sr
        a._data[1, 0] = -sr
        a._data[1, 1] = cr
        return a

    @classmethod
    def from_mat4(cls, mat4: "Mat4") -> "Mat3":
        """Return the upper-left 3x3 of a Mat4."""
        result = cls()
        result._data = mat4._data[:3, :3].copy()
        return result

from_mat4(mat4) classmethod

Return the upper-left 3x3 of a Mat4.

Source code in ncca/ngl/mat3.py
70
71
72
73
74
75
@classmethod
def from_mat4(cls, mat4: "Mat4") -> "Mat3":
    """Return the upper-left 3x3 of a Mat4."""
    result = cls()
    result._data = mat4._data[:3, :3].copy()
    return result

rotate_x(angle) classmethod

Return a rotation matrix around the X axis by angle degrees.

Source code in ncca/ngl/mat3.py
31
32
33
34
35
36
37
38
39
40
41
42
@classmethod
def rotate_x(cls, angle: float) -> "Mat3":
    """Return a rotation matrix around the X axis by angle degrees."""
    a = cls()
    beta = math.radians(angle)
    sr = math.sin(beta)
    cr = math.cos(beta)
    a._data[1, 1] = cr
    a._data[1, 2] = sr
    a._data[2, 1] = -sr
    a._data[2, 2] = cr
    return a

rotate_y(angle) classmethod

Return a rotation matrix around the Y axis by angle degrees.

Source code in ncca/ngl/mat3.py
44
45
46
47
48
49
50
51
52
53
54
55
@classmethod
def rotate_y(cls, angle: float) -> "Mat3":
    """Return a rotation matrix around the Y axis by angle degrees."""
    a = cls()
    beta = math.radians(angle)
    sr = math.sin(beta)
    cr = math.cos(beta)
    a._data[0, 0] = cr
    a._data[0, 2] = -sr
    a._data[2, 0] = sr
    a._data[2, 2] = cr
    return a

rotate_z(angle) classmethod

Return a rotation matrix around the Z axis by angle degrees.

Source code in ncca/ngl/mat3.py
57
58
59
60
61
62
63
64
65
66
67
68
@classmethod
def rotate_z(cls, angle: float) -> "Mat3":
    """Return a rotation matrix around the Z axis by angle degrees."""
    a = cls()
    beta = math.radians(angle)
    sr = math.sin(beta)
    cr = math.cos(beta)
    a._data[0, 0] = cr
    a._data[0, 1] = sr
    a._data[1, 0] = -sr
    a._data[1, 1] = cr
    return a

scale(x, y, z) classmethod

Return a scale matrix with the diagonal set to (x, y, z).

Source code in ncca/ngl/mat3.py
22
23
24
25
26
27
28
29
@classmethod
def scale(cls, x: float, y: float, z: float) -> "Mat3":
    """Return a scale matrix with the diagonal set to (x, y, z)."""
    a = cls()
    a._data[0, 0] = x
    a._data[1, 1] = y
    a._data[2, 2] = z
    return a

Mat4

Bases: MatrixBase

A 4x4 matrix for 3D affine and projective transforms.

Source code in ncca/ngl/mat4.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
class Mat4(MatrixBase):
    """A 4x4 matrix for 3D affine and projective transforms."""

    SIZE = 4

    def _vec_type(self) -> type:
        from .vec4 import Vec4

        return Vec4

    @classmethod
    def scale(cls, x: float, y: float, z: float) -> "Mat4":
        """Return a scale matrix with the diagonal set to (x, y, z, 1)."""
        a = cls()
        a._data[0, 0] = x
        a._data[1, 1] = y
        a._data[2, 2] = z
        return a

    @classmethod
    def translate(cls, x: float, y: float, z: float) -> "Mat4":
        """Return a translation matrix."""
        a = cls()
        a._data[3, 0] = x
        a._data[3, 1] = y
        a._data[3, 2] = z
        return a

    @classmethod
    def rotate_x(cls, angle: float) -> "Mat4":
        """Return a rotation matrix around the X axis by angle degrees."""
        a = cls()
        beta = math.radians(angle)
        sr = math.sin(beta)
        cr = math.cos(beta)
        a._data[1, 1] = cr
        a._data[1, 2] = sr
        a._data[2, 1] = -sr
        a._data[2, 2] = cr
        return a

    @classmethod
    def rotate_y(cls, angle: float) -> "Mat4":
        """Return a rotation matrix around the Y axis by angle degrees."""
        a = cls()
        beta = math.radians(angle)
        sr = math.sin(beta)
        cr = math.cos(beta)
        a._data[0, 0] = cr
        a._data[0, 2] = -sr
        a._data[2, 0] = sr
        a._data[2, 2] = cr
        return a

    @classmethod
    def rotate_z(cls, angle: float) -> "Mat4":
        """Return a rotation matrix around the Z axis by angle degrees."""
        a = cls()
        beta = math.radians(angle)
        sr = math.sin(beta)
        cr = math.cos(beta)
        a._data[0, 0] = cr
        a._data[0, 1] = sr
        a._data[1, 0] = -sr
        a._data[1, 1] = cr
        return a

    @classmethod
    def from_mat3(cls, mat3: "Mat3") -> "Mat4":
        """Return a Mat4 with the given Mat3 as its upper-left block."""
        result = cls()
        result._data[:3, :3] = mat3._data
        return result

from_mat3(mat3) classmethod

Return a Mat4 with the given Mat3 as its upper-left block.

Source code in ncca/ngl/mat4.py
79
80
81
82
83
84
@classmethod
def from_mat3(cls, mat3: "Mat3") -> "Mat4":
    """Return a Mat4 with the given Mat3 as its upper-left block."""
    result = cls()
    result._data[:3, :3] = mat3._data
    return result

rotate_x(angle) classmethod

Return a rotation matrix around the X axis by angle degrees.

Source code in ncca/ngl/mat4.py
40
41
42
43
44
45
46
47
48
49
50
51
@classmethod
def rotate_x(cls, angle: float) -> "Mat4":
    """Return a rotation matrix around the X axis by angle degrees."""
    a = cls()
    beta = math.radians(angle)
    sr = math.sin(beta)
    cr = math.cos(beta)
    a._data[1, 1] = cr
    a._data[1, 2] = sr
    a._data[2, 1] = -sr
    a._data[2, 2] = cr
    return a

rotate_y(angle) classmethod

Return a rotation matrix around the Y axis by angle degrees.

Source code in ncca/ngl/mat4.py
53
54
55
56
57
58
59
60
61
62
63
64
@classmethod
def rotate_y(cls, angle: float) -> "Mat4":
    """Return a rotation matrix around the Y axis by angle degrees."""
    a = cls()
    beta = math.radians(angle)
    sr = math.sin(beta)
    cr = math.cos(beta)
    a._data[0, 0] = cr
    a._data[0, 2] = -sr
    a._data[2, 0] = sr
    a._data[2, 2] = cr
    return a

rotate_z(angle) classmethod

Return a rotation matrix around the Z axis by angle degrees.

Source code in ncca/ngl/mat4.py
66
67
68
69
70
71
72
73
74
75
76
77
@classmethod
def rotate_z(cls, angle: float) -> "Mat4":
    """Return a rotation matrix around the Z axis by angle degrees."""
    a = cls()
    beta = math.radians(angle)
    sr = math.sin(beta)
    cr = math.cos(beta)
    a._data[0, 0] = cr
    a._data[0, 1] = sr
    a._data[1, 0] = -sr
    a._data[1, 1] = cr
    return a

scale(x, y, z) classmethod

Return a scale matrix with the diagonal set to (x, y, z, 1).

Source code in ncca/ngl/mat4.py
22
23
24
25
26
27
28
29
@classmethod
def scale(cls, x: float, y: float, z: float) -> "Mat4":
    """Return a scale matrix with the diagonal set to (x, y, z, 1)."""
    a = cls()
    a._data[0, 0] = x
    a._data[1, 1] = y
    a._data[2, 2] = z
    return a

translate(x, y, z) classmethod

Return a translation matrix.

Source code in ncca/ngl/mat4.py
31
32
33
34
35
36
37
38
@classmethod
def translate(cls, x: float, y: float, z: float) -> "Mat4":
    """Return a translation matrix."""
    a = cls()
    a._data[3, 0] = x
    a._data[3, 1] = y
    a._data[3, 2] = z
    return a

Quaternion

A quaternion for representing and composing 3D rotations.

Attributes:
  • s (float) –

    The scalar part of the quaternion.

  • x (float) –

    The x-coordinate of the vector part of the quaternion.

  • y (float) –

    The y-coordinate of the vector part of the quaternion.

  • z (float) –

    The z-coordinate of the vector part of the quaternion.

Source code in ncca/ngl/quaternion.py
 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
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
class Quaternion:
    """A quaternion for representing and composing 3D rotations.

    Attributes:
        s (float): The scalar part of the quaternion.
        x (float): The x-coordinate of the vector part of the quaternion.
        y (float): The y-coordinate of the vector part of the quaternion.
        z (float): The z-coordinate of the vector part of the quaternion.
    """

    __slots__ = ("_data",)  # Store as [s, x, y, z]

    def __init__(
        self, s: float = 1.0, x: float = 0.0, y: float = 0.0, z: float = 0.0
    ) -> None:
        """Initializes a new instance of the Quaternion class.

        Args:
            s (float): The scalar part of the quaternion.
            x (float): The x-coordinate of the vector part of the quaternion.
            y (float): The y-coordinate of the vector part of the quaternion.
            z (float): The z-coordinate of the vector part of the quaternion.
        """
        self._data = np.array(
            [float(s), float(x), float(y), float(z)], dtype=np.float32
        )

    @classmethod
    def from_mat4(cls, mat: "Mat4") -> "Quaternion":
        """Creates a new Quaternion from a Mat4 rotation matrix.

        Args:
            mat (Mat4): The rotation matrix to convert.

        Returns:
            Quaternion: A new Quaternion representing the rotation matrix.
        """
        matrix = mat.to_list()
        T = 1.0 + matrix[0] + matrix[5] + matrix[10]
        if T > 0.00000001:  # to avoid large distortions!
            scale = math.sqrt(T) * 2.0
            x = (matrix[6] - matrix[9]) / scale
            y = (matrix[8] - matrix[2]) / scale
            z = (matrix[1] - matrix[4]) / scale
            s = 0.25 * scale
        elif matrix[0] > matrix[5] and matrix[0] > matrix[10]:
            scale = math.sqrt(1.0 + matrix[0] - matrix[5] - matrix[10]) * 2.0
            x = 0.25 * scale
            y = (matrix[4] + matrix[1]) / scale
            z = (matrix[2] + matrix[8]) / scale
            s = (matrix[6] - matrix[9]) / scale
        elif matrix[5] > matrix[10]:
            scale = math.sqrt(1.0 + matrix[5] - matrix[0] - matrix[10]) * 2.0
            x = (matrix[4] + matrix[1]) / scale
            y = 0.25 * scale
            z = (matrix[9] + matrix[6]) / scale
            s = (matrix[8] - matrix[2]) / scale
        else:
            scale = math.sqrt(1.0 + matrix[10] - matrix[0] - matrix[5]) * 2.0
            x = (matrix[8] + matrix[2]) / scale
            y = (matrix[9] + matrix[6]) / scale
            z = 0.25 * scale
            s = (matrix[1] - matrix[4]) / scale

        return cls(s, x, y, z)

    @classmethod
    def from_axis_angle(cls, axis: "Vec3", angle: float) -> "Quaternion":
        """Creates a new Quaternion from an axis and angle.

        Args:
            axis (Vec3): The axis of rotation.
            angle (float): The angle of rotation in degrees.

        Returns:
            Quaternion: A new Quaternion representing the rotation.
        """
        angle_rad = math.radians(angle)
        half_angle = angle_rad * 0.5
        s = math.cos(half_angle)
        sin_half_angle = math.sin(half_angle)
        x = axis.x * sin_half_angle
        y = axis.y * sin_half_angle
        z = axis.z * sin_half_angle
        return cls(s, x, y, z)

    def __add__(self, rhs: "Quaternion") -> "Quaternion":
        """Quaternion addition a+b, component-wise."""
        result = Quaternion()
        result._data = self._data + rhs._data
        return result

    def __sub__(self, rhs: "Quaternion") -> "Quaternion":
        """Quaternion subtraction a-b, component-wise."""
        result = Quaternion()
        result._data = self._data - rhs._data
        return result

    def __matmul__(self, rhs: "Quaternion") -> "Quaternion":
        """Quaternion product (Hamilton), returning a new quaternion."""
        if not isinstance(rhs, Quaternion):
            raise TypeError("@ requires a Quaternion")
        s1, x1, y1, z1 = self._data
        s2, x2, y2, z2 = rhs._data
        return Quaternion(
            s1 * s2 - x1 * x2 - y1 * y2 - z1 * z2,
            s1 * x2 + x1 * s2 + y1 * z2 - z1 * y2,
            s1 * y2 - x1 * z2 + y1 * s2 + z1 * x2,
            s1 * z2 + x1 * y2 - y1 * x2 + z1 * s2,
        )

    def __mul__(self, rhs: "float | int | Vec3") -> "Quaternion | Vec3":
        """Scalar scale or Vec3 rotation. Quaternion product uses @."""
        if isinstance(rhs, Quaternion):
            raise TypeError("use q1 @ q2 for the quaternion product")
        if isinstance(rhs, (int, float)):
            result = Quaternion()
            result._data = self._data * np.float32(rhs)
            return result
        if isinstance(rhs, Vec3):
            # Quaternion-vector multiplication (rotate vector by quaternion)
            qw = self.s
            qx = self.x
            qy = self.y
            qz = self.z

            vx = rhs.x
            vy = rhs.y
            vz = rhs.z

            # pq (quaternion * pure quaternion from vector)
            pw = -qx * vx - qy * vy - qz * vz
            px = qw * vx + qy * vz - qz * vy
            py = qw * vy - qx * vz + qz * vx
            pz = qw * vz + qx * vy - qy * vx

            # pqp* (result * conjugate of quaternion)
            return Vec3(
                -pw * qx + px * qw - py * qz + pz * qy,
                -pw * qy + px * qz + py * qw - pz * qx,
                -pw * qz - px * qy + py * qx + pz * qw,
            )
        raise TypeError(f"cannot multiply Quaternion by {type(rhs)}")

    def __rmul__(self, rhs: float) -> "Quaternion":
        """Scalar scale (right operand)."""
        if isinstance(rhs, (int, float)):
            return self * rhs
        raise TypeError(f"cannot multiply {type(rhs)} by Quaternion")

    def __neg__(self) -> "Quaternion":
        """Return a new quaternion with every component negated."""
        result = Quaternion()
        result._data = -self._data
        return result

    def __truediv__(self, rhs: float | int) -> "Quaternion":
        """Scalar division, returning a new quaternion.

        Raises:
            ZeroDivisionError: If rhs is zero.
            TypeError: If rhs is not a scalar.
        """
        if isinstance(rhs, (int, float)):
            if rhs == 0:
                raise ZeroDivisionError("division by zero")
            result = Quaternion()
            result._data = self._data / np.float32(rhs)
            return result
        raise TypeError(f"cannot divide Quaternion by {type(rhs)}")

    def __getitem__(self, index: int) -> float:
        """Return the component at index (0=s, 1=x, 2=y, 3=z).

        Raises:
            IndexError: If the index is out of range.
        """
        if index < 0 or index >= 4:
            raise IndexError("Index out of range. Valid indices are 0, 1, 2, 3.")
        return float(self._data[index])

    def normalized(self) -> "Quaternion":
        """Return a new unit-length quaternion.

        Raises:
            ZeroDivisionError: If the quaternion has zero length.
        """
        length = self.length()
        if math.isclose(length, 0.0):
            raise ZeroDivisionError("Quaternion.normalized: length is zero")
        result = Quaternion()
        result._data = self._data / np.float32(length)
        return result

    def length(self) -> float:
        """Return the length/magnitude of the quaternion."""
        return float(np.linalg.norm(self._data))

    def length_squared(self) -> float:
        """Return the squared magnitude."""
        return float(np.dot(self._data, self._data))

    def conjugate(self) -> "Quaternion":
        """Return the conjugate of the quaternion (s, -x, -y, -z)."""
        result = Quaternion()
        result._data = self._data.copy()
        result._data[1:] *= -1  # Negate x, y, z components
        return result

    def inverse(self) -> "Quaternion":
        """Return the multiplicative inverse (conjugate / |q|^2)."""
        lsq = self.length_squared()
        if math.isclose(lsq, 0.0):
            raise ZeroDivisionError("Quaternion.inverse: zero quaternion")
        result = self.conjugate()
        result._data = result._data / np.float32(lsq)
        return result

    def dot(self, rhs: "Quaternion") -> float:
        """Dot product of two quaternions."""
        return float(np.dot(self._data, rhs._data))

    def slerp(self, rhs: "Quaternion", t: float) -> "Quaternion":
        """Spherical linear interpolation from self to rhs at t in [0, 1]."""
        dot = float(np.dot(self._data, rhs._data))
        rhs_data = rhs._data.copy()
        if dot < 0.0:
            dot = -dot
            rhs_data = -rhs_data
        if dot > 0.9995:
            data = self._data + np.float32(t) * (rhs_data - self._data)
            data = data / np.linalg.norm(data)
        else:
            theta0 = math.acos(max(-1.0, min(1.0, dot)))
            theta = theta0 * t
            s0 = math.cos(theta) - dot * math.sin(theta) / math.sin(theta0)
            s1 = math.sin(theta) / math.sin(theta0)
            data = np.float32(s0) * self._data + np.float32(s1) * rhs_data
        result = Quaternion()
        result._data = data.astype(np.float32)
        return result

    def to_mat4(self) -> Mat4:
        """Return the equivalent rotation matrix (row-vector convention)."""
        s, x, y, z = (float(v) for v in self._data)
        m = Mat4()
        m._data[0, 0] = 1.0 - 2.0 * (y * y + z * z)
        m._data[0, 1] = 2.0 * (x * y + s * z)
        m._data[0, 2] = 2.0 * (x * z - s * y)
        m._data[1, 0] = 2.0 * (x * y - s * z)
        m._data[1, 1] = 1.0 - 2.0 * (x * x + z * z)
        m._data[1, 2] = 2.0 * (y * z + s * x)
        m._data[2, 0] = 2.0 * (x * z + s * y)
        m._data[2, 1] = 2.0 * (y * z - s * x)
        m._data[2, 2] = 1.0 - 2.0 * (x * x + y * y)
        return m

    def set(self, s: float, x: float, y: float, z: float) -> None:
        """Set all four components."""
        self._data[:] = (float(s), float(x), float(y), float(z))

    def copy(self) -> "Quaternion":
        """Return a new quaternion with the same values."""
        return Quaternion(*self._data)

    def to_numpy(self) -> np.ndarray:
        """Return the quaternion as a numpy array [s, x, y, z]."""
        return self._data.copy()

    def to_list(self) -> list[float]:
        """Return the quaternion as a list [s, x, y, z]."""
        return self._data.tolist()

    def to_tuple(self) -> tuple[float, float, float, float]:
        """Return (s, x, y, z) as plain floats."""
        return tuple(float(v) for v in self._data)

    @classmethod
    def from_list(cls, lst: list[float]) -> "Quaternion":
        """Create from [s, x, y, z]."""
        if len(lst) != 4:
            raise ValueError("Quaternion.from_list requires 4 values")
        return cls(*lst)

    @classmethod
    def from_numpy(cls, arr: np.ndarray) -> "Quaternion":
        """Create from an array [s, x, y, z]."""
        arr = np.asarray(arr, dtype=np.float32)
        if arr.shape != (4,):
            raise ValueError("Quaternion.from_numpy requires shape (4,)")
        return cls(*arr)

    def __eq__(self, rhs: object) -> bool:
        """Quaternion comparison a==b using numpy.allclose."""
        if not isinstance(rhs, Quaternion):
            return NotImplemented
        return bool(np.allclose(self._data, rhs._data, rtol=1e-5, atol=1e-6))

    def __ne__(self, rhs: object) -> bool:
        """Quaternion comparison a!=b using numpy.allclose."""
        result = self.__eq__(rhs)
        return result if result is NotImplemented else not result

    def __hash__(self) -> int:
        """Compute hash for use in sets and dictionaries."""
        from .util import hash_combine

        seed = 0
        for v in self._data:
            seed = hash_combine(seed, hash(float(np.float32(v))))
        return seed

    def __len__(self) -> int:
        """Return the number of components (always 4)."""
        return 4

    def __iter__(self) -> Generator[float, None, None]:
        """Yield the components in order (s, x, y, z)."""
        return iter(self._data.tolist())

    def __repr__(self) -> str:
        """Eval-able representation, e.g. Quaternion(1.0, 0.0, 0.0, 0.0)."""
        args = ", ".join(repr(float(v)) for v in self._data)
        return f"Quaternion({args})"

    def __str__(self) -> str:
        """Pretty representation, e.g. Quaternion(1.0, [0.0, 0.0, 0.0])."""
        s, x, y, z = (float(v) for v in self._data)
        return f"Quaternion({s}, [{x}, {y}, {z}])"

__add__(rhs)

Quaternion addition a+b, component-wise.

Source code in ncca/ngl/quaternion.py
107
108
109
110
111
def __add__(self, rhs: "Quaternion") -> "Quaternion":
    """Quaternion addition a+b, component-wise."""
    result = Quaternion()
    result._data = self._data + rhs._data
    return result

__eq__(rhs)

Quaternion comparison a==b using numpy.allclose.

Source code in ncca/ngl/quaternion.py
313
314
315
316
317
def __eq__(self, rhs: object) -> bool:
    """Quaternion comparison a==b using numpy.allclose."""
    if not isinstance(rhs, Quaternion):
        return NotImplemented
    return bool(np.allclose(self._data, rhs._data, rtol=1e-5, atol=1e-6))

__getitem__(index)

Return the component at index (0=s, 1=x, 2=y, 3=z).

Raises:
  • IndexError

    If the index is out of range.

Source code in ncca/ngl/quaternion.py
192
193
194
195
196
197
198
199
200
def __getitem__(self, index: int) -> float:
    """Return the component at index (0=s, 1=x, 2=y, 3=z).

    Raises:
        IndexError: If the index is out of range.
    """
    if index < 0 or index >= 4:
        raise IndexError("Index out of range. Valid indices are 0, 1, 2, 3.")
    return float(self._data[index])

__hash__()

Compute hash for use in sets and dictionaries.

Source code in ncca/ngl/quaternion.py
324
325
326
327
328
329
330
331
def __hash__(self) -> int:
    """Compute hash for use in sets and dictionaries."""
    from .util import hash_combine

    seed = 0
    for v in self._data:
        seed = hash_combine(seed, hash(float(np.float32(v))))
    return seed

__init__(s=1.0, x=0.0, y=0.0, z=0.0)

Initializes a new instance of the Quaternion class.

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

    The scalar part of the quaternion.

  • x (float, default: 0.0 ) –

    The x-coordinate of the vector part of the quaternion.

  • y (float, default: 0.0 ) –

    The y-coordinate of the vector part of the quaternion.

  • z (float, default: 0.0 ) –

    The z-coordinate of the vector part of the quaternion.

Source code in ncca/ngl/quaternion.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def __init__(
    self, s: float = 1.0, x: float = 0.0, y: float = 0.0, z: float = 0.0
) -> None:
    """Initializes a new instance of the Quaternion class.

    Args:
        s (float): The scalar part of the quaternion.
        x (float): The x-coordinate of the vector part of the quaternion.
        y (float): The y-coordinate of the vector part of the quaternion.
        z (float): The z-coordinate of the vector part of the quaternion.
    """
    self._data = np.array(
        [float(s), float(x), float(y), float(z)], dtype=np.float32
    )

__iter__()

Yield the components in order (s, x, y, z).

Source code in ncca/ngl/quaternion.py
337
338
339
def __iter__(self) -> Generator[float, None, None]:
    """Yield the components in order (s, x, y, z)."""
    return iter(self._data.tolist())

__len__()

Return the number of components (always 4).

Source code in ncca/ngl/quaternion.py
333
334
335
def __len__(self) -> int:
    """Return the number of components (always 4)."""
    return 4

__matmul__(rhs)

Quaternion product (Hamilton), returning a new quaternion.

Source code in ncca/ngl/quaternion.py
119
120
121
122
123
124
125
126
127
128
129
130
def __matmul__(self, rhs: "Quaternion") -> "Quaternion":
    """Quaternion product (Hamilton), returning a new quaternion."""
    if not isinstance(rhs, Quaternion):
        raise TypeError("@ requires a Quaternion")
    s1, x1, y1, z1 = self._data
    s2, x2, y2, z2 = rhs._data
    return Quaternion(
        s1 * s2 - x1 * x2 - y1 * y2 - z1 * z2,
        s1 * x2 + x1 * s2 + y1 * z2 - z1 * y2,
        s1 * y2 - x1 * z2 + y1 * s2 + z1 * x2,
        s1 * z2 + x1 * y2 - y1 * x2 + z1 * s2,
    )

__mul__(rhs)

Scalar scale or Vec3 rotation. Quaternion product uses @.

Source code in ncca/ngl/quaternion.py
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
def __mul__(self, rhs: "float | int | Vec3") -> "Quaternion | Vec3":
    """Scalar scale or Vec3 rotation. Quaternion product uses @."""
    if isinstance(rhs, Quaternion):
        raise TypeError("use q1 @ q2 for the quaternion product")
    if isinstance(rhs, (int, float)):
        result = Quaternion()
        result._data = self._data * np.float32(rhs)
        return result
    if isinstance(rhs, Vec3):
        # Quaternion-vector multiplication (rotate vector by quaternion)
        qw = self.s
        qx = self.x
        qy = self.y
        qz = self.z

        vx = rhs.x
        vy = rhs.y
        vz = rhs.z

        # pq (quaternion * pure quaternion from vector)
        pw = -qx * vx - qy * vy - qz * vz
        px = qw * vx + qy * vz - qz * vy
        py = qw * vy - qx * vz + qz * vx
        pz = qw * vz + qx * vy - qy * vx

        # pqp* (result * conjugate of quaternion)
        return Vec3(
            -pw * qx + px * qw - py * qz + pz * qy,
            -pw * qy + px * qz + py * qw - pz * qx,
            -pw * qz - px * qy + py * qx + pz * qw,
        )
    raise TypeError(f"cannot multiply Quaternion by {type(rhs)}")

__ne__(rhs)

Quaternion comparison a!=b using numpy.allclose.

Source code in ncca/ngl/quaternion.py
319
320
321
322
def __ne__(self, rhs: object) -> bool:
    """Quaternion comparison a!=b using numpy.allclose."""
    result = self.__eq__(rhs)
    return result if result is NotImplemented else not result

__neg__()

Return a new quaternion with every component negated.

Source code in ncca/ngl/quaternion.py
171
172
173
174
175
def __neg__(self) -> "Quaternion":
    """Return a new quaternion with every component negated."""
    result = Quaternion()
    result._data = -self._data
    return result

__repr__()

Eval-able representation, e.g. Quaternion(1.0, 0.0, 0.0, 0.0).

Source code in ncca/ngl/quaternion.py
341
342
343
344
def __repr__(self) -> str:
    """Eval-able representation, e.g. Quaternion(1.0, 0.0, 0.0, 0.0)."""
    args = ", ".join(repr(float(v)) for v in self._data)
    return f"Quaternion({args})"

__rmul__(rhs)

Scalar scale (right operand).

Source code in ncca/ngl/quaternion.py
165
166
167
168
169
def __rmul__(self, rhs: float) -> "Quaternion":
    """Scalar scale (right operand)."""
    if isinstance(rhs, (int, float)):
        return self * rhs
    raise TypeError(f"cannot multiply {type(rhs)} by Quaternion")

__str__()

Pretty representation, e.g. Quaternion(1.0, [0.0, 0.0, 0.0]).

Source code in ncca/ngl/quaternion.py
346
347
348
349
def __str__(self) -> str:
    """Pretty representation, e.g. Quaternion(1.0, [0.0, 0.0, 0.0])."""
    s, x, y, z = (float(v) for v in self._data)
    return f"Quaternion({s}, [{x}, {y}, {z}])"

__sub__(rhs)

Quaternion subtraction a-b, component-wise.

Source code in ncca/ngl/quaternion.py
113
114
115
116
117
def __sub__(self, rhs: "Quaternion") -> "Quaternion":
    """Quaternion subtraction a-b, component-wise."""
    result = Quaternion()
    result._data = self._data - rhs._data
    return result

__truediv__(rhs)

Scalar division, returning a new quaternion.

Raises:
  • ZeroDivisionError

    If rhs is zero.

  • TypeError

    If rhs is not a scalar.

Source code in ncca/ngl/quaternion.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def __truediv__(self, rhs: float | int) -> "Quaternion":
    """Scalar division, returning a new quaternion.

    Raises:
        ZeroDivisionError: If rhs is zero.
        TypeError: If rhs is not a scalar.
    """
    if isinstance(rhs, (int, float)):
        if rhs == 0:
            raise ZeroDivisionError("division by zero")
        result = Quaternion()
        result._data = self._data / np.float32(rhs)
        return result
    raise TypeError(f"cannot divide Quaternion by {type(rhs)}")

conjugate()

Return the conjugate of the quaternion (s, -x, -y, -z).

Source code in ncca/ngl/quaternion.py
223
224
225
226
227
228
def conjugate(self) -> "Quaternion":
    """Return the conjugate of the quaternion (s, -x, -y, -z)."""
    result = Quaternion()
    result._data = self._data.copy()
    result._data[1:] *= -1  # Negate x, y, z components
    return result

copy()

Return a new quaternion with the same values.

Source code in ncca/ngl/quaternion.py
282
283
284
def copy(self) -> "Quaternion":
    """Return a new quaternion with the same values."""
    return Quaternion(*self._data)

dot(rhs)

Dot product of two quaternions.

Source code in ncca/ngl/quaternion.py
239
240
241
def dot(self, rhs: "Quaternion") -> float:
    """Dot product of two quaternions."""
    return float(np.dot(self._data, rhs._data))

from_axis_angle(axis, angle) classmethod

Creates a new Quaternion from an axis and angle.

Parameters:
  • axis (Vec3) –

    The axis of rotation.

  • angle (float) –

    The angle of rotation in degrees.

Returns:
  • Quaternion( Quaternion ) –

    A new Quaternion representing the rotation.

Source code in ncca/ngl/quaternion.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@classmethod
def from_axis_angle(cls, axis: "Vec3", angle: float) -> "Quaternion":
    """Creates a new Quaternion from an axis and angle.

    Args:
        axis (Vec3): The axis of rotation.
        angle (float): The angle of rotation in degrees.

    Returns:
        Quaternion: A new Quaternion representing the rotation.
    """
    angle_rad = math.radians(angle)
    half_angle = angle_rad * 0.5
    s = math.cos(half_angle)
    sin_half_angle = math.sin(half_angle)
    x = axis.x * sin_half_angle
    y = axis.y * sin_half_angle
    z = axis.z * sin_half_angle
    return cls(s, x, y, z)

from_list(lst) classmethod

Create from [s, x, y, z].

Source code in ncca/ngl/quaternion.py
298
299
300
301
302
303
@classmethod
def from_list(cls, lst: list[float]) -> "Quaternion":
    """Create from [s, x, y, z]."""
    if len(lst) != 4:
        raise ValueError("Quaternion.from_list requires 4 values")
    return cls(*lst)

from_mat4(mat) classmethod

Creates a new Quaternion from a Mat4 rotation matrix.

Parameters:
  • mat (Mat4) –

    The rotation matrix to convert.

Returns:
  • Quaternion( Quaternion ) –

    A new Quaternion representing the rotation matrix.

Source code in ncca/ngl/quaternion.py
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
@classmethod
def from_mat4(cls, mat: "Mat4") -> "Quaternion":
    """Creates a new Quaternion from a Mat4 rotation matrix.

    Args:
        mat (Mat4): The rotation matrix to convert.

    Returns:
        Quaternion: A new Quaternion representing the rotation matrix.
    """
    matrix = mat.to_list()
    T = 1.0 + matrix[0] + matrix[5] + matrix[10]
    if T > 0.00000001:  # to avoid large distortions!
        scale = math.sqrt(T) * 2.0
        x = (matrix[6] - matrix[9]) / scale
        y = (matrix[8] - matrix[2]) / scale
        z = (matrix[1] - matrix[4]) / scale
        s = 0.25 * scale
    elif matrix[0] > matrix[5] and matrix[0] > matrix[10]:
        scale = math.sqrt(1.0 + matrix[0] - matrix[5] - matrix[10]) * 2.0
        x = 0.25 * scale
        y = (matrix[4] + matrix[1]) / scale
        z = (matrix[2] + matrix[8]) / scale
        s = (matrix[6] - matrix[9]) / scale
    elif matrix[5] > matrix[10]:
        scale = math.sqrt(1.0 + matrix[5] - matrix[0] - matrix[10]) * 2.0
        x = (matrix[4] + matrix[1]) / scale
        y = 0.25 * scale
        z = (matrix[9] + matrix[6]) / scale
        s = (matrix[8] - matrix[2]) / scale
    else:
        scale = math.sqrt(1.0 + matrix[10] - matrix[0] - matrix[5]) * 2.0
        x = (matrix[8] + matrix[2]) / scale
        y = (matrix[9] + matrix[6]) / scale
        z = 0.25 * scale
        s = (matrix[1] - matrix[4]) / scale

    return cls(s, x, y, z)

from_numpy(arr) classmethod

Create from an array [s, x, y, z].

Source code in ncca/ngl/quaternion.py
305
306
307
308
309
310
311
@classmethod
def from_numpy(cls, arr: np.ndarray) -> "Quaternion":
    """Create from an array [s, x, y, z]."""
    arr = np.asarray(arr, dtype=np.float32)
    if arr.shape != (4,):
        raise ValueError("Quaternion.from_numpy requires shape (4,)")
    return cls(*arr)

inverse()

Return the multiplicative inverse (conjugate / |q|^2).

Source code in ncca/ngl/quaternion.py
230
231
232
233
234
235
236
237
def inverse(self) -> "Quaternion":
    """Return the multiplicative inverse (conjugate / |q|^2)."""
    lsq = self.length_squared()
    if math.isclose(lsq, 0.0):
        raise ZeroDivisionError("Quaternion.inverse: zero quaternion")
    result = self.conjugate()
    result._data = result._data / np.float32(lsq)
    return result

length()

Return the length/magnitude of the quaternion.

Source code in ncca/ngl/quaternion.py
215
216
217
def length(self) -> float:
    """Return the length/magnitude of the quaternion."""
    return float(np.linalg.norm(self._data))

length_squared()

Return the squared magnitude.

Source code in ncca/ngl/quaternion.py
219
220
221
def length_squared(self) -> float:
    """Return the squared magnitude."""
    return float(np.dot(self._data, self._data))

normalized()

Return a new unit-length quaternion.

Raises:
  • ZeroDivisionError

    If the quaternion has zero length.

Source code in ncca/ngl/quaternion.py
202
203
204
205
206
207
208
209
210
211
212
213
def normalized(self) -> "Quaternion":
    """Return a new unit-length quaternion.

    Raises:
        ZeroDivisionError: If the quaternion has zero length.
    """
    length = self.length()
    if math.isclose(length, 0.0):
        raise ZeroDivisionError("Quaternion.normalized: length is zero")
    result = Quaternion()
    result._data = self._data / np.float32(length)
    return result

set(s, x, y, z)

Set all four components.

Source code in ncca/ngl/quaternion.py
278
279
280
def set(self, s: float, x: float, y: float, z: float) -> None:
    """Set all four components."""
    self._data[:] = (float(s), float(x), float(y), float(z))

slerp(rhs, t)

Spherical linear interpolation from self to rhs at t in [0, 1].

Source code in ncca/ngl/quaternion.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def slerp(self, rhs: "Quaternion", t: float) -> "Quaternion":
    """Spherical linear interpolation from self to rhs at t in [0, 1]."""
    dot = float(np.dot(self._data, rhs._data))
    rhs_data = rhs._data.copy()
    if dot < 0.0:
        dot = -dot
        rhs_data = -rhs_data
    if dot > 0.9995:
        data = self._data + np.float32(t) * (rhs_data - self._data)
        data = data / np.linalg.norm(data)
    else:
        theta0 = math.acos(max(-1.0, min(1.0, dot)))
        theta = theta0 * t
        s0 = math.cos(theta) - dot * math.sin(theta) / math.sin(theta0)
        s1 = math.sin(theta) / math.sin(theta0)
        data = np.float32(s0) * self._data + np.float32(s1) * rhs_data
    result = Quaternion()
    result._data = data.astype(np.float32)
    return result

to_list()

Return the quaternion as a list [s, x, y, z].

Source code in ncca/ngl/quaternion.py
290
291
292
def to_list(self) -> list[float]:
    """Return the quaternion as a list [s, x, y, z]."""
    return self._data.tolist()

to_mat4()

Return the equivalent rotation matrix (row-vector convention).

Source code in ncca/ngl/quaternion.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def to_mat4(self) -> Mat4:
    """Return the equivalent rotation matrix (row-vector convention)."""
    s, x, y, z = (float(v) for v in self._data)
    m = Mat4()
    m._data[0, 0] = 1.0 - 2.0 * (y * y + z * z)
    m._data[0, 1] = 2.0 * (x * y + s * z)
    m._data[0, 2] = 2.0 * (x * z - s * y)
    m._data[1, 0] = 2.0 * (x * y - s * z)
    m._data[1, 1] = 1.0 - 2.0 * (x * x + z * z)
    m._data[1, 2] = 2.0 * (y * z + s * x)
    m._data[2, 0] = 2.0 * (x * z + s * y)
    m._data[2, 1] = 2.0 * (y * z - s * x)
    m._data[2, 2] = 1.0 - 2.0 * (x * x + y * y)
    return m

to_numpy()

Return the quaternion as a numpy array [s, x, y, z].

Source code in ncca/ngl/quaternion.py
286
287
288
def to_numpy(self) -> np.ndarray:
    """Return the quaternion as a numpy array [s, x, y, z]."""
    return self._data.copy()

to_tuple()

Return (s, x, y, z) as plain floats.

Source code in ncca/ngl/quaternion.py
294
295
296
def to_tuple(self) -> tuple[float, float, float, float]:
    """Return (s, x, y, z) as plain floats."""
    return tuple(float(v) for v in self._data)

BBox

A bounding box class for 3D geometry.

Stores center, dimensions, extents, vertices, and normals for a box. Provides methods to recalculate from center/dimensions or from extents.

Source code in ncca/ngl/bbox.py
  6
  7
  8
  9
 10
 11
 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
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
class BBox:
    """A bounding box class for 3D geometry.

    Stores center, dimensions, extents, vertices, and normals for a box.
    Provides methods to recalculate from center/dimensions or from extents.
    """

    def __init__(
        self,
        center: Vec3 = Vec3(),
        width: float = 2.0,
        height: float = 2.0,
        depth: float = 2.0,
    ) -> None:
        """Initialize a bounding box from center and dimensions.

        Args:
            center: Center of the bounding box (Vec3)
            width: Width of the box
            height: Height of the box
            depth: Depth of the box
        """
        self._center: Vec3 = center
        self._width: float = width
        self._height: float = height
        self._depth: float = depth
        self._min_x: float = 0.0
        self._max_x: float = 0.0
        self._min_y: float = 0.0
        self._max_y: float = 0.0
        self._min_z: float = 0.0
        self._max_z: float = 0.0
        self._verts: list[Vec3] = [Vec3() for _ in range(8)]
        self._normals: list[Vec3] = [Vec3() for _ in range(6)]
        self.recalculate_from_center_dims()

    @classmethod
    def from_extents(
        cls,
        min_x: float,
        max_x: float,
        min_y: float,
        max_y: float,
        min_z: float,
        max_z: float,
    ) -> "BBox":
        """Create a bounding box from min/max extents.

        Args:
            min_x: Minimum x extent.
            max_x: Maximum x extent.
            min_y: Minimum y extent.
            max_y: Maximum y extent.
            min_z: Minimum z extent.
            max_z: Maximum z extent.

        Returns:
            BBox: The constructed bounding box
        """
        bbox = cls()
        bbox.set_extents(min_x, max_x, min_y, max_y, min_z, max_z)
        return bbox

    @property
    def center(self) -> Vec3:
        """Get or set the center of the bounding box."""
        return self._center

    @center.setter
    def center(self, value: Vec3) -> None:
        self._center = value
        self.recalculate_from_center_dims()

    @property
    def width(self) -> float:
        """Get or set the width of the bounding box."""
        return self._width

    @width.setter
    def width(self, value: float) -> None:
        self._width = value
        self.recalculate_from_center_dims()

    @property
    def height(self) -> float:
        """Get or set the height of the bounding box."""
        return self._height

    @height.setter
    def height(self, value: float) -> None:
        self._height = value
        self.recalculate_from_center_dims()

    @property
    def depth(self) -> float:
        """Get or set the depth of the bounding box."""
        return self._depth

    @depth.setter
    def depth(self, value: float) -> None:
        self._depth = value
        self.recalculate_from_center_dims()

    @property
    def min_x(self) -> float:
        """Get the minimum x extent."""
        return self._min_x

    @property
    def max_x(self) -> float:
        """Get the maximum x extent."""
        return self._max_x

    @property
    def min_y(self) -> float:
        """Get the minimum y extent."""
        return self._min_y

    @property
    def max_y(self) -> float:
        """Get the maximum y extent."""
        return self._max_y

    @property
    def min_z(self) -> float:
        """Get the minimum z extent."""
        return self._min_z

    @property
    def max_z(self) -> float:
        """Get the maximum z extent."""
        return self._max_z

    def get_vertex_array(self) -> list[Vec3]:
        """Get the list of 8 vertices for the bounding box.

        Returns:
            list[Vec3]: The 8 vertices of the box.
        """
        return self._verts

    def get_normal_array(self) -> list[Vec3]:
        """Get the list of 6 normals for the bounding box faces.

        Returns:
            list[Vec3]: The 6 normals of the box.
        """
        return self._normals

    def set_extents(
        self,
        min_x: float,
        max_x: float,
        min_y: float,
        max_y: float,
        min_z: float,
        max_z: float,
    ) -> None:
        """Set the extents of the bounding box and recalculate center/dimensions.

        Args:
            min_x: Minimum x extent.
            max_x: Maximum x extent.
            min_y: Minimum y extent.
            max_y: Maximum y extent.
            min_z: Minimum z extent.
            max_z: Maximum z extent.
        """
        self._min_x = min_x
        self._max_x = max_x
        self._min_y = min_y
        self._max_y = max_y
        self._min_z = min_z
        self._max_z = max_z
        self.recalculate_from_extents()

    def recalculate_from_center_dims(self) -> None:
        """Recalculate extents and update vertices/normals from center and dimensions."""
        half_width = self._width / 2.0
        half_height = self._height / 2.0
        half_depth = self._depth / 2.0

        self._min_x = self._center.x - half_width
        self._max_x = self._center.x + half_width
        self._min_y = self._center.y - half_height
        self._max_y = self._center.y + half_height
        self._min_z = self._center.z - half_depth
        self._max_z = self._center.z + half_depth
        self._update_verts_and_normals()

    def recalculate_from_extents(self) -> None:
        """Recalculate center and dimensions from extents, then update vertices/normals."""
        self._width = self._max_x - self._min_x
        self._height = self._max_y - self._min_y
        self._depth = self._max_z - self._min_z
        self._center = Vec3(
            self._min_x + self._width / 2.0,
            self._min_y + self._height / 2.0,
            self._min_z + self._depth / 2.0,
        )
        self._update_verts_and_normals()

    def _update_verts_and_normals(self) -> None:
        """Update the 8 vertices and 6 normals of the bounding box based on current extents."""
        self._verts[0].set(self._min_x, self._max_y, self._min_z)
        self._verts[1].set(self._max_x, self._max_y, self._min_z)
        self._verts[2].set(self._max_x, self._max_y, self._max_z)
        self._verts[3].set(self._min_x, self._max_y, self._max_z)
        self._verts[4].set(self._min_x, self._min_y, self._min_z)
        self._verts[5].set(self._max_x, self._min_y, self._min_z)
        self._verts[6].set(self._max_x, self._min_y, self._max_z)
        self._verts[7].set(self._min_x, self._min_y, self._max_z)

        self._normals[0].set(0.0, 1.0, 0.0)
        self._normals[1].set(0.0, -1.0, 0.0)
        self._normals[2].set(1.0, 0.0, 0.0)
        self._normals[3].set(-1.0, 0.0, 0.0)
        self._normals[4].set(0.0, 0.0, 1.0)
        self._normals[5].set(0.0, 0.0, -1.0)

center property writable

Get or set the center of the bounding box.

depth property writable

Get or set the depth of the bounding box.

height property writable

Get or set the height of the bounding box.

max_x property

Get the maximum x extent.

max_y property

Get the maximum y extent.

max_z property

Get the maximum z extent.

min_x property

Get the minimum x extent.

min_y property

Get the minimum y extent.

min_z property

Get the minimum z extent.

width property writable

Get or set the width of the bounding box.

__init__(center=Vec3(), width=2.0, height=2.0, depth=2.0)

Initialize a bounding box from center and dimensions.

Parameters:
  • center (Vec3, default: Vec3() ) –

    Center of the bounding box (Vec3)

  • width (float, default: 2.0 ) –

    Width of the box

  • height (float, default: 2.0 ) –

    Height of the box

  • depth (float, default: 2.0 ) –

    Depth of the box

Source code in ncca/ngl/bbox.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
def __init__(
    self,
    center: Vec3 = Vec3(),
    width: float = 2.0,
    height: float = 2.0,
    depth: float = 2.0,
) -> None:
    """Initialize a bounding box from center and dimensions.

    Args:
        center: Center of the bounding box (Vec3)
        width: Width of the box
        height: Height of the box
        depth: Depth of the box
    """
    self._center: Vec3 = center
    self._width: float = width
    self._height: float = height
    self._depth: float = depth
    self._min_x: float = 0.0
    self._max_x: float = 0.0
    self._min_y: float = 0.0
    self._max_y: float = 0.0
    self._min_z: float = 0.0
    self._max_z: float = 0.0
    self._verts: list[Vec3] = [Vec3() for _ in range(8)]
    self._normals: list[Vec3] = [Vec3() for _ in range(6)]
    self.recalculate_from_center_dims()

from_extents(min_x, max_x, min_y, max_y, min_z, max_z) classmethod

Create a bounding box from min/max extents.

Parameters:
  • min_x (float) –

    Minimum x extent.

  • max_x (float) –

    Maximum x extent.

  • min_y (float) –

    Minimum y extent.

  • max_y (float) –

    Maximum y extent.

  • min_z (float) –

    Minimum z extent.

  • max_z (float) –

    Maximum z extent.

Returns:
  • BBox( BBox ) –

    The constructed bounding box

Source code in ncca/ngl/bbox.py
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
@classmethod
def from_extents(
    cls,
    min_x: float,
    max_x: float,
    min_y: float,
    max_y: float,
    min_z: float,
    max_z: float,
) -> "BBox":
    """Create a bounding box from min/max extents.

    Args:
        min_x: Minimum x extent.
        max_x: Maximum x extent.
        min_y: Minimum y extent.
        max_y: Maximum y extent.
        min_z: Minimum z extent.
        max_z: Maximum z extent.

    Returns:
        BBox: The constructed bounding box
    """
    bbox = cls()
    bbox.set_extents(min_x, max_x, min_y, max_y, min_z, max_z)
    return bbox

get_normal_array()

Get the list of 6 normals for the bounding box faces.

Returns:
  • list[Vec3]

    list[Vec3]: The 6 normals of the box.

Source code in ncca/ngl/bbox.py
147
148
149
150
151
152
153
def get_normal_array(self) -> list[Vec3]:
    """Get the list of 6 normals for the bounding box faces.

    Returns:
        list[Vec3]: The 6 normals of the box.
    """
    return self._normals

get_vertex_array()

Get the list of 8 vertices for the bounding box.

Returns:
  • list[Vec3]

    list[Vec3]: The 8 vertices of the box.

Source code in ncca/ngl/bbox.py
139
140
141
142
143
144
145
def get_vertex_array(self) -> list[Vec3]:
    """Get the list of 8 vertices for the bounding box.

    Returns:
        list[Vec3]: The 8 vertices of the box.
    """
    return self._verts

recalculate_from_center_dims()

Recalculate extents and update vertices/normals from center and dimensions.

Source code in ncca/ngl/bbox.py
182
183
184
185
186
187
188
189
190
191
192
193
194
def recalculate_from_center_dims(self) -> None:
    """Recalculate extents and update vertices/normals from center and dimensions."""
    half_width = self._width / 2.0
    half_height = self._height / 2.0
    half_depth = self._depth / 2.0

    self._min_x = self._center.x - half_width
    self._max_x = self._center.x + half_width
    self._min_y = self._center.y - half_height
    self._max_y = self._center.y + half_height
    self._min_z = self._center.z - half_depth
    self._max_z = self._center.z + half_depth
    self._update_verts_and_normals()

recalculate_from_extents()

Recalculate center and dimensions from extents, then update vertices/normals.

Source code in ncca/ngl/bbox.py
196
197
198
199
200
201
202
203
204
205
206
def recalculate_from_extents(self) -> None:
    """Recalculate center and dimensions from extents, then update vertices/normals."""
    self._width = self._max_x - self._min_x
    self._height = self._max_y - self._min_y
    self._depth = self._max_z - self._min_z
    self._center = Vec3(
        self._min_x + self._width / 2.0,
        self._min_y + self._height / 2.0,
        self._min_z + self._depth / 2.0,
    )
    self._update_verts_and_normals()

set_extents(min_x, max_x, min_y, max_y, min_z, max_z)

Set the extents of the bounding box and recalculate center/dimensions.

Parameters:
  • min_x (float) –

    Minimum x extent.

  • max_x (float) –

    Maximum x extent.

  • min_y (float) –

    Minimum y extent.

  • max_y (float) –

    Maximum y extent.

  • min_z (float) –

    Minimum z extent.

  • max_z (float) –

    Maximum z extent.

Source code in ncca/ngl/bbox.py
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
def set_extents(
    self,
    min_x: float,
    max_x: float,
    min_y: float,
    max_y: float,
    min_z: float,
    max_z: float,
) -> None:
    """Set the extents of the bounding box and recalculate center/dimensions.

    Args:
        min_x: Minimum x extent.
        max_x: Maximum x extent.
        min_y: Minimum y extent.
        max_y: Maximum y extent.
        min_z: Minimum z extent.
        max_z: Maximum z extent.
    """
    self._min_x = min_x
    self._max_x = max_x
    self._min_y = min_y
    self._max_y = max_y
    self._min_z = min_z
    self._max_z = max_z
    self.recalculate_from_extents()

Plane

A mathematical plane.

Source code in ncca/ngl/plane.py
 6
 7
 8
 9
10
11
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
class Plane:
    """A mathematical plane."""

    def __init__(
        self,
        p1: Vec3 | None = None,
        p2: Vec3 | None = None,
        p3: Vec3 | None = None,
    ) -> None:
        """Construct a plane, optionally through three points.

        Args:
            p1: First point on the plane.
            p2: Second point on the plane.
            p3: Third point on the plane.
        """
        self._normal = Vec3(0.0, 1.0, 0.0)
        self._point = Vec3()
        self._d = 0.0
        if p1 and p2 and p3:
            self.set_points(p1, p2, p3)

    @property
    def normal(self) -> Vec3:
        """The plane's unit normal vector."""
        return self._normal

    @property
    def point(self) -> Vec3:
        """A point known to lie on the plane."""
        return self._point

    @property
    def d(self) -> float:
        """The plane's distance term in the equation normal.p + d = 0."""
        return self._d

    def set_points(self, p1: Vec3, p2: Vec3, p3: Vec3) -> None:
        """Define the plane from three points."""
        aux1 = p1 - p2
        aux2 = p3 - p2
        self._normal = aux2.cross(aux1)
        self._normal = self._normal.normalized()
        self._point = p2
        self._d = -(self._normal.inner(self._point))

    def set_normal_point(self, normal: Vec3, point: Vec3) -> None:
        """Define the plane from a normal and a point on the plane."""
        self._normal = normal
        self._normal = self._normal.normalized()
        self._point = point
        self._d = -(self._normal.inner(self._point))

    def set_floats(self, a: float, b: float, c: float, d: float) -> None:
        """Define the plane from the coefficients of ax + by + cz + d = 0."""
        self._normal.set(a, b, c)
        length = self._normal.length()
        self._normal = self._normal.normalized()
        self._d = d / length

    def distance(self, p: Vec3) -> float:
        """Return the signed distance from point p to the plane."""
        return self._d + self._normal.inner(p)

d property

The plane's distance term in the equation normal.p + d = 0.

normal property

The plane's unit normal vector.

point property

A point known to lie on the plane.

__init__(p1=None, p2=None, p3=None)

Construct a plane, optionally through three points.

Parameters:
  • p1 (Vec3 | None, default: None ) –

    First point on the plane.

  • p2 (Vec3 | None, default: None ) –

    Second point on the plane.

  • p3 (Vec3 | None, default: None ) –

    Third point on the plane.

Source code in ncca/ngl/plane.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def __init__(
    self,
    p1: Vec3 | None = None,
    p2: Vec3 | None = None,
    p3: Vec3 | None = None,
) -> None:
    """Construct a plane, optionally through three points.

    Args:
        p1: First point on the plane.
        p2: Second point on the plane.
        p3: Third point on the plane.
    """
    self._normal = Vec3(0.0, 1.0, 0.0)
    self._point = Vec3()
    self._d = 0.0
    if p1 and p2 and p3:
        self.set_points(p1, p2, p3)

distance(p)

Return the signed distance from point p to the plane.

Source code in ncca/ngl/plane.py
66
67
68
def distance(self, p: Vec3) -> float:
    """Return the signed distance from point p to the plane."""
    return self._d + self._normal.inner(p)

set_floats(a, b, c, d)

Define the plane from the coefficients of ax + by + cz + d = 0.

Source code in ncca/ngl/plane.py
59
60
61
62
63
64
def set_floats(self, a: float, b: float, c: float, d: float) -> None:
    """Define the plane from the coefficients of ax + by + cz + d = 0."""
    self._normal.set(a, b, c)
    length = self._normal.length()
    self._normal = self._normal.normalized()
    self._d = d / length

set_normal_point(normal, point)

Define the plane from a normal and a point on the plane.

Source code in ncca/ngl/plane.py
52
53
54
55
56
57
def set_normal_point(self, normal: Vec3, point: Vec3) -> None:
    """Define the plane from a normal and a point on the plane."""
    self._normal = normal
    self._normal = self._normal.normalized()
    self._point = point
    self._d = -(self._normal.inner(self._point))

set_points(p1, p2, p3)

Define the plane from three points.

Source code in ncca/ngl/plane.py
43
44
45
46
47
48
49
50
def set_points(self, p1: Vec3, p2: Vec3, p3: Vec3) -> None:
    """Define the plane from three points."""
    aux1 = p1 - p2
    aux2 = p3 - p2
    self._normal = aux2.cross(aux1)
    self._normal = self._normal.normalized()
    self._point = p2
    self._d = -(self._normal.inner(self._point))

Transform

A position/rotation/scale transform that can generate a Mat4.

Attributes:
  • position (Vec3) –

    Translation component.

  • rotation (Vec3) –

    Rotation component in degrees.

  • scale (Vec3) –

    Scale component.

  • order (str) –

    Rotation order, one of the keys in rot_order.

Source code in ncca/ngl/transform.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
class Transform:
    """A position/rotation/scale transform that can generate a Mat4.

    Attributes:
        position (Vec3): Translation component.
        rotation (Vec3): Rotation component in degrees.
        scale (Vec3): Scale component.
        order (str): Rotation order, one of the keys in `rot_order`.
    """

    rot_order = {
        "xyz": "rz@ry@rx",
        "yzx": "rx@rz@ry",
        "zxy": "ry@rx@rz",
        "xzy": "ry@rz@rx",
        "yxz": "rz@rx@ry",
        "zyx": "rx@ry@rz",
    }

    def __init__(self) -> None:
        """Initialize the transform to identity position/rotation/scale."""
        self.position = Vec3(0.0, 0.0, 0.0)
        self.rotation = Vec3(0.0, 0.0, 0.0)
        self.scale = Vec3(1.0, 1.0, 1.0)
        self._matrix = Mat4()
        self.need_recalc = True
        self.order = "xyz"

    def _set_value(self, args: tuple[Any, ...]) -> Vec3:
        """Build a Vec3 from either (x, y, z), a list/tuple, or a vec-like object.

        Args:
            args: Either a single list/tuple/vec-like object, or three floats.

        Returns:
            The resulting Vec3.

        Raises:
            ValueError: If neither 1 nor 3 arguments are given.
        """
        v = Vec3()
        self.need_recalc = True
        if len(args) == 1:  # one argument
            if isinstance(args[0], (list, tuple)):
                v.x = args[0][0]
                v.y = args[0][1]
                v.z = args[0][2]
            else:  # try vec types
                v.x = args[0].x
                v.y = args[0].y
                v.z = args[0].z
            return v
        elif len(args) == 3:  # 3 as x,y,z
            v.x = float(args[0])
            v.y = float(args[1])
            v.z = float(args[2])
            return v
        else:
            raise ValueError

    def reset(self) -> None:
        """Reset position, rotation, scale and order to their defaults."""
        self.position = Vec3()
        self.rotation = Vec3()
        self.scale = Vec3(1, 1, 1)
        self.order = "xyz"
        self.need_recalc = True

    def set_position(self, *args: Any) -> None:
        """Set position attrib using either x,y,z or vec types."""
        self.position = self._set_value(args)

    def set_rotation(self, *args: Any) -> None:
        """Set rotation attrib using either x,y,z or vec types."""
        self.rotation = self._set_value(args)

    def set_scale(self, *args: Any) -> None:
        """Set scale attrib using either x,y,z or vec types."""
        self.scale = self._set_value(args)

    def set_order(self, order: str) -> None:
        """Set rotation order from string e.g xyz or zyx."""
        if order not in self.rot_order:
            raise TransformRotationOrder
        self.order = order
        self.need_recalc = True

    def matrix(self) -> Mat4:
        """Return a transform matrix based on rotation order."""
        if self.need_recalc is True:
            scale = Mat4.scale(self.scale.x, self.scale.y, self.scale.z)
            rx = Mat4.rotate_x(self.rotation.x)  # noqa: F841
            ry = Mat4.rotate_y(self.rotation.y)  # noqa: F841
            rz = Mat4.rotate_z(self.rotation.z)  # noqa: F841
            rotation_scale = eval(self.rot_order.get(self.order)) @ scale
            self._matrix = rotation_scale
            self._matrix[3][0] = self.position.x
            self._matrix[3][1] = self.position.y
            self._matrix[3][2] = self.position.z
            self._matrix[3][3] = 1.0
            self.need_recalc = False
        return self._matrix

    def __str__(self) -> str:
        """Pretty representation showing position, rotation and scale."""
        return f"pos {self.position}\nrot {self.rotation}\nscale {self.scale}"

__init__()

Initialize the transform to identity position/rotation/scale.

Source code in ncca/ngl/transform.py
32
33
34
35
36
37
38
39
def __init__(self) -> None:
    """Initialize the transform to identity position/rotation/scale."""
    self.position = Vec3(0.0, 0.0, 0.0)
    self.rotation = Vec3(0.0, 0.0, 0.0)
    self.scale = Vec3(1.0, 1.0, 1.0)
    self._matrix = Mat4()
    self.need_recalc = True
    self.order = "xyz"

__str__()

Pretty representation showing position, rotation and scale.

Source code in ncca/ngl/transform.py
116
117
118
def __str__(self) -> str:
    """Pretty representation showing position, rotation and scale."""
    return f"pos {self.position}\nrot {self.rotation}\nscale {self.scale}"

matrix()

Return a transform matrix based on rotation order.

Source code in ncca/ngl/transform.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def matrix(self) -> Mat4:
    """Return a transform matrix based on rotation order."""
    if self.need_recalc is True:
        scale = Mat4.scale(self.scale.x, self.scale.y, self.scale.z)
        rx = Mat4.rotate_x(self.rotation.x)  # noqa: F841
        ry = Mat4.rotate_y(self.rotation.y)  # noqa: F841
        rz = Mat4.rotate_z(self.rotation.z)  # noqa: F841
        rotation_scale = eval(self.rot_order.get(self.order)) @ scale
        self._matrix = rotation_scale
        self._matrix[3][0] = self.position.x
        self._matrix[3][1] = self.position.y
        self._matrix[3][2] = self.position.z
        self._matrix[3][3] = 1.0
        self.need_recalc = False
    return self._matrix

reset()

Reset position, rotation, scale and order to their defaults.

Source code in ncca/ngl/transform.py
73
74
75
76
77
78
79
def reset(self) -> None:
    """Reset position, rotation, scale and order to their defaults."""
    self.position = Vec3()
    self.rotation = Vec3()
    self.scale = Vec3(1, 1, 1)
    self.order = "xyz"
    self.need_recalc = True

set_order(order)

Set rotation order from string e.g xyz or zyx.

Source code in ncca/ngl/transform.py
93
94
95
96
97
98
def set_order(self, order: str) -> None:
    """Set rotation order from string e.g xyz or zyx."""
    if order not in self.rot_order:
        raise TransformRotationOrder
    self.order = order
    self.need_recalc = True

set_position(*args)

Set position attrib using either x,y,z or vec types.

Source code in ncca/ngl/transform.py
81
82
83
def set_position(self, *args: Any) -> None:
    """Set position attrib using either x,y,z or vec types."""
    self.position = self._set_value(args)

set_rotation(*args)

Set rotation attrib using either x,y,z or vec types.

Source code in ncca/ngl/transform.py
85
86
87
def set_rotation(self, *args: Any) -> None:
    """Set rotation attrib using either x,y,z or vec types."""
    self.rotation = self._set_value(args)

set_scale(*args)

Set scale attrib using either x,y,z or vec types.

Source code in ncca/ngl/transform.py
89
90
91
def set_scale(self, *args: Any) -> None:
    """Set scale attrib using either x,y,z or vec types."""
    self.scale = self._set_value(args)

BezierCurve

A Bezier curve class.

Source code in ncca/ngl/bezier_curve.py
 6
 7
 8
 9
10
11
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
class BezierCurve:
    """A Bezier curve class."""

    def __init__(
        self,
        control_points: list[Vec3] | None = None,
        knots: list[float] | None = None,
    ) -> None:
        """Create a curve from optional control points and knots.

        Args:
            control_points: Initial control points; empty list if omitted.
            knots: Knot vector; generated automatically if omitted.
        """
        self._cp = control_points if control_points is not None else []
        self._knots = knots if knots is not None else []
        self._degree = 0
        self._order = 0
        self._num_cp = 0
        self._num_knots = 0
        if self._cp:
            self._num_cp = len(self._cp)
            self._degree = self._num_cp
            self._order = self._degree + 1
            if not self._knots:
                self.create_knots()
            self._num_knots = len(self._knots)

    @property
    def control_points(self) -> list[Vec3]:
        """The curve's control points."""
        return self._cp

    @property
    def knots(self) -> list[float]:
        """The curve's knot vector."""
        return self._knots

    def add_point(
        self, x: float | Vec3, y: float | None = None, z: float | None = None
    ) -> None:
        """Add a control point, either as a Vec3 or as x, y, z floats."""
        if isinstance(x, Vec3):
            self._cp.append(x)
        else:
            self._cp.append(Vec3(x, y, z))
        self._num_cp += 1
        self._degree = self._num_cp
        self._order = self._degree + 1
        self.create_knots()

    def add_knot(self, k: float) -> None:
        """Append a knot value to the knot vector."""
        self._knots.append(k)
        self._num_knots = len(self._knots)

    def create_knots(self) -> None:
        """Generate a clamped knot vector for the current control points."""
        self._num_knots = self._num_cp + self._order
        self._knots = [0.0] * (self._num_knots // 2) + [1.0] * (
            self._num_knots - (self._num_knots // 2)
        )

    def get_point_on_curve(self, u: float) -> Vec3:
        """Evaluate the curve at parameter u and return the point."""
        p = Vec3()
        for i in range(self._num_cp):
            val = self.cox_de_boor(u, i, self._degree, self._knots)
            if val > 0.001:
                p += self._cp[i] * val
        return p

    def cox_de_boor(self, u: float, i: int, k: int, knots: list[float]) -> float:
        """Recursively evaluate the Cox-de Boor basis function."""
        if k == 1:
            return 1.0 if knots[i] <= u <= knots[i + 1] else 0.0

        den1 = knots[i + k - 1] - knots[i]
        den2 = knots[i + k] - knots[i + 1]

        eq1 = 0.0
        if den1 > 0:
            eq1 = ((u - knots[i]) / den1) * self.cox_de_boor(u, i, k - 1, knots)

        eq2 = 0.0
        if den2 > 0:
            eq2 = ((knots[i + k] - u) / den2) * self.cox_de_boor(u, i + 1, k - 1, knots)

        return eq1 + eq2

control_points property

The curve's control points.

knots property

The curve's knot vector.

__init__(control_points=None, knots=None)

Create a curve from optional control points and knots.

Parameters:
  • control_points (list[Vec3] | None, default: None ) –

    Initial control points; empty list if omitted.

  • knots (list[float] | None, default: None ) –

    Knot vector; generated automatically if omitted.

Source code in ncca/ngl/bezier_curve.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def __init__(
    self,
    control_points: list[Vec3] | None = None,
    knots: list[float] | None = None,
) -> None:
    """Create a curve from optional control points and knots.

    Args:
        control_points: Initial control points; empty list if omitted.
        knots: Knot vector; generated automatically if omitted.
    """
    self._cp = control_points if control_points is not None else []
    self._knots = knots if knots is not None else []
    self._degree = 0
    self._order = 0
    self._num_cp = 0
    self._num_knots = 0
    if self._cp:
        self._num_cp = len(self._cp)
        self._degree = self._num_cp
        self._order = self._degree + 1
        if not self._knots:
            self.create_knots()
        self._num_knots = len(self._knots)

add_knot(k)

Append a knot value to the knot vector.

Source code in ncca/ngl/bezier_curve.py
57
58
59
60
def add_knot(self, k: float) -> None:
    """Append a knot value to the knot vector."""
    self._knots.append(k)
    self._num_knots = len(self._knots)

add_point(x, y=None, z=None)

Add a control point, either as a Vec3 or as x, y, z floats.

Source code in ncca/ngl/bezier_curve.py
44
45
46
47
48
49
50
51
52
53
54
55
def add_point(
    self, x: float | Vec3, y: float | None = None, z: float | None = None
) -> None:
    """Add a control point, either as a Vec3 or as x, y, z floats."""
    if isinstance(x, Vec3):
        self._cp.append(x)
    else:
        self._cp.append(Vec3(x, y, z))
    self._num_cp += 1
    self._degree = self._num_cp
    self._order = self._degree + 1
    self.create_knots()

cox_de_boor(u, i, k, knots)

Recursively evaluate the Cox-de Boor basis function.

Source code in ncca/ngl/bezier_curve.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def cox_de_boor(self, u: float, i: int, k: int, knots: list[float]) -> float:
    """Recursively evaluate the Cox-de Boor basis function."""
    if k == 1:
        return 1.0 if knots[i] <= u <= knots[i + 1] else 0.0

    den1 = knots[i + k - 1] - knots[i]
    den2 = knots[i + k] - knots[i + 1]

    eq1 = 0.0
    if den1 > 0:
        eq1 = ((u - knots[i]) / den1) * self.cox_de_boor(u, i, k - 1, knots)

    eq2 = 0.0
    if den2 > 0:
        eq2 = ((knots[i + k] - u) / den2) * self.cox_de_boor(u, i + 1, k - 1, knots)

    return eq1 + eq2

create_knots()

Generate a clamped knot vector for the current control points.

Source code in ncca/ngl/bezier_curve.py
62
63
64
65
66
67
def create_knots(self) -> None:
    """Generate a clamped knot vector for the current control points."""
    self._num_knots = self._num_cp + self._order
    self._knots = [0.0] * (self._num_knots // 2) + [1.0] * (
        self._num_knots - (self._num_knots // 2)
    )

get_point_on_curve(u)

Evaluate the curve at parameter u and return the point.

Source code in ncca/ngl/bezier_curve.py
69
70
71
72
73
74
75
76
def get_point_on_curve(self, u: float) -> Vec3:
    """Evaluate the curve at parameter u and return the point."""
    p = Vec3()
    for i in range(self._num_cp):
        val = self.cox_de_boor(u, i, self._degree, self._knots)
        if val > 0.001:
            p += self._cp[i] * val
    return p

Utility Functions

Free functions from ncca.ngl.util, re-exported from ncca.ngl. The camera and projection ones are explained in the Cameras and Projection tutorial; clamp, lerp, and calc_normal have their own tutorial.

clamp

Clamp num to the range [low, high].

Raises:
  • ValueError

    If low >= high.

Source code in ncca/ngl/util.py
18
19
20
21
22
23
24
25
26
def clamp(num: float, low: float, high: float) -> float:
    """Clamp num to the range [low, high].

    Raises:
        ValueError: If low >= high.
    """
    if low > high or low == high:
        raise ValueError
    return max(min(num, high), low)

lerp

Linearly interpolate between a and b at parameter t.

Works for floats and any type supporting + and scalar * (Vec2/3/4, Quaternion, matrices).

Parameters:
  • a (T) –

    Start value.

  • b (T) –

    End value.

  • t (float) –

    Interpolation parameter, typically in [0, 1].

Returns:
  • T

    The interpolated value, of the same type as a and b.

Source code in ncca/ngl/util.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def lerp(a: T, b: T, t: float) -> T:
    """Linearly interpolate between a and b at parameter t.

    Works for floats and any type supporting + and scalar * (Vec2/3/4,
    Quaternion, matrices).

    Args:
        a: Start value.
        b: End value.
        t: Interpolation parameter, typically in [0, 1].

    Returns:
        The interpolated value, of the same type as a and b.
    """
    return a + (b - a) * t

calc_normal

Calculates the normal of a triangle defined by three points.

This is a Python implementation of the NGL C++ Util::calcNormal function. It uses the vector cross product method for clarity and leverages the py-ngl library. The order of the cross product is chosen to match the output of the C++ version.

Parameters:
  • p1 (Vec3) –

    The first vertex of the triangle.

  • p2 (Vec3) –

    The second vertex of the triangle.

  • p3 (Vec3) –

    The third vertex of the triangle.

Returns:
  • Vec3

    The normalized normal vector of the triangle.

Source code in ncca/ngl/util.py
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
def calc_normal(p1: "Vec3", p2: "Vec3", p3: "Vec3") -> "Vec3":
    """Calculates the normal of a triangle defined by three points.

    This is a Python implementation of the NGL C++ Util::calcNormal function.
    It uses the vector cross product method for clarity and leverages the py-ngl library.
    The order of the cross product is chosen to match the output of the C++ version.

    Args:
        p1: The first vertex of the triangle.
        p2: The second vertex of the triangle.
        p3: The third vertex of the triangle.

    Returns:
        The normalized normal vector of the triangle.
    """
    # Two vectors on the plane of the triangle
    v1 = p3 - p1
    v2 = p2 - p1

    # The cross product gives the normal vector.
    # The order (v1 x v2) is used to match the C++ implementation's result.
    normal = v1.cross(v2)

    # Normalize the result to get a unit length normal
    normal = normal.normalized()

    return normal

look_at

Calculate 4x4 matrix for camera lookAt.

Source code in ncca/ngl/util.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def look_at(eye: "Vec3", look: "Vec3", up: "Vec3") -> Mat4:
    """Calculate 4x4 matrix for camera lookAt."""
    n = look - eye
    u = up
    v = n.cross(u)
    u = v.cross(n)
    n = n.normalized()
    v = v.normalized()
    u = u.normalized()

    result = Mat4.identity()
    result[0][0] = v.x
    result[1][0] = v.y
    result[2][0] = v.z
    result[0][1] = u.x
    result[1][1] = u.y
    result[2][1] = u.z
    result[0][2] = -n.x
    result[1][2] = -n.y
    result[2][2] = -n.z
    result[3][0] = -eye.dot(v)
    result[3][1] = -eye.dot(u)
    result[3][2] = eye.dot(n)
    return result

perspective

Calculate a perspective matrix for various 3D graphics APIs.

Default mode is OpenGL, but will convert for Vulkan and WebGPU if required.

Args

fov : float - Field of view angle in degrees. aspect : float - Aspect ratio of the viewport. near : float - Near clipping plane distance. far : float - Far clipping plane distance.

Returns:
  • Mat4

    Mat4 - The perspective matrix.

Source code in ncca/ngl/util.py
 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
def perspective(
    fov: float,
    aspect: float,
    near: float,
    far: float,
    mode: PerspMode = PerspMode.OpenGL,
) -> Mat4:
    """Calculate a perspective matrix for various 3D graphics APIs.

    Default mode is OpenGL, but will convert for Vulkan and WebGPU if
    required.

    Args :
        fov : float - Field of view angle in degrees.
        aspect : float - Aspect ratio of the viewport.
        near : float - Near clipping plane distance.
        far : float - Far clipping plane distance.

    Returns:
        Mat4 - The perspective matrix.
    """
    m = Mat4.zero()  # as per glm
    _range = math.tan(math.radians(fov / 2.0)) * near
    left = -_range * aspect
    right = _range * aspect
    bottom = -_range
    top = _range
    m[0][0] = (2.0 * near) / (right - left)
    m[1][1] = (2.0 * near) / (top - bottom)
    match mode:
        case PerspMode.OpenGL:
            m[2][2] = -(far + near) / (far - near)
            m[2][3] = -1.0
            m[3][2] = -(2.0 * far * near) / (far - near)

        # This ensures the clip space Z range is [0, 1] as required by Vulkan and WebGPU.
        case PerspMode.WebGPU | PerspMode.Vulkan:
            m[2][2] = -far / (far - near)
            m[2][3] = -1.0
            m[3][2] = -(far * near) / (far - near)
    return m

PerspMode

Bases: Enum

Target graphics API clip-space convention for projection matrices.

Source code in ncca/ngl/util.py
55
56
57
58
59
60
class PerspMode(enum.Enum):
    """Target graphics API clip-space convention for projection matrices."""

    OpenGL = "OpenGL"
    WebGPU = "WebGPU"
    Vulkan = "Vulkan"

ortho

Calculate an orthographic projection matrix.

Parameters:
  • left (float) –

    Left clipping plane.

  • right (float) –

    Right clipping plane.

  • bottom (float) –

    Bottom clipping plane.

  • top (float) –

    Top clipping plane.

  • near (float) –

    Near clipping plane distance.

  • far (float) –

    Far clipping plane distance.

  • mode (PerspMode, default: OpenGL ) –

    Target graphics API clip-space convention.

Returns:
  • Mat4( Mat4 ) –

    The orthographic projection matrix.

Source code in ncca/ngl/util.py
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
def ortho(
    left: float,
    right: float,
    bottom: float,
    top: float,
    near: float,
    far: float,
    mode: PerspMode = PerspMode.OpenGL,
) -> Mat4:
    """Calculate an orthographic projection matrix.

    Args:
        left: Left clipping plane.
        right: Right clipping plane.
        bottom: Bottom clipping plane.
        top: Top clipping plane.
        near: Near clipping plane distance.
        far: Far clipping plane distance.
        mode: Target graphics API clip-space convention.

    Returns:
        Mat4: The orthographic projection matrix.
    """
    m = Mat4.identity()
    m[0][0] = 2.0 / (right - left)
    m[1][1] = 2.0 / (top - bottom)
    match mode:
        case PerspMode.OpenGL:
            m[2][2] = -2.0 / (far - near)
            m[3][2] = -(far + near) / (far - near)
        case PerspMode.WebGPU | PerspMode.Vulkan:
            m[2][2] = -1.0 / (far - near)
            m[3][2] = -near / (far - near)
    m[3][0] = -(right + left) / (right - left)
    m[3][1] = -(top + bottom) / (top - bottom)
    return m

frustum

Create a frustum projection matrix.

Parameters:
  • left (float) –

    Left clipping plane.

  • right (float) –

    Right clipping plane.

  • bottom (float) –

    Bottom clipping plane.

  • top (float) –

    Top clipping plane.

  • near (float) –

    Near clipping plane distance.

  • far (float) –

    Far clipping plane distance.

Returns:
  • Mat4( Mat4 ) –

    The frustum projection matrix.

Source code in ncca/ngl/util.py
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
def frustum(
    left: float, right: float, bottom: float, top: float, near: float, far: float
) -> Mat4:
    """Create a frustum projection matrix.

    Args:
        left: Left clipping plane.
        right: Right clipping plane.
        bottom: Bottom clipping plane.
        top: Top clipping plane.
        near: Near clipping plane distance.
        far: Far clipping plane distance.

    Returns:
        Mat4: The frustum projection matrix.
    """
    m = Mat4.zero()
    m[0][0] = (2.0 * near) / (right - left)
    m[1][1] = (2.0 * near) / (top - bottom)
    m[2][0] = (right + left) / (right - left)
    m[2][1] = (top + bottom) / (top - bottom)
    m[2][2] = -(far + near) / (far - near)
    m[2][3] = -1.0
    m[3][2] = -(2.0 * far * near) / (far - near)
    return m

renderman_look_at

Calculate 4x4 matrix for RenderMan camera lookAt.

Accounts for RenderMan's right-handed Y-down, Z-forward coordinate system.

Parameters:
  • eye (Vec3) –

    Vec3 - camera position

  • look (Vec3) –

    Vec3 - point to look at

  • up (Vec3) –

    Vec3 - up vector (typically (0, 1, 0) in world space)

Returns:
  • Mat4

    Mat4 - 4x4 transformation matrix

Source code in ncca/ngl/util.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def renderman_look_at(eye: "Vec3", look: "Vec3", up: "Vec3") -> Mat4:
    """Calculate 4x4 matrix for RenderMan camera lookAt.

    Accounts for RenderMan's right-handed Y-down, Z-forward coordinate system.

    Args:
        eye: Vec3 - camera position
        look: Vec3 - point to look at
        up: Vec3 - up vector (typically (0, 1, 0) in world space)

    Returns:
        Mat4 - 4x4 transformation matrix
    """
    # Calculate view direction (from eye to look point)
    n = look - eye
    n = n.normalized()

    # Calculate right vector
    up.y = -up.y
    v = n.cross(up)
    v = v.normalized()

    # Recalculate orthogonal up vector
    u = v.cross(n)
    u = u.normalized()

    # Build the matrix for RenderMan's coordinate system
    # RenderMan uses Y-down, Z-forward
    result = Mat4.identity()

    # Right vector (X-axis)
    result[0][0] = v.x
    result[1][0] = v.y
    result[2][0] = v.z

    # Up vector (Y-axis) - negated for Y-down convention
    result[0][1] = -u.x
    result[1][1] = -u.y
    result[2][1] = -u.z

    # Forward vector (Z-axis) - camera looks down +Z
    result[0][2] = n.x
    result[1][2] = n.y
    result[2][2] = n.z

    # Translation (camera position)
    result[3][0] = -eye.dot(v)
    result[3][1] = -eye.dot(u)  # Negated Y component
    result[3][2] = -eye.dot(n)

    return result

Exceptions

MatrixError

Bases: Exception

Raised for invalid matrix construction or operations.

Source code in ncca/ngl/mat_base.py
14
15
class MatrixError(Exception):
    """Raised for invalid matrix construction or operations."""

TransformRotationOrder

Bases: Exception

Raised when an unrecognised rotation order string is set.

Source code in ncca/ngl/transform.py
 9
10
class TransformRotationOrder(Exception):
    """Raised when an unrecognised rotation order string is set."""