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.
ncca/ngl/obj.py ADDED
@@ -0,0 +1,416 @@
1
+ from .base_mesh import BaseMesh, Face
2
+ from .texture import Texture
3
+ from .vec3 import Vec3
4
+
5
+
6
+ class ObjParseVertexError(Exception):
7
+ pass
8
+
9
+
10
+ class ObjParseNormalError(Exception):
11
+ pass
12
+
13
+
14
+ class ObjParseUVError(Exception):
15
+ pass
16
+
17
+
18
+ class ObjParseFaceError(Exception):
19
+ pass
20
+
21
+
22
+ class Obj(BaseMesh):
23
+ """
24
+ OBJ mesh loader and exporter.
25
+
26
+ Inherits from BaseMesh and provides methods to parse, load, and save OBJ files,
27
+ including support for vertices, normals, UVs, faces, and optional vertex colors.
28
+ """
29
+
30
+ def __init__(self):
31
+ """
32
+ Initialize an empty OBJ mesh.
33
+ Tracks current offsets for vertices, normals, and UVs to handle negative indices.
34
+ """
35
+ super().__init__()
36
+ # as faces can use negative index values keep track of index
37
+ self._current_vertex_offset: int = 0
38
+ self._current_normal_offset: int = 0
39
+ self._current_uv_offset: int = 0
40
+
41
+ def _parse_vertex(self, tokens: list[str]) -> None:
42
+ """
43
+ Parse a vertex line from the OBJ file.
44
+
45
+ Args:
46
+ tokens: List of string tokens from the line.
47
+ Raises:
48
+ ObjParseVertexError: If vertex parsing fails.
49
+ """
50
+ try:
51
+ self.vertex.append(
52
+ Vec3(float(tokens[1]), float(tokens[2]), float(tokens[3]))
53
+ )
54
+ self._current_vertex_offset += 1
55
+ if len(tokens) == 7: # we have the non standard colour
56
+ if not hasattr(self, "colour"):
57
+ self.colour = []
58
+ self.colour.append(
59
+ Vec3(float(tokens[4]), float(tokens[5]), float(tokens[6]))
60
+ )
61
+ except ValueError:
62
+ raise ObjParseVertexError
63
+
64
+ def _parse_normal(self, tokens: list[str]) -> None:
65
+ """
66
+ Parse a normal line from the OBJ file.
67
+
68
+ Args:
69
+ tokens: List of string tokens from the line.
70
+ Raises:
71
+ ObjParseNormalError: If normal parsing fails.
72
+ """
73
+ try:
74
+ self.normals.append(
75
+ Vec3(float(tokens[1]), float(tokens[2]), float(tokens[3]))
76
+ )
77
+ self._current_normal_offset += 1
78
+ except ValueError:
79
+ raise ObjParseNormalError
80
+
81
+ def _parse_uv(self, tokens: list[str]) -> None:
82
+ """
83
+ Parse a UV line from the OBJ file.
84
+
85
+ Args:
86
+ tokens: List of string tokens from the line.
87
+ Raises:
88
+ ObjParseUVError: If UV parsing fails.
89
+ """
90
+ try:
91
+ # some DCC's use vec3 for UV so may as well support
92
+ z = 0.0
93
+ if len(tokens) == 4:
94
+ z = float(tokens[3])
95
+ self.uv.append(Vec3(float(tokens[1]), float(tokens[2]), z))
96
+ self._current_uv_offset += 1
97
+ except ValueError:
98
+ raise ObjParseUVError
99
+
100
+ def _parse_face_vertex_normal_uv(self, tokens: list[str]) -> None:
101
+ """
102
+ Parse a face line with vertex/uv/normal indices (f v/vt/vn ...).
103
+
104
+ Args:
105
+ tokens: List of string tokens from the line.
106
+ Raises:
107
+ ObjParseFaceError: If face parsing fails.
108
+ """
109
+ f = Face()
110
+ for token in tokens[1:]: # skip f
111
+ # each one of these should be v/vt/vn
112
+ vn = token.split("/")
113
+ try:
114
+ # note we need to subtract one from the list as obj index from 1
115
+ idx = int(vn[0]) - 1
116
+ if idx < 0: # negative index so grab the index
117
+ # note we index from 0 not 1 like obj so adjust
118
+ idx = self._current_vertex_offset + (idx + 1)
119
+ f.vertex.append(idx)
120
+ # same for UV
121
+ idx = int(vn[1]) - 1
122
+ if idx < 0: # negative index so grab the index
123
+ # note we index from 0 not 1 like obj so adjust
124
+ idx = self._current_uv_offset + (idx + 1)
125
+ f.uv.append(idx)
126
+ # same for normals
127
+ idx = int(vn[2]) - 1
128
+ if idx < 0: # negative index so grab the index
129
+ # note we index from 0 not 1 like obj so adjust
130
+ idx = self._current_normal_offset + (idx + 1)
131
+ f.normal.append(idx)
132
+ except ValueError:
133
+ raise ObjParseFaceError
134
+ self.faces.append(f)
135
+
136
+ def _parse_face_vertex(self, tokens: list[str]) -> None:
137
+ """
138
+ Parse a face line with only vertex indices (f v v v ...).
139
+
140
+ Args:
141
+ tokens: List of string tokens from the line.
142
+ Raises:
143
+ ObjParseFaceError: If face parsing fails.
144
+ """
145
+ f = Face()
146
+ for token in tokens[1:]: # skip f
147
+ # each one of these should be v v
148
+ try:
149
+ # note we need to subtract one from the list as obj index from 1
150
+ idx = int(token) - 1
151
+ if idx < 0: # negative index so grab the index
152
+ # note we index from 0 not 1 like obj so adjust
153
+ idx = self._current_vertex_offset + (idx + 1)
154
+ f.vertex.append(idx)
155
+ except ValueError:
156
+ raise ObjParseFaceError
157
+ self.faces.append(f)
158
+
159
+ def _parse_face_vertex_normal(self, tokens: list[str]) -> None:
160
+ """
161
+ Parse a face line with vertex//normal indices (f v//vn ...).
162
+
163
+ Args:
164
+ tokens: List of string tokens from the line.
165
+ Raises:
166
+ ObjParseFaceError: If face parsing fails.
167
+ """
168
+ f = Face()
169
+ for token in tokens[1:]: # skip f
170
+ # each one of these should be v//vn
171
+ vn = token.split("//")
172
+ try:
173
+ # note we need to subtract one from the list as obj index from 1
174
+ idx = int(vn[0]) - 1
175
+ if idx < 0: # negative index so grab the index
176
+ # note we index from 0 not 1 like obj so adjust
177
+ idx = self._current_vertex_offset + (idx + 1)
178
+ f.vertex.append(idx)
179
+ # same for normals
180
+ idx = int(vn[1]) - 1
181
+ if idx < 0: # negative index so grab the index
182
+ # note we index from 0 not 1 like obj so adjust
183
+ idx = self._current_normal_offset + (idx + 1)
184
+ f.normal.append(idx)
185
+ except ValueError:
186
+ raise ObjParseFaceError
187
+ self.faces.append(f)
188
+
189
+ def _parse_face_vertex_uv(self, tokens: list[str]) -> None:
190
+ """
191
+ Parse a face line with vertex/uv indices (f v/vt ...).
192
+
193
+ Args:
194
+ tokens: List of string tokens from the line.
195
+ Raises:
196
+ ObjParseFaceError: If face parsing fails.
197
+ """
198
+ f = Face()
199
+ for token in tokens[1:]: # skip f
200
+ # each one of these should be v/vt
201
+ vn = token.split("/")
202
+ try:
203
+ # note we need to subtract one from the list as obj index from 1
204
+ idx = int(vn[0]) - 1
205
+ if idx < 0: # negative index so grab the index
206
+ # note we index from 0 not 1 like obj so adjust
207
+ idx = self._current_vertex_offset + (idx + 1)
208
+ f.vertex.append(idx)
209
+ # same for uv
210
+ idx = int(vn[1]) - 1
211
+ if idx < 0: # negative index so grab the index
212
+ # note we index from 0 not 1 like obj so adjust
213
+ idx = self._current_uv_offset + (idx + 1)
214
+ f.uv.append(idx)
215
+ except ValueError:
216
+ raise ObjParseFaceError
217
+ self.faces.append(f)
218
+
219
+ def _parse_face(self, tokens: list[str]) -> None:
220
+ """
221
+ Parse a face line, dispatching to the correct face parser based on format.
222
+
223
+ Args:
224
+ tokens: List of string tokens from the line.
225
+ """
226
+ # first let's find what sort of face we are dealing with I assume most likely case is all
227
+ if tokens[1].count("/") == 2 and tokens[1].find("//") == -1:
228
+ self._parse_face_vertex_normal_uv(tokens)
229
+ elif tokens[1].find("/") == -1:
230
+ self._parse_face_vertex(tokens)
231
+ elif tokens[1].find("//") != -1:
232
+ self._parse_face_vertex_normal(tokens)
233
+ # if we have 1 / it is a VertUV format
234
+ elif tokens[1].count("/") == 1:
235
+ self._parse_face_vertex_uv(tokens)
236
+
237
+ def load(self, file: str) -> bool:
238
+ """
239
+ Load an OBJ file and parse its contents into the mesh.
240
+
241
+ Args:
242
+ file: Path to the OBJ file.
243
+
244
+ Returns:
245
+ bool: True if loading was successful.
246
+ """
247
+ with open(file, "r") as obj_file:
248
+ lines = obj_file.readlines()
249
+ for line in lines:
250
+ line = line.strip() # strip whitespace
251
+ if len(line) > 0: # skip empty lines
252
+ tokens = line.split()
253
+ if tokens[0] == "v":
254
+ self._parse_vertex(tokens)
255
+ elif tokens[0] == "vn":
256
+ self._parse_normal(tokens)
257
+ elif tokens[0] == "vt":
258
+ self._parse_uv(tokens)
259
+ elif tokens[0] == "f":
260
+ self._parse_face(tokens)
261
+ return True
262
+
263
+ @classmethod
264
+ def from_file(cls, fname: str) -> "Obj":
265
+ """
266
+ Create an Obj instance from a file.
267
+
268
+ Args:
269
+ fname: Path to the OBJ file.
270
+
271
+ Returns:
272
+ Obj: The loaded Obj instance.
273
+ """
274
+ obj = Obj()
275
+ obj.load(fname)
276
+ return obj
277
+
278
+ def add_vertex(self, vertex: Vec3) -> None:
279
+ """
280
+ Add a vertex to the mesh.
281
+
282
+ Args:
283
+ vertex: The vertex to add.
284
+ """
285
+ self.vertex.append(vertex)
286
+
287
+ def add_vertex_colour(self, vertex: Vec3, colour: Vec3) -> None:
288
+ """
289
+ Add a vertex and its color to the mesh.
290
+
291
+ Args:
292
+ vertex: The vertex to add.
293
+ colour: The color to associate with the vertex.
294
+ """
295
+ self.vertex.append(vertex)
296
+ if not hasattr(self, "colour"):
297
+ self.colour = []
298
+ self.colour.append(colour)
299
+
300
+ def add_normal(self, normal: Vec3) -> None:
301
+ """
302
+ Add a normal to the mesh.
303
+
304
+ Args:
305
+ normal: The normal to add.
306
+ """
307
+ self.normals.append(normal)
308
+
309
+ def add_uv(self, uv: Vec3) -> None:
310
+ """
311
+ Add a UV coordinate to the mesh.
312
+
313
+ Args:
314
+ uv: The UV coordinate to add.
315
+ """
316
+ self.uv.append(uv)
317
+
318
+ def add_face(self, face: Face) -> None:
319
+ """
320
+ Add a face to the mesh.
321
+
322
+ Args:
323
+ face: The face to add.
324
+ """
325
+ self.faces.append(face)
326
+
327
+ def save(self, filename: str) -> None:
328
+ """
329
+ Save the mesh to an OBJ file.
330
+
331
+ Args:
332
+ filename: Path to the output OBJ file.
333
+ """
334
+ with open(filename, "w") as obj_file:
335
+ obj_file.write("# This file was created by nccapy/Geo/Obj.py exporter\n")
336
+ self._write_vertices(obj_file)
337
+ self._write_uvs(obj_file)
338
+ self._write_normals(obj_file)
339
+ self._write_faces(obj_file)
340
+
341
+ def _write_vertices(self, obj_file) -> None:
342
+ """
343
+ Write vertices (and optional colors) to the OBJ file.
344
+
345
+ Args:
346
+ obj_file: Open file object for writing.
347
+ """
348
+ for i, v in enumerate(self.vertex):
349
+ obj_file.write(f"v {v.x} {v.y} {v.z} ")
350
+ if hasattr(self, "colour"): # write colour if present
351
+ obj_file.write(
352
+ f"{self.colour[i].x} {self.colour[i].y} {self.colour[i].z} "
353
+ )
354
+ obj_file.write("\n")
355
+
356
+ def _write_uvs(self, obj_file) -> None:
357
+ """
358
+ Write UV coordinates to the OBJ file.
359
+
360
+ Args:
361
+ obj_file: Open file object for writing.
362
+ """
363
+ for v in self.uv:
364
+ obj_file.write(f"vt {v.x} {v.y} \n")
365
+
366
+ def _write_normals(self, obj_file) -> None:
367
+ """
368
+ Write normals to the OBJ file.
369
+
370
+ Args:
371
+ obj_file: Open file object for writing.
372
+ """
373
+ for v in self.normals:
374
+ obj_file.write(f"vn {v.x} {v.y} {v.z} \n")
375
+
376
+ def _write_faces(self, obj_file) -> None:
377
+ """
378
+ Write faces to the OBJ file.
379
+
380
+ Args:
381
+ obj_file: Open file object for writing.
382
+ """
383
+ for face in self.faces:
384
+ obj_file.write("f")
385
+ for i in range(len(face.vertex)):
386
+ obj_file.write(f" {face.vertex[i] + 1}")
387
+ if len(face.uv) != 0:
388
+ obj_file.write(f"/{face.uv[i] + 1}")
389
+ if len(face.normal) != 0:
390
+ if len(face.uv) == 0:
391
+ obj_file.write("//")
392
+ else:
393
+ obj_file.write("/")
394
+ obj_file.write(f"{face.normal[i] + 1}")
395
+ obj_file.write("\n")
396
+
397
+ @classmethod
398
+ def obj_with_vao(cls, mesh_name: str, texture_name: str = None) -> "Obj":
399
+ """
400
+ Load an OBJ mesh and optionally a texture, then create a VAO.
401
+
402
+ Args:
403
+ mesh_name: Path to the OBJ mesh file.
404
+ texture_name: Optional path to the texture file.
405
+
406
+ Returns:
407
+ Obj: The loaded and VAO-initialized mesh.
408
+ """
409
+ mesh = Obj()
410
+ mesh.load(mesh_name)
411
+ if texture_name:
412
+ texture = Texture(texture_name)
413
+ mesh.texture_id = texture.set_texture_gl()
414
+ print(f"{mesh.texture_id=}")
415
+ mesh.create_vao()
416
+ return mesh
ncca/ngl/plane.py ADDED
@@ -0,0 +1,47 @@
1
+ from .vec3 import Vec3
2
+
3
+
4
+ class Plane:
5
+ """A mathematical plane."""
6
+
7
+ def __init__(self, p1: Vec3 = None, p2: Vec3 = None, p3: Vec3 = None) -> None:
8
+ self._normal = Vec3(0.0, 1.0, 0.0)
9
+ self._point = Vec3()
10
+ self._d = 0.0
11
+ if p1 and p2 and p3:
12
+ self.set_points(p1, p2, p3)
13
+
14
+ @property
15
+ def normal(self) -> Vec3:
16
+ return self._normal
17
+
18
+ @property
19
+ def point(self) -> Vec3:
20
+ return self._point
21
+
22
+ @property
23
+ def d(self) -> float:
24
+ return self._d
25
+
26
+ def set_points(self, p1: Vec3, p2: Vec3, p3: Vec3) -> None:
27
+ aux1 = p1 - p2
28
+ aux2 = p3 - p2
29
+ self._normal = aux2.cross(aux1)
30
+ self._normal.normalize()
31
+ self._point = p2
32
+ self._d = -(self._normal.inner(self._point))
33
+
34
+ def set_normal_point(self, normal: Vec3, point: Vec3) -> None:
35
+ self._normal = normal
36
+ self._normal.normalize()
37
+ self._point = point
38
+ self._d = -(self._normal.inner(self._point))
39
+
40
+ def set_floats(self, a: float, b: float, c: float, d: float) -> None:
41
+ self._normal.set(a, b, c)
42
+ length = self._normal.length()
43
+ self._normal.normalize()
44
+ self._d = d / length
45
+
46
+ def distance(self, p: Vec3) -> float:
47
+ return self._d + self._normal.inner(p)