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/PrimData/pack_arrays.py +20 -0
- ncca/ngl/__init__.py +100 -0
- ncca/ngl/abstract_vao.py +89 -0
- ncca/ngl/base_mesh.py +170 -0
- ncca/ngl/base_mesh.pyi +11 -0
- ncca/ngl/bbox.py +224 -0
- ncca/ngl/bezier_curve.py +75 -0
- ncca/ngl/first_person_camera.py +174 -0
- ncca/ngl/image.py +94 -0
- ncca/ngl/log.py +44 -0
- ncca/ngl/mat2.py +128 -0
- ncca/ngl/mat3.py +466 -0
- ncca/ngl/mat4.py +456 -0
- ncca/ngl/multi_buffer_vao.py +49 -0
- ncca/ngl/obj.py +416 -0
- ncca/ngl/plane.py +47 -0
- ncca/ngl/primitives.py +706 -0
- ncca/ngl/pyside_event_handling_mixin.py +318 -0
- ncca/ngl/quaternion.py +112 -0
- ncca/ngl/random.py +167 -0
- ncca/ngl/shader.py +229 -0
- ncca/ngl/shader_lib.py +536 -0
- ncca/ngl/shader_program.py +816 -0
- ncca/ngl/simple_index_vao.py +65 -0
- ncca/ngl/simple_vao.py +42 -0
- ncca/ngl/text.py +346 -0
- ncca/ngl/texture.py +75 -0
- ncca/ngl/transform.py +95 -0
- ncca/ngl/util.py +128 -0
- ncca/ngl/vao_factory.py +34 -0
- ncca/ngl/vec2.py +350 -0
- ncca/ngl/vec2_array.py +106 -0
- ncca/ngl/vec3.py +401 -0
- ncca/ngl/vec3_array.py +110 -0
- ncca/ngl/vec4.py +229 -0
- ncca/ngl/vec4_array.py +106 -0
- ncca_ngl-0.1.0.dist-info/METADATA +23 -0
- ncca_ngl-0.1.0.dist-info/RECORD +41 -0
- ncca_ngl-0.1.0.dist-info/WHEEL +5 -0
- ncca_ngl-0.1.0.dist-info/licenses/LICENSE.txt +7 -0
- ncca_ngl-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|
ncca/ngl/abstract_vao.py
ADDED
|
@@ -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
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)
|
ncca/ngl/bezier_curve.py
ADDED
|
@@ -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
|