ncca-ngl 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import numpy as np
4
+ import pathlib
5
+
6
+ files = pathlib.Path(".").glob("*.npy")
7
+
8
+ data = {}
9
+ for f in files:
10
+ data[str(f.stem)] = np.load(f)
11
+ # arrays.append(np.load(f))
12
+ # names.append(str(f.stem))
13
+ print(data.keys())
14
+
15
+
16
+ np.savez_compressed("Primitives", **data)
17
+
18
+ loaded = np.load("Primitives.npz")
19
+ print(loaded)
20
+ print(loaded["dragon"])
ncca/ngl/__init__.py ADDED
@@ -0,0 +1,100 @@
1
+ from .abstract_vao import AbstractVAO, VertexData
2
+ from .base_mesh import BaseMesh, Face
3
+ from .bbox import BBox
4
+ from .bezier_curve import BezierCurve
5
+ from .first_person_camera import FirstPersonCamera
6
+ from .image import Image, ImageModes
7
+ from .log import logger
8
+ from .mat2 import Mat2
9
+ from .mat3 import Mat3, Mat3Error, Mat3NotSquare
10
+ from .mat4 import Mat4, Mat4Error, Mat4NotSquare
11
+ from .multi_buffer_vao import MultiBufferVAO
12
+ from .obj import (
13
+ Obj,
14
+ ObjParseFaceError,
15
+ ObjParseNormalError,
16
+ ObjParseUVError,
17
+ ObjParseVertexError,
18
+ )
19
+ from .plane import Plane
20
+ from .primitives import Primitives, Prims
21
+ from .pyside_event_handling_mixin import PySideEventHandlingMixin
22
+ from .quaternion import Quaternion
23
+ from .random import Random
24
+ from .shader import MatrixTranspose, Shader, ShaderType
25
+ from .shader_lib import DefaultShader, ShaderLib
26
+ from .shader_program import ShaderProgram
27
+ from .simple_index_vao import IndexVertexData, SimpleIndexVAO
28
+ from .simple_vao import SimpleVAO
29
+ from .text import Text
30
+ from .texture import Texture
31
+ from .transform import Transform, TransformRotationOrder
32
+ from .util import calc_normal, clamp, frustum, lerp, look_at, ortho, perspective
33
+ from .vao_factory import VAOFactory, VAOType
34
+ from .vec2 import Vec2
35
+ from .vec2_array import Vec2Array
36
+ from .vec3 import Vec3
37
+ from .vec3_array import Vec3Array
38
+ from .vec4 import Vec4
39
+ from .vec4_array import Vec4Array
40
+
41
+ all = [
42
+ AbstractVAO,
43
+ VertexData,
44
+ BaseMesh,
45
+ Face,
46
+ BBox,
47
+ BezierCurve,
48
+ Image,
49
+ ImageModes,
50
+ Mat2,
51
+ Mat3,
52
+ Mat4,
53
+ MultiBufferVAO,
54
+ Obj,
55
+ Plane,
56
+ Quaternion,
57
+ MatrixTranspose,
58
+ Shader,
59
+ ShaderProgram,
60
+ ShaderType,
61
+ ShaderLib,
62
+ IndexVertexData,
63
+ SimpleIndexVAO,
64
+ SimpleVAO,
65
+ Texture,
66
+ VAOFactory,
67
+ Vec2,
68
+ Vec3,
69
+ Vec4,
70
+ Vec3Array,
71
+ Vec2Array,
72
+ Vec4Array,
73
+ ObjParseVertexError,
74
+ ObjParseNormalError,
75
+ ObjParseUVError,
76
+ ObjParseFaceError,
77
+ clamp,
78
+ lerp,
79
+ look_at,
80
+ perspective,
81
+ ortho,
82
+ frustum,
83
+ Transform,
84
+ TransformRotationOrder,
85
+ Random,
86
+ Text,
87
+ calc_normal,
88
+ Mat3Error,
89
+ Mat4Error,
90
+ Mat3NotSquare,
91
+ Mat4NotSquare,
92
+ Mat4NotSquare,
93
+ VAOType,
94
+ DefaultShader,
95
+ logger,
96
+ Primitives,
97
+ Prims,
98
+ FirstPersonCamera,
99
+ PySideEventHandlingMixin,
100
+ ]
@@ -0,0 +1,89 @@
1
+ import abc
2
+ import ctypes
3
+
4
+ import numpy as np
5
+ import OpenGL.GL as gl
6
+
7
+ from .log import logger
8
+
9
+
10
+ class VertexData:
11
+ def __init__(self, data, size, mode=gl.GL_STATIC_DRAW):
12
+ if isinstance(data, np.ndarray):
13
+ self.data = data
14
+ else:
15
+ self.data = np.array(data, dtype=np.float32)
16
+ self.size = size
17
+ self.mode = mode
18
+
19
+
20
+ class AbstractVAO(abc.ABC):
21
+ def __init__(self, mode=gl.GL_TRIANGLES):
22
+ self.id = gl.glGenVertexArrays(1)
23
+ self.mode = mode
24
+ self.bound = False
25
+ self.allocated = False
26
+ self.indices_count = 0
27
+
28
+ def bind(self):
29
+ gl.glBindVertexArray(self.id)
30
+ self.bound = True
31
+
32
+ def unbind(self):
33
+ gl.glBindVertexArray(0)
34
+ self.bound = False
35
+
36
+ def __enter__(self):
37
+ self.bind()
38
+ return self
39
+
40
+ def __exit__(self, exc_type, exc_val, exc_tb):
41
+ self.unbind()
42
+
43
+ @abc.abstractmethod
44
+ def draw(self):
45
+ raise NotImplementedError
46
+
47
+ @abc.abstractmethod
48
+ def set_data(self, data):
49
+ raise NotImplementedError
50
+
51
+ @abc.abstractmethod
52
+ def remove_vao(self):
53
+ raise NotImplementedError
54
+
55
+ def set_vertex_attribute_pointer(
56
+ self, id, size, type, stride, offset, normalize=False
57
+ ):
58
+ if not self.bound:
59
+ logger.error("VAO not bound in set_vertex_attribute_pointer")
60
+ gl.glVertexAttribPointer(
61
+ id, size, type, normalize, stride, ctypes.c_void_p(offset)
62
+ )
63
+ gl.glEnableVertexAttribArray(id)
64
+
65
+ def set_num_indices(self, count):
66
+ self.indices_count = count
67
+
68
+ def num_indices(self):
69
+ return self.indices_count
70
+
71
+ def get_mode(self):
72
+ return self.mode
73
+
74
+ def set_mode(self, mode):
75
+ self.mode = mode
76
+
77
+ @abc.abstractmethod
78
+ def get_buffer_id(self, index=0):
79
+ raise NotImplementedError
80
+
81
+ @abc.abstractmethod
82
+ def map_buffer(self, index=0, access_mode=gl.GL_READ_WRITE):
83
+ raise NotImplementedError
84
+
85
+ def unmap_buffer(self):
86
+ gl.glUnmapBuffer(gl.GL_ARRAY_BUFFER)
87
+
88
+ def get_id(self):
89
+ return self.id
ncca/ngl/base_mesh.py ADDED
@@ -0,0 +1,170 @@
1
+ from dataclasses import dataclass
2
+
3
+ import numpy as np
4
+ import OpenGL.GL as gl
5
+
6
+ from . import vao_factory
7
+ from .abstract_vao import VertexData
8
+ from .bbox import BBox
9
+ from .log import logger
10
+
11
+
12
+ class Face:
13
+ """
14
+ Simple face structure for mesh geometry.
15
+ Holds indices for vertices, UVs, and normals.
16
+ """
17
+
18
+ slots = ("vertex", "uv", "normal")
19
+
20
+ def __init__(self):
21
+ self.vertex: list[int] = []
22
+ self.uv: list[int] = []
23
+ self.normal: list[int] = []
24
+
25
+
26
+ class BaseMesh:
27
+ """
28
+ Base class for mesh geometry.
29
+ Provides storage for vertices, normals, UVs, faces, and VAO management.
30
+ """
31
+
32
+ def __init__(self):
33
+ self.vertex: list = []
34
+ self.normals: list = []
35
+ self.uv: list = []
36
+ self.faces: list[Face] = []
37
+ self.vao = None
38
+ self.bbox = None
39
+ self.min_x: float = 0.0
40
+ self.max_x: float = 0.0
41
+ self.min_y: float = 0.0
42
+ self.max_y: float = 0.0
43
+ self.min_z: float = 0.0
44
+ self.max_z: float = 0.0
45
+ self.texture_id: int = 0
46
+ self.texture: bool = False
47
+
48
+ def is_triangular(self) -> bool:
49
+ """
50
+ Check if all faces in the mesh are triangles.
51
+
52
+ Returns:
53
+ bool: True if all faces are triangles, False otherwise.
54
+ """
55
+ return all(len(f.vertex) == 3 for f in self.faces)
56
+
57
+ def create_vao(self, reset_vao: bool = False) -> None:
58
+ """
59
+ Create a Vertex Array Object (VAO) for the mesh.
60
+ Only supports triangular meshes.
61
+
62
+ Args:
63
+ reset_vao: If True, will not create a new VAO if one already exists.
64
+ Raises:
65
+ RuntimeError: If the mesh is not composed entirely of triangles.
66
+ """
67
+ if reset_vao:
68
+ if self.vao is not None:
69
+ logger.warning("VAO exist so returning")
70
+ return
71
+ else:
72
+ if self.vao is not None:
73
+ logger.warning("Creating new VAO")
74
+
75
+ data_pack_type = 0
76
+ if self.is_triangular():
77
+ data_pack_type = gl.GL_TRIANGLES
78
+ if data_pack_type == 0:
79
+ logger.error("Can only create VBO from all Triangle data at present")
80
+ raise RuntimeError("Can only create VBO from all Triangle data at present")
81
+
82
+ @dataclass
83
+ class VertData:
84
+ """
85
+ Structure for a single vertex's data, including position, normal, and UV.
86
+ """
87
+
88
+ x: float = 0.0
89
+ y: float = 0.0
90
+ z: float = 0.0
91
+ nx: float = 0.0
92
+ ny: float = 0.0
93
+ nz: float = 0.0
94
+ u: float = 0.0
95
+ v: float = 0.0
96
+
97
+ def as_array(self) -> np.ndarray:
98
+ return np.array(
99
+ [self.x, self.y, self.z, self.nx, self.ny, self.nz, self.u, self.v],
100
+ dtype=np.float32,
101
+ )
102
+
103
+ vbo_mesh: list[VertData] = []
104
+ for face in self.faces:
105
+ for i in range(3):
106
+ d = VertData()
107
+ d.x = self.vertex[face.vertex[i]].x
108
+ d.y = self.vertex[face.vertex[i]].y
109
+ d.z = self.vertex[face.vertex[i]].z
110
+ if self.normals and self.uv:
111
+ d.nx = self.normals[face.normal[i]].x
112
+ d.ny = self.normals[face.normal[i]].y
113
+ d.nz = self.normals[face.normal[i]].z
114
+ d.u = self.uv[face.uv[i]].x
115
+ d.v = 1 - self.uv[face.uv[i]].y # Flip V for OpenGL
116
+ elif self.normals and not self.uv:
117
+ d.nx = self.normals[face.normal[i]].x
118
+ d.ny = self.normals[face.normal[i]].y
119
+ d.nz = self.normals[face.normal[i]].z
120
+ elif not self.normals and self.uv:
121
+ d.u = self.uv[face.uv[i]].x
122
+ d.v = 1 - self.uv[face.uv[i]].y
123
+ vbo_mesh.append(d)
124
+
125
+ mesh_data = np.concatenate([v.as_array() for v in vbo_mesh]).astype(np.float32)
126
+ self.vao = vao_factory.VAOFactory.create_vao(
127
+ vao_factory.VAOType.SIMPLE, data_pack_type
128
+ )
129
+ with self.vao as vao:
130
+ mesh_size = len(mesh_data) // 8
131
+ vao.set_data(VertexData(mesh_data, mesh_size))
132
+ # vertex
133
+ vao.set_vertex_attribute_pointer(0, 3, gl.GL_FLOAT, 8 * 4, 0)
134
+ # normals
135
+ vao.set_vertex_attribute_pointer(1, 3, gl.GL_FLOAT, 8 * 4, 3 * 4)
136
+ # uvs
137
+ vao.set_vertex_attribute_pointer(2, 2, gl.GL_FLOAT, 8 * 4, 6 * 4)
138
+ vao.set_num_indices(mesh_size)
139
+ self.calc_dimensions()
140
+ self.bbox = BBox.from_extents(
141
+ self.min_x, self.max_x, self.min_y, self.max_y, self.min_z, self.max_z
142
+ )
143
+
144
+ def calc_dimensions(self) -> None:
145
+ """
146
+ Calculate the bounding box extents for the mesh.
147
+ Updates min_x, max_x, min_y, max_y, min_z, max_z.
148
+ """
149
+ if not self.vertex:
150
+ return
151
+ self.min_x = self.max_x = self.vertex[0].x
152
+ self.min_y = self.max_y = self.vertex[0].y
153
+ self.min_z = self.max_z = self.vertex[0].z
154
+ for v in self.vertex:
155
+ self.min_x = min(self.min_x, v.x)
156
+ self.max_x = max(self.max_x, v.x)
157
+ self.min_y = min(self.min_y, v.y)
158
+ self.max_y = max(self.max_y, v.y)
159
+ self.min_z = min(self.min_z, v.z)
160
+ self.max_z = max(self.max_z, v.z)
161
+
162
+ def draw(self) -> None:
163
+ """
164
+ Draw the mesh using its VAO and bound texture (if any).
165
+ """
166
+ if self.vao:
167
+ if self.texture_id:
168
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture_id)
169
+ with self.vao as vao:
170
+ vao.draw()
ncca/ngl/base_mesh.pyi ADDED
@@ -0,0 +1,11 @@
1
+ class Face:
2
+ vert: list
3
+ uv: list
4
+ normal: list
5
+
6
+ class BaseMesh:
7
+ verts: list
8
+ normals: list
9
+ uv: list
10
+ faces: list
11
+ def __init__(self) -> None: ...
ncca/ngl/bbox.py ADDED
@@ -0,0 +1,224 @@
1
+ from .vec3 import Vec3
2
+
3
+
4
+ class BBox:
5
+ """
6
+ A bounding box class for 3D geometry.
7
+
8
+ Stores center, dimensions, extents, vertices, and normals for a box.
9
+ Provides methods to recalculate from center/dimensions or from extents.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ center: Vec3 = Vec3(),
15
+ width: float = 2.0,
16
+ height: float = 2.0,
17
+ depth: float = 2.0,
18
+ ) -> None:
19
+ """
20
+ Initialize a bounding box from center and dimensions.
21
+
22
+ Args:
23
+ center: Center of the bounding box (Vec3)
24
+ width: Width of the box
25
+ height: Height of the box
26
+ depth: Depth of the box
27
+ """
28
+ self._center: Vec3 = center
29
+ self._width: float = width
30
+ self._height: float = height
31
+ self._depth: float = depth
32
+ self._min_x: float = 0.0
33
+ self._max_x: float = 0.0
34
+ self._min_y: float = 0.0
35
+ self._max_y: float = 0.0
36
+ self._min_z: float = 0.0
37
+ self._max_z: float = 0.0
38
+ self._verts: list[Vec3] = [Vec3() for _ in range(8)]
39
+ self._normals: list[Vec3] = [Vec3() for _ in range(6)]
40
+ self.recalculate_from_center_dims()
41
+
42
+ @classmethod
43
+ def from_extents(
44
+ cls,
45
+ min_x: float,
46
+ max_x: float,
47
+ min_y: float,
48
+ max_y: float,
49
+ min_z: float,
50
+ max_z: float,
51
+ ) -> "BBox":
52
+ """
53
+ Create a bounding box from min/max extents.
54
+
55
+ Args:
56
+ min_x, max_x, min_y, max_y, min_z, max_z: Box extents
57
+
58
+ Returns:
59
+ BBox: The constructed bounding box
60
+ """
61
+ bbox = cls()
62
+ bbox.set_extents(min_x, max_x, min_y, max_y, min_z, max_z)
63
+ return bbox
64
+
65
+ @property
66
+ def center(self) -> Vec3:
67
+ """Get or set the center of the bounding box."""
68
+ return self._center
69
+
70
+ @center.setter
71
+ def center(self, value: Vec3) -> None:
72
+ self._center = value
73
+ self.recalculate_from_center_dims()
74
+
75
+ @property
76
+ def width(self) -> float:
77
+ """Get or set the width of the bounding box."""
78
+ return self._width
79
+
80
+ @width.setter
81
+ def width(self, value: float) -> None:
82
+ self._width = value
83
+ self.recalculate_from_center_dims()
84
+
85
+ @property
86
+ def height(self) -> float:
87
+ """Get or set the height of the bounding box."""
88
+ return self._height
89
+
90
+ @height.setter
91
+ def height(self, value: float) -> None:
92
+ self._height = value
93
+ self.recalculate_from_center_dims()
94
+
95
+ @property
96
+ def depth(self) -> float:
97
+ """Get or set the depth of the bounding box."""
98
+ return self._depth
99
+
100
+ @depth.setter
101
+ def depth(self, value: float) -> None:
102
+ self._depth = value
103
+ self.recalculate_from_center_dims()
104
+
105
+ @property
106
+ def min_x(self) -> float:
107
+ """Get the minimum x extent."""
108
+ return self._min_x
109
+
110
+ @property
111
+ def max_x(self) -> float:
112
+ """Get the maximum x extent."""
113
+ return self._max_x
114
+
115
+ @property
116
+ def min_y(self) -> float:
117
+ """Get the minimum y extent."""
118
+ return self._min_y
119
+
120
+ @property
121
+ def max_y(self) -> float:
122
+ """Get the maximum y extent."""
123
+ return self._max_y
124
+
125
+ @property
126
+ def min_z(self) -> float:
127
+ """Get the minimum z extent."""
128
+ return self._min_z
129
+
130
+ @property
131
+ def max_z(self) -> float:
132
+ """Get the maximum z extent."""
133
+ return self._max_z
134
+
135
+ def get_vertex_array(self) -> list[Vec3]:
136
+ """
137
+ Get the list of 8 vertices for the bounding box.
138
+
139
+ Returns:
140
+ list[Vec3]: The 8 vertices of the box.
141
+ """
142
+ return self._verts
143
+
144
+ def get_normal_array(self) -> list[Vec3]:
145
+ """
146
+ Get the list of 6 normals for the bounding box faces.
147
+
148
+ Returns:
149
+ list[Vec3]: The 6 normals of the box.
150
+ """
151
+ return self._normals
152
+
153
+ def set_extents(
154
+ self,
155
+ min_x: float,
156
+ max_x: float,
157
+ min_y: float,
158
+ max_y: float,
159
+ min_z: float,
160
+ max_z: float,
161
+ ) -> None:
162
+ """
163
+ Set the extents of the bounding box and recalculate center/dimensions.
164
+
165
+ Args:
166
+ min_x, max_x, min_y, max_y, min_z, max_z: Box extents
167
+ """
168
+ self._min_x = min_x
169
+ self._max_x = max_x
170
+ self._min_y = min_y
171
+ self._max_y = max_y
172
+ self._min_z = min_z
173
+ self._max_z = max_z
174
+ self.recalculate_from_extents()
175
+
176
+ def recalculate_from_center_dims(self) -> None:
177
+ """
178
+ Recalculate extents and update vertices/normals from center and dimensions.
179
+ """
180
+ half_width = self._width / 2.0
181
+ half_height = self._height / 2.0
182
+ half_depth = self._depth / 2.0
183
+
184
+ self._min_x = self._center.x - half_width
185
+ self._max_x = self._center.x + half_width
186
+ self._min_y = self._center.y - half_height
187
+ self._max_y = self._center.y + half_height
188
+ self._min_z = self._center.z - half_depth
189
+ self._max_z = self._center.z + half_depth
190
+ self._update_verts_and_normals()
191
+
192
+ def recalculate_from_extents(self) -> None:
193
+ """
194
+ Recalculate center and dimensions from extents, then update vertices/normals.
195
+ """
196
+ self._width = self._max_x - self._min_x
197
+ self._height = self._max_y - self._min_y
198
+ self._depth = self._max_z - self._min_z
199
+ self._center = Vec3(
200
+ self._min_x + self._width / 2.0,
201
+ self._min_y + self._height / 2.0,
202
+ self._min_z + self._depth / 2.0,
203
+ )
204
+ self._update_verts_and_normals()
205
+
206
+ def _update_verts_and_normals(self) -> None:
207
+ """
208
+ Update the 8 vertices and 6 normals of the bounding box based on current extents.
209
+ """
210
+ self._verts[0].set(self._min_x, self._max_y, self._min_z)
211
+ self._verts[1].set(self._max_x, self._max_y, self._min_z)
212
+ self._verts[2].set(self._max_x, self._max_y, self._max_z)
213
+ self._verts[3].set(self._min_x, self._max_y, self._max_z)
214
+ self._verts[4].set(self._min_x, self._min_y, self._min_z)
215
+ self._verts[5].set(self._max_x, self._min_y, self._min_z)
216
+ self._verts[6].set(self._max_x, self._min_y, self._max_z)
217
+ self._verts[7].set(self._min_x, self._min_y, self._max_z)
218
+
219
+ self._normals[0].set(0.0, 1.0, 0.0)
220
+ self._normals[1].set(0.0, -1.0, 0.0)
221
+ self._normals[2].set(1.0, 0.0, 0.0)
222
+ self._normals[3].set(-1.0, 0.0, 0.0)
223
+ self._normals[4].set(0.0, 0.0, 1.0)
224
+ self._normals[5].set(0.0, 0.0, -1.0)
@@ -0,0 +1,75 @@
1
+ from .vec3 import Vec3
2
+
3
+
4
+ class BezierCurve:
5
+ """A Bezier curve class."""
6
+
7
+ def __init__(
8
+ self, control_points: list[Vec3] = None, knots: list[float] = None
9
+ ) -> None:
10
+ self._cp = control_points if control_points is not None else []
11
+ self._knots = knots if knots is not None else []
12
+ self._degree = 0
13
+ self._order = 0
14
+ self._num_cp = 0
15
+ self._num_knots = 0
16
+ if self._cp:
17
+ self._num_cp = len(self._cp)
18
+ self._degree = self._num_cp
19
+ self._order = self._degree + 1
20
+ if not self._knots:
21
+ self.create_knots()
22
+ self._num_knots = len(self._knots)
23
+
24
+ @property
25
+ def control_points(self) -> list[Vec3]:
26
+ return self._cp
27
+
28
+ @property
29
+ def knots(self) -> list[float]:
30
+ return self._knots
31
+
32
+ def add_point(self, x: float | Vec3, y: float = None, z: float = None) -> None:
33
+ if isinstance(x, Vec3):
34
+ self._cp.append(x)
35
+ else:
36
+ self._cp.append(Vec3(x, y, z))
37
+ self._num_cp += 1
38
+ self._degree = self._num_cp
39
+ self._order = self._degree + 1
40
+ self.create_knots()
41
+
42
+ def add_knot(self, k: float) -> None:
43
+ self._knots.append(k)
44
+ self._num_knots = len(self._knots)
45
+
46
+ def create_knots(self) -> None:
47
+ self._num_knots = self._num_cp + self._order
48
+ self._knots = [0.0] * (self._num_knots // 2) + [1.0] * (
49
+ self._num_knots - (self._num_knots // 2)
50
+ )
51
+
52
+ def get_point_on_curve(self, u: float) -> Vec3:
53
+ p = Vec3()
54
+ for i in range(self._num_cp):
55
+ val = self.cox_de_boor(u, i, self._degree, self._knots)
56
+ if val > 0.001:
57
+ p += self._cp[i] * val
58
+ return p
59
+
60
+ def cox_de_boor(self, u: float, i: int, k: int, knots: list[float]) -> float:
61
+ if k == 1:
62
+ return 1.0 if knots[i] <= u <= knots[i + 1] else 0.0
63
+
64
+ den1 = knots[i + k - 1] - knots[i]
65
+ den2 = knots[i + k] - knots[i + 1]
66
+
67
+ eq1 = 0.0
68
+ if den1 > 0:
69
+ eq1 = ((u - knots[i]) / den1) * self.cox_de_boor(u, i, k - 1, knots)
70
+
71
+ eq2 = 0.0
72
+ if den2 > 0:
73
+ eq2 = ((knots[i + k] - u) / den2) * self.cox_de_boor(u, i + 1, k - 1, knots)
74
+
75
+ return eq1 + eq2