ncca-ngl 0.3.0__py3-none-any.whl → 0.3.1__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/widgets/NGLDebug.log +0 -0
- ncca/ngl/widgets/__init__.py +12 -0
- ncca/ngl/widgets/__main__.py +77 -0
- ncca/ngl/widgets/glsl/phong.frag +27 -0
- ncca/ngl/widgets/glsl/phong.vert +14 -0
- ncca/ngl/widgets/glsl/picking.frag +7 -0
- ncca/ngl/widgets/glsl/picking.vert +7 -0
- ncca/ngl/widgets/lookatwidget.py +77 -0
- ncca/ngl/widgets/mat4widget.py +7 -0
- ncca/ngl/widgets/rgbacolourwidget.py +157 -0
- ncca/ngl/widgets/rgbcolourwidget.py +143 -0
- ncca/ngl/widgets/transformation_widget.py +299 -0
- ncca/ngl/widgets/transformwidget.py +81 -0
- ncca/ngl/widgets/vec2widget.py +141 -0
- ncca/ngl/widgets/vec3widget.py +157 -0
- ncca/ngl/widgets/vec4widget.py +178 -0
- {ncca_ngl-0.3.0.dist-info → ncca_ngl-0.3.1.dist-info}/METADATA +1 -1
- {ncca_ngl-0.3.0.dist-info → ncca_ngl-0.3.1.dist-info}/RECORD +19 -3
- {ncca_ngl-0.3.0.dist-info → ncca_ngl-0.3.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from OpenGL.GL import *
|
|
4
|
+
from PySide6.QtCore import Property, QEasingCurve, QPoint, QPropertyAnimation, Signal, Slot
|
|
5
|
+
from PySide6.QtGui import QQuaternion
|
|
6
|
+
from PySide6.QtOpenGLWidgets import QOpenGLWidget
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QApplication,
|
|
9
|
+
QMainWindow,
|
|
10
|
+
QTextEdit,
|
|
11
|
+
QVBoxLayout,
|
|
12
|
+
QWidget,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ncca.ngl.mat4 import Mat4
|
|
16
|
+
from ncca.ngl.prim_data import PrimData, Prims
|
|
17
|
+
from ncca.ngl.quaternion import Quaternion
|
|
18
|
+
from ncca.ngl.shader_lib import ShaderLib
|
|
19
|
+
from ncca.ngl.simple_vao import SimpleVAO, VertexData
|
|
20
|
+
from ncca.ngl.transform import Transform
|
|
21
|
+
from ncca.ngl.vec3 import Vec3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TransformationWidget(QOpenGLWidget):
|
|
25
|
+
matrix_updated = Signal(Mat4)
|
|
26
|
+
|
|
27
|
+
def __init__(self, parent=None):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self.transform = Transform()
|
|
30
|
+
self.projection = Mat4()
|
|
31
|
+
self.view = Mat4()
|
|
32
|
+
self.mouse_pos = QPoint()
|
|
33
|
+
self.vao = SimpleVAO()
|
|
34
|
+
|
|
35
|
+
self.face_rotations = {
|
|
36
|
+
"front": Quaternion.from_axis_angle(Vec3(0, 1, 0), 0),
|
|
37
|
+
"back": Quaternion.from_axis_angle(Vec3(0, 1, 0), 180),
|
|
38
|
+
"left": Quaternion.from_axis_angle(Vec3(0, 1, 0), -90),
|
|
39
|
+
"right": Quaternion.from_axis_angle(Vec3(0, 1, 0), 90),
|
|
40
|
+
"top": Quaternion.from_axis_angle(Vec3(1, 0, 0), -90),
|
|
41
|
+
"bottom": Quaternion.from_axis_angle(Vec3(1, 0, 0), 90),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
self.face_ids = {
|
|
45
|
+
1: "right",
|
|
46
|
+
2: "left",
|
|
47
|
+
3: "top",
|
|
48
|
+
4: "bottom",
|
|
49
|
+
5: "front",
|
|
50
|
+
6: "back",
|
|
51
|
+
}
|
|
52
|
+
self.picking_fbo = None
|
|
53
|
+
self.picking_texture = None
|
|
54
|
+
self.depth_texture = None
|
|
55
|
+
|
|
56
|
+
def initializeGL(self):
|
|
57
|
+
glClearColor(0.4, 0.4, 0.4, 1.0)
|
|
58
|
+
glEnable(GL_DEPTH_TEST)
|
|
59
|
+
glEnable(GL_MULTISAMPLE)
|
|
60
|
+
|
|
61
|
+
# ShaderLib.load_shader("picking", "glsl/picking.vert", "glsl/picking.frag")
|
|
62
|
+
# ShaderLib.load_shader("phong", "glsl/phong.vert", "glsl/phong.frag")
|
|
63
|
+
|
|
64
|
+
# cube_data = PrimData.primitive(Prims.CUBE)
|
|
65
|
+
# print(f"Cube data size: {cube_data.size}")
|
|
66
|
+
# print(f"Cube data shape: {cube_data.shape}")
|
|
67
|
+
# with self.vao:
|
|
68
|
+
# data = VertexData(data=cube_data, size=cube_data.size)
|
|
69
|
+
# self.vao.set_data(data)
|
|
70
|
+
# vert_data_size = 8 * 4 # 4 is sizeof float and 8 is x,y,z,nx,ny,nz,uv
|
|
71
|
+
# self.vao.set_vertex_attribute_pointer(0, 3, GL_FLOAT, vert_data_size, 0)
|
|
72
|
+
# self.vao.set_vertex_attribute_pointer(1, 3, GL_FLOAT, vert_data_size, Vec3.sizeof())
|
|
73
|
+
# self.vao.set_vertex_attribute_pointer(2, 2, GL_FLOAT, vert_data_size, 2 * Vec3.sizeof())
|
|
74
|
+
# self.vao.set_num_indices(cube_data.size // 8)
|
|
75
|
+
|
|
76
|
+
self.view.look_at(Vec3(0, 0, 3), Vec3(0, 0, 0), Vec3(0, 1, 0))
|
|
77
|
+
self.setFocus()
|
|
78
|
+
|
|
79
|
+
def resizeGL(self, w, h):
|
|
80
|
+
if h == 0:
|
|
81
|
+
h = 1
|
|
82
|
+
# self.projection.perspective(45, w / h, 0.01, 20)
|
|
83
|
+
# self._create_picking_buffer(w, h)
|
|
84
|
+
|
|
85
|
+
def paintGL(self):
|
|
86
|
+
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
|
87
|
+
# if self.picking_fbo is None:
|
|
88
|
+
# self._create_picking_buffer(self.width(), self.height())
|
|
89
|
+
# self._render_picking_pass()
|
|
90
|
+
# self._render_scene_pass()
|
|
91
|
+
# self.matrix_updated.emit(self.transform.get_matrix())
|
|
92
|
+
|
|
93
|
+
def _create_picking_buffer(self, w, h):
|
|
94
|
+
if self.picking_fbo is not None:
|
|
95
|
+
glDeleteFramebuffers(1, [self.picking_fbo])
|
|
96
|
+
glDeleteTextures(1, [self.picking_texture])
|
|
97
|
+
glDeleteTextures(1, [self.depth_texture])
|
|
98
|
+
|
|
99
|
+
self.picking_fbo = glGenFramebuffers(1)
|
|
100
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.picking_fbo)
|
|
101
|
+
|
|
102
|
+
self.picking_texture = glGenTextures(1)
|
|
103
|
+
glBindTexture(GL_TEXTURE_2D, self.picking_texture)
|
|
104
|
+
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, None)
|
|
105
|
+
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.picking_texture, 0)
|
|
106
|
+
|
|
107
|
+
self.depth_texture = glGenTextures(1)
|
|
108
|
+
glBindTexture(GL_TEXTURE_2D, self.depth_texture)
|
|
109
|
+
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, None)
|
|
110
|
+
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, self.depth_texture, 0)
|
|
111
|
+
|
|
112
|
+
if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE:
|
|
113
|
+
print("Framebuffer is not complete")
|
|
114
|
+
|
|
115
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
116
|
+
|
|
117
|
+
def _render_picking_pass(self):
|
|
118
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.picking_fbo)
|
|
119
|
+
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
|
120
|
+
|
|
121
|
+
ShaderLib.use("picking")
|
|
122
|
+
mvp = self.projection @ self.view @ self.transform.get_matrix()
|
|
123
|
+
ShaderLib.set_uniform("MVP", mvp)
|
|
124
|
+
with self.vao:
|
|
125
|
+
for i, face_id in self.face_ids.items():
|
|
126
|
+
r = (i & 0x0000FF) / 255.0
|
|
127
|
+
g = ((i & 0x00FF00) >> 8) / 255.0
|
|
128
|
+
b = ((i & 0xFF0000) >> 16) / 255.0
|
|
129
|
+
ShaderLib.set_uniform("face_id", Vec3(r, g, b))
|
|
130
|
+
glDrawArrays(GL_TRIANGLES, (i - 1) * 6, 6)
|
|
131
|
+
|
|
132
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
133
|
+
|
|
134
|
+
def _render_scene_pass(self):
|
|
135
|
+
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
|
136
|
+
|
|
137
|
+
ShaderLib.use("phong")
|
|
138
|
+
mvp = self.projection @ self.view @ self.transform.get_matrix()
|
|
139
|
+
ShaderLib.set_uniform("MVP", mvp)
|
|
140
|
+
ShaderLib.set_uniform("model", self.transform.get_matrix())
|
|
141
|
+
ShaderLib.set_uniform("normal_matrix", self.transform.get_matrix().inverse().transpose())
|
|
142
|
+
ShaderLib.set_uniform("light_pos", Vec3(0, 0, 3))
|
|
143
|
+
ShaderLib.set_uniform("view_pos", Vec3(0, 0, 3))
|
|
144
|
+
ShaderLib.set_uniform("light_color", Vec3(1, 1, 1))
|
|
145
|
+
ShaderLib.set_uniform("object_color", Vec3(0.6, 0.6, 0.6))
|
|
146
|
+
|
|
147
|
+
with self.vao:
|
|
148
|
+
self.vao.draw()
|
|
149
|
+
|
|
150
|
+
def mousePressEvent(self, event):
|
|
151
|
+
self.mouse_pos = event.pos()
|
|
152
|
+
|
|
153
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.picking_fbo)
|
|
154
|
+
x, y = int(event.position().x()), int(self.height() - event.position().y())
|
|
155
|
+
pixel = glReadPixels(x, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE)
|
|
156
|
+
face_id = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16)
|
|
157
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
158
|
+
|
|
159
|
+
if face_id in self.face_ids:
|
|
160
|
+
face_name = self.face_ids[face_id]
|
|
161
|
+
self.snap_to_face(face_name)
|
|
162
|
+
|
|
163
|
+
def mouseMoveEvent(self, event):
|
|
164
|
+
if event.buttons():
|
|
165
|
+
diff = event.position() - self.mouse_pos
|
|
166
|
+
self.mouse_pos = event.position()
|
|
167
|
+
|
|
168
|
+
axis = Vec3(diff.y(), diff.x(), 0)
|
|
169
|
+
angle = axis.length() * 0.5
|
|
170
|
+
if angle > 0:
|
|
171
|
+
rotation = Quaternion.from_axis_angle(axis.normalize(), angle)
|
|
172
|
+
self.transform.add_rotation(rotation)
|
|
173
|
+
self.update()
|
|
174
|
+
|
|
175
|
+
def snap_to_face(self, face_name):
|
|
176
|
+
target_rotation = self.face_rotations[face_name]
|
|
177
|
+
|
|
178
|
+
animation = QPropertyAnimation(self, b"qrotation")
|
|
179
|
+
animation.setDuration(400)
|
|
180
|
+
animation.setStartValue(self.transform.get_rotation())
|
|
181
|
+
animation.setEndValue(target_rotation)
|
|
182
|
+
animation.setEasingCurve(QEasingCurve.InOutQuad)
|
|
183
|
+
animation.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
|
|
184
|
+
|
|
185
|
+
@Property(QQuaternion)
|
|
186
|
+
def qrotation(self):
|
|
187
|
+
q = self.transform.get_rotation()
|
|
188
|
+
return QQuaternion(q.w, q.x, q.y, q.z)
|
|
189
|
+
|
|
190
|
+
@qrotation.setter
|
|
191
|
+
def set_qrotation(self, rotation):
|
|
192
|
+
q = Quaternion(rotation.scalar(), rotation.x(), rotation.y(), rotation.z())
|
|
193
|
+
self.transform.set_rotation(q)
|
|
194
|
+
self.update()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class MainWindow(QMainWindow):
|
|
198
|
+
def __init__(self):
|
|
199
|
+
super().__init__()
|
|
200
|
+
self.setWindowTitle("3D Transformation Widget")
|
|
201
|
+
self.setGeometry(100, 100, 800, 600)
|
|
202
|
+
|
|
203
|
+
central_widget = QWidget()
|
|
204
|
+
self.setCentralWidget(central_widget)
|
|
205
|
+
layout = QVBoxLayout(central_widget)
|
|
206
|
+
|
|
207
|
+
self.gl_widget = TransformationWidget()
|
|
208
|
+
layout.addWidget(self.gl_widget, 1)
|
|
209
|
+
|
|
210
|
+
self.matrix_display = QTextEdit()
|
|
211
|
+
self.matrix_display.setReadOnly(True)
|
|
212
|
+
self.matrix_display.setFixedHeight(120)
|
|
213
|
+
layout.addWidget(self.matrix_display)
|
|
214
|
+
|
|
215
|
+
self.gl_widget.matrix_updated.connect(self.update_matrix_display)
|
|
216
|
+
|
|
217
|
+
@Slot(Mat4)
|
|
218
|
+
def update_matrix_display(self, matrix):
|
|
219
|
+
self.matrix_display.setText(str(matrix))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
# We need to create some dummy shaders for this to run
|
|
224
|
+
# as the shader lib will fail otherwise.
|
|
225
|
+
import os
|
|
226
|
+
|
|
227
|
+
from PySide6.QtGui import QSurfaceFormat
|
|
228
|
+
|
|
229
|
+
if not os.path.exists("glsl"):
|
|
230
|
+
os.makedirs("glsl")
|
|
231
|
+
with open("glsl/picking.vert", "w") as f:
|
|
232
|
+
f.write("""#version 330 core
|
|
233
|
+
layout (location = 0) in vec3 aPos;
|
|
234
|
+
uniform mat4 MVP;
|
|
235
|
+
void main()
|
|
236
|
+
{
|
|
237
|
+
gl_Position = MVP * vec4(aPos, 1.0);
|
|
238
|
+
}""")
|
|
239
|
+
with open("glsl/picking.frag", "w") as f:
|
|
240
|
+
f.write("""#version 330 core
|
|
241
|
+
out vec3 FragColor;
|
|
242
|
+
uniform vec3 face_id;
|
|
243
|
+
void main()
|
|
244
|
+
{
|
|
245
|
+
FragColor = face_id;
|
|
246
|
+
}""")
|
|
247
|
+
with open("glsl/phong.vert", "w") as f:
|
|
248
|
+
f.write("""#version 330 core
|
|
249
|
+
layout (location = 0) in vec3 aPos;
|
|
250
|
+
layout (location = 1) in vec3 aNormal;
|
|
251
|
+
out vec3 FragPos;
|
|
252
|
+
out vec3 Normal;
|
|
253
|
+
uniform mat4 model;
|
|
254
|
+
uniform mat4 MVP;
|
|
255
|
+
uniform mat3 normal_matrix;
|
|
256
|
+
void main()
|
|
257
|
+
{
|
|
258
|
+
FragPos = vec3(model * vec4(aPos, 1.0));
|
|
259
|
+
Normal = normal_matrix * aNormal;
|
|
260
|
+
gl_Position = MVP * vec4(aPos, 1.0);
|
|
261
|
+
}""")
|
|
262
|
+
with open("glsl/phong.frag", "w") as f:
|
|
263
|
+
f.write("""#version 330 core
|
|
264
|
+
out vec4 FragColor;
|
|
265
|
+
in vec3 FragPos;
|
|
266
|
+
in vec3 Normal;
|
|
267
|
+
uniform vec3 light_pos;
|
|
268
|
+
uniform vec3 view_pos;
|
|
269
|
+
uniform vec3 light_color;
|
|
270
|
+
uniform vec3 object_color;
|
|
271
|
+
void main()
|
|
272
|
+
{
|
|
273
|
+
// Ambient
|
|
274
|
+
float ambient_strength = 0.1;
|
|
275
|
+
vec3 ambient = ambient_strength * light_color;
|
|
276
|
+
// Diffuse
|
|
277
|
+
vec3 norm = normalize(Normal);
|
|
278
|
+
vec3 light_dir = normalize(light_pos - FragPos);
|
|
279
|
+
float diff = max(dot(norm, light_dir), 0.0);
|
|
280
|
+
vec3 diffuse = diff * light_color;
|
|
281
|
+
// Specular
|
|
282
|
+
float specular_strength = 0.5;
|
|
283
|
+
vec3 view_dir = normalize(view_pos - FragPos);
|
|
284
|
+
vec3 reflect_dir = reflect(-light_dir, norm);
|
|
285
|
+
float spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32);
|
|
286
|
+
vec3 specular = specular_strength * spec * light_color;
|
|
287
|
+
vec3 result = (ambient + diffuse + specular) * object_color;
|
|
288
|
+
FragColor = vec4(result, 1.0);
|
|
289
|
+
}""")
|
|
290
|
+
|
|
291
|
+
app = QApplication(sys.argv)
|
|
292
|
+
format = QSurfaceFormat()
|
|
293
|
+
format.setProfile(QSurfaceFormat.CoreProfile)
|
|
294
|
+
format.setVersion(4, 1)
|
|
295
|
+
QSurfaceFormat.setDefaultFormat(format)
|
|
296
|
+
|
|
297
|
+
main_win = MainWindow()
|
|
298
|
+
main_win.show()
|
|
299
|
+
sys.exit(app.exec())
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from PySide6.QtCore import Property, QSignalBlocker, Qt, Signal
|
|
2
|
+
from PySide6.QtWidgets import QComboBox, QFrame, QLabel, QToolButton, QVBoxLayout, QWidget
|
|
3
|
+
|
|
4
|
+
from ncca.ngl import Mat4, Transform, TransformRotationOrder, Vec3
|
|
5
|
+
|
|
6
|
+
from .vec3widget import Vec3Widget
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TransformWidget(QFrame):
|
|
10
|
+
"""A widget for displaying and editing a Transform object, with foldable sections."""
|
|
11
|
+
|
|
12
|
+
valueChanged = Signal(Mat4)
|
|
13
|
+
_rotation_order = ["xyz", "yzx", "zxy", "xzy", "yxz", "zyx"]
|
|
14
|
+
|
|
15
|
+
def __init__(self, name: str, parent: QWidget | None = None) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Args:
|
|
18
|
+
name: The name of the widget.
|
|
19
|
+
parent: The parent widget.
|
|
20
|
+
"""
|
|
21
|
+
super().__init__(parent)
|
|
22
|
+
self.setFrameShape(QFrame.Shape.StyledPanel)
|
|
23
|
+
self._name = name
|
|
24
|
+
|
|
25
|
+
main_layout = QVBoxLayout(self)
|
|
26
|
+
main_layout.setContentsMargins(2, 2, 2, 2)
|
|
27
|
+
main_layout.setSpacing(0)
|
|
28
|
+
|
|
29
|
+
self._toggle_button = QToolButton(self)
|
|
30
|
+
self._toggle_button.setText(self._name)
|
|
31
|
+
self._toggle_button.setCheckable(True)
|
|
32
|
+
self._toggle_button.setChecked(True)
|
|
33
|
+
self._toggle_button.setStyleSheet("QToolButton { border: none; }")
|
|
34
|
+
self._toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
|
35
|
+
self._toggle_button.setArrowType(Qt.ArrowType.DownArrow)
|
|
36
|
+
self._toggle_button.clicked.connect(self.toggle_collapsed)
|
|
37
|
+
|
|
38
|
+
self._content_widget = QWidget(self)
|
|
39
|
+
content_layout = QVBoxLayout(self._content_widget)
|
|
40
|
+
content_layout.setContentsMargins(0, 0, 0, 0)
|
|
41
|
+
|
|
42
|
+
self._position = Vec3Widget("Position", Vec3(0.0, 0.0, 0.0), self)
|
|
43
|
+
self._rotation = Vec3Widget("Rotation", Vec3(0.0, 0.0, 0.0), self)
|
|
44
|
+
self._scale = Vec3Widget("Scale", Vec3(1.0, 1.0, 1.0), self)
|
|
45
|
+
self._rot_order = QComboBox(self)
|
|
46
|
+
for v in self._rotation_order:
|
|
47
|
+
self._rot_order.addItem(v)
|
|
48
|
+
self._position.valueChanged.connect(self._update_matrix)
|
|
49
|
+
self._rotation.valueChanged.connect(self._update_matrix)
|
|
50
|
+
self._scale.valueChanged.connect(self._update_matrix)
|
|
51
|
+
self._rot_order.currentIndexChanged.connect(self._update_matrix)
|
|
52
|
+
content_layout.addWidget(self._position)
|
|
53
|
+
content_layout.addWidget(self._rotation)
|
|
54
|
+
content_layout.addWidget(self._scale)
|
|
55
|
+
content_layout.addWidget(QLabel("Rotation Order"))
|
|
56
|
+
content_layout.addWidget(self._rot_order)
|
|
57
|
+
main_layout.addWidget(self._toggle_button)
|
|
58
|
+
main_layout.addWidget(self._content_widget)
|
|
59
|
+
|
|
60
|
+
def toggle_collapsed(self, checked: bool) -> None:
|
|
61
|
+
"""Toggles the visibility of the content widget."""
|
|
62
|
+
if checked:
|
|
63
|
+
self._toggle_button.setArrowType(Qt.ArrowType.DownArrow)
|
|
64
|
+
self._content_widget.setVisible(True)
|
|
65
|
+
else:
|
|
66
|
+
self._toggle_button.setArrowType(Qt.ArrowType.RightArrow)
|
|
67
|
+
self._content_widget.setVisible(False)
|
|
68
|
+
|
|
69
|
+
def _update_matrix(self) -> None:
|
|
70
|
+
"""Updates the transformation matrix based on the widget values."""
|
|
71
|
+
position = self._position.get_value()
|
|
72
|
+
rotation = self._rotation.get_value()
|
|
73
|
+
scale = self._scale.get_value()
|
|
74
|
+
|
|
75
|
+
tx = Transform()
|
|
76
|
+
tx.set_order(self._rot_order.currentText())
|
|
77
|
+
tx.set_position(position.x, position.y, position.z)
|
|
78
|
+
tx.set_rotation(rotation.x, rotation.y, rotation.z)
|
|
79
|
+
tx.set_scale(scale.x, scale.y, scale.z)
|
|
80
|
+
print(tx.get_matrix())
|
|
81
|
+
self.valueChanged.emit(tx.get_matrix())
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from PySide6.QtCore import Property, QSignalBlocker, Signal
|
|
2
|
+
from PySide6.QtWidgets import QDoubleSpinBox, QFrame, QHBoxLayout, QLabel, QWidget
|
|
3
|
+
|
|
4
|
+
from ncca.ngl import Vec2
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Vec2Widget(QFrame):
|
|
8
|
+
"""A widget for displaying and editing a Vec3 object."""
|
|
9
|
+
|
|
10
|
+
valueChanged = Signal(Vec2)
|
|
11
|
+
xValueChanged = Signal(float)
|
|
12
|
+
yValueChanged = Signal(float)
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str, value: Vec2 = Vec2(0.0, 0.0), parent: QWidget | None = None) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Args:
|
|
17
|
+
name: The name of the widget.
|
|
18
|
+
value: The initial value of the widget.
|
|
19
|
+
parent: The parent widget.
|
|
20
|
+
"""
|
|
21
|
+
super().__init__(parent)
|
|
22
|
+
self.setFrameShape(QFrame.Shape.StyledPanel)
|
|
23
|
+
self._value = value
|
|
24
|
+
self._name = name
|
|
25
|
+
layout = QHBoxLayout()
|
|
26
|
+
|
|
27
|
+
self.x_spinbox = self._create_spinbox(self._value.x)
|
|
28
|
+
self.y_spinbox = self._create_spinbox(self._value.y)
|
|
29
|
+
|
|
30
|
+
self._label = QLabel(self._name)
|
|
31
|
+
layout.addWidget(self._label)
|
|
32
|
+
layout.addWidget(self.x_spinbox)
|
|
33
|
+
layout.addWidget(self.y_spinbox)
|
|
34
|
+
self.setLayout(layout)
|
|
35
|
+
|
|
36
|
+
def _create_spinbox(self, value: float) -> QDoubleSpinBox:
|
|
37
|
+
"""Helper method to create and configure a QDoubleSpinBox.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
value: The initial value of the spinbox.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A configured QDoubleSpinBox.
|
|
44
|
+
"""
|
|
45
|
+
spinbox = QDoubleSpinBox()
|
|
46
|
+
spinbox.setValue(value)
|
|
47
|
+
spinbox.setRange(-5.0, 5.0)
|
|
48
|
+
spinbox.setSingleStep(0.01)
|
|
49
|
+
spinbox.valueChanged.connect(self._on_value_changed)
|
|
50
|
+
return spinbox
|
|
51
|
+
|
|
52
|
+
def get_value(self) -> Vec2:
|
|
53
|
+
"""
|
|
54
|
+
Returns:
|
|
55
|
+
The current value of the widget.
|
|
56
|
+
"""
|
|
57
|
+
return self._value
|
|
58
|
+
|
|
59
|
+
def _on_value_changed(self, value: float) -> None:
|
|
60
|
+
"""This slot is called when the value of a spinbox changes.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
value: The new value of the spinbox.
|
|
64
|
+
"""
|
|
65
|
+
sender = self.sender()
|
|
66
|
+
if sender == self.x_spinbox:
|
|
67
|
+
self._value.x = value
|
|
68
|
+
self.xValueChanged.emit(value)
|
|
69
|
+
elif sender == self.y_spinbox:
|
|
70
|
+
self._value.y = value
|
|
71
|
+
self.yValueChanged.emit(value)
|
|
72
|
+
# emit the Vec2 value changed signal
|
|
73
|
+
self.valueChanged.emit(self._value)
|
|
74
|
+
|
|
75
|
+
def set_value(self, value: Vec2) -> None:
|
|
76
|
+
"""Sets the value of the widget.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
value: The new value of the widget.
|
|
80
|
+
"""
|
|
81
|
+
with QSignalBlocker(self.x_spinbox), QSignalBlocker(self.y_spinbox), QSignalBlocker(self.z_spinbox):
|
|
82
|
+
self.x_spinbox.setValue(value.x)
|
|
83
|
+
self.y_spinbox.setValue(value.y)
|
|
84
|
+
self._value = value
|
|
85
|
+
self.valueChanged.emit(self._value)
|
|
86
|
+
|
|
87
|
+
def get_name(self) -> str:
|
|
88
|
+
"""
|
|
89
|
+
Returns:
|
|
90
|
+
The name of the widget.
|
|
91
|
+
"""
|
|
92
|
+
return self._name
|
|
93
|
+
|
|
94
|
+
def set_range(self, min_val: float, max_val: float) -> None:
|
|
95
|
+
"""Sets the range for all spinboxes.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
min_val: The minimum value.
|
|
99
|
+
max_val: The maximum value.
|
|
100
|
+
"""
|
|
101
|
+
for spinbox in (self.x_spinbox, self.y_spinbox):
|
|
102
|
+
spinbox.setRange(min_val, max_val)
|
|
103
|
+
|
|
104
|
+
def set_x_range(self, min_val: float, max_val: float) -> None:
|
|
105
|
+
"""Sets the range for the x spinbox.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
min_val: The minimum value.
|
|
109
|
+
max_val: The maximum value.
|
|
110
|
+
"""
|
|
111
|
+
self.x_spinbox.setRange(min_val, max_val)
|
|
112
|
+
|
|
113
|
+
def set_y_range(self, min_val: float, max_val: float) -> None:
|
|
114
|
+
"""Sets the range for the y spinbox.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
min_val: The minimum value.
|
|
118
|
+
max_val: The maximum value.
|
|
119
|
+
"""
|
|
120
|
+
self.y_spinbox.setRange(min_val, max_val)
|
|
121
|
+
|
|
122
|
+
def set_single_step(self, step: float) -> None:
|
|
123
|
+
"""Sets the single step for all spinboxes.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
step: The single step value.
|
|
127
|
+
"""
|
|
128
|
+
for spinbox in (self.x_spinbox, self.y_spinbox):
|
|
129
|
+
spinbox.setSingleStep(step)
|
|
130
|
+
|
|
131
|
+
def set_name(self, name: str) -> None:
|
|
132
|
+
"""Sets the name of the widget.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
name: The new name of the widget.
|
|
136
|
+
"""
|
|
137
|
+
self._name = name
|
|
138
|
+
self._label.setText(name)
|
|
139
|
+
|
|
140
|
+
value = Property(Vec2, get_value, set_value)
|
|
141
|
+
name = Property(str, get_name, set_name)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from PySide6.QtCore import Property, QSignalBlocker, Signal
|
|
2
|
+
from PySide6.QtWidgets import QDoubleSpinBox, QFrame, QHBoxLayout, QLabel, QWidget
|
|
3
|
+
|
|
4
|
+
from ncca.ngl import Vec3
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Vec3Widget(QFrame):
|
|
8
|
+
"""A widget for displaying and editing a Vec3 object."""
|
|
9
|
+
|
|
10
|
+
valueChanged = Signal(Vec3)
|
|
11
|
+
xValueChanged = Signal(float)
|
|
12
|
+
yValueChanged = Signal(float)
|
|
13
|
+
zValueChanged = Signal(float)
|
|
14
|
+
|
|
15
|
+
def __init__(self, name: str, value: Vec3 = Vec3(0.0, 0.0, 0.0), parent: QWidget | None = None) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Args:
|
|
18
|
+
name: The name of the widget.
|
|
19
|
+
value: The initial value of the widget.
|
|
20
|
+
parent: The parent widget.
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(parent)
|
|
23
|
+
self.setFrameShape(QFrame.Shape.StyledPanel)
|
|
24
|
+
self._value = value
|
|
25
|
+
self._name = name
|
|
26
|
+
layout = QHBoxLayout()
|
|
27
|
+
|
|
28
|
+
self.x_spinbox = self._create_spinbox(self._value.x)
|
|
29
|
+
self.y_spinbox = self._create_spinbox(self._value.y)
|
|
30
|
+
self.z_spinbox = self._create_spinbox(self._value.z)
|
|
31
|
+
|
|
32
|
+
self._label = QLabel(self._name)
|
|
33
|
+
layout.addWidget(self._label)
|
|
34
|
+
layout.addWidget(self.x_spinbox)
|
|
35
|
+
layout.addWidget(self.y_spinbox)
|
|
36
|
+
layout.addWidget(self.z_spinbox)
|
|
37
|
+
self.setLayout(layout)
|
|
38
|
+
|
|
39
|
+
def _create_spinbox(self, value: float) -> QDoubleSpinBox:
|
|
40
|
+
"""Helper method to create and configure a QDoubleSpinBox.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
value: The initial value of the spinbox.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A configured QDoubleSpinBox.
|
|
47
|
+
"""
|
|
48
|
+
spinbox = QDoubleSpinBox()
|
|
49
|
+
spinbox.setValue(value)
|
|
50
|
+
spinbox.setRange(-5.0, 5.0)
|
|
51
|
+
spinbox.setSingleStep(0.01)
|
|
52
|
+
spinbox.valueChanged.connect(self._on_value_changed)
|
|
53
|
+
return spinbox
|
|
54
|
+
|
|
55
|
+
def get_value(self) -> Vec3:
|
|
56
|
+
"""
|
|
57
|
+
Returns:
|
|
58
|
+
The current value of the widget.
|
|
59
|
+
"""
|
|
60
|
+
return self._value
|
|
61
|
+
|
|
62
|
+
def _on_value_changed(self, value: float) -> None:
|
|
63
|
+
"""This slot is called when the value of a spinbox changes.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
value: The new value of the spinbox.
|
|
67
|
+
"""
|
|
68
|
+
sender = self.sender()
|
|
69
|
+
if sender == self.x_spinbox:
|
|
70
|
+
self._value.x = value
|
|
71
|
+
self.xValueChanged.emit(value)
|
|
72
|
+
elif sender == self.y_spinbox:
|
|
73
|
+
self._value.y = value
|
|
74
|
+
self.yValueChanged.emit(value)
|
|
75
|
+
elif sender == self.z_spinbox:
|
|
76
|
+
self._value.z = value
|
|
77
|
+
self.zValueChanged.emit(value)
|
|
78
|
+
# emit the Vec3 value changed signal
|
|
79
|
+
self.valueChanged.emit(self._value)
|
|
80
|
+
|
|
81
|
+
def set_value(self, value: Vec3) -> None:
|
|
82
|
+
"""Sets the value of the widget.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
value: The new value of the widget.
|
|
86
|
+
"""
|
|
87
|
+
with QSignalBlocker(self.x_spinbox), QSignalBlocker(self.y_spinbox), QSignalBlocker(self.z_spinbox):
|
|
88
|
+
self.x_spinbox.setValue(value.x)
|
|
89
|
+
self.y_spinbox.setValue(value.y)
|
|
90
|
+
self.z_spinbox.setValue(value.z)
|
|
91
|
+
self._value = value
|
|
92
|
+
self.valueChanged.emit(self._value)
|
|
93
|
+
|
|
94
|
+
def get_name(self) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Returns:
|
|
97
|
+
The name of the widget.
|
|
98
|
+
"""
|
|
99
|
+
return self._name
|
|
100
|
+
|
|
101
|
+
def set_range(self, min_val: float, max_val: float) -> None:
|
|
102
|
+
"""Sets the range for all spinboxes.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
min_val: The minimum value.
|
|
106
|
+
max_val: The maximum value.
|
|
107
|
+
"""
|
|
108
|
+
for spinbox in (self.x_spinbox, self.y_spinbox, self.z_spinbox):
|
|
109
|
+
spinbox.setRange(min_val, max_val)
|
|
110
|
+
|
|
111
|
+
def set_x_range(self, min_val: float, max_val: float) -> None:
|
|
112
|
+
"""Sets the range for the x spinbox.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
min_val: The minimum value.
|
|
116
|
+
max_val: The maximum value.
|
|
117
|
+
"""
|
|
118
|
+
self.x_spinbox.setRange(min_val, max_val)
|
|
119
|
+
|
|
120
|
+
def set_y_range(self, min_val: float, max_val: float) -> None:
|
|
121
|
+
"""Sets the range for the y spinbox.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
min_val: The minimum value.
|
|
125
|
+
max_val: The maximum value.
|
|
126
|
+
"""
|
|
127
|
+
self.y_spinbox.setRange(min_val, max_val)
|
|
128
|
+
|
|
129
|
+
def set_z_range(self, min_val: float, max_val: float) -> None:
|
|
130
|
+
"""Sets the range for the z spinbox.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
min_val: The minimum value.
|
|
134
|
+
max_val: The maximum value.
|
|
135
|
+
"""
|
|
136
|
+
self.z_spinbox.setRange(min_val, max_val)
|
|
137
|
+
|
|
138
|
+
def set_single_step(self, step: float) -> None:
|
|
139
|
+
"""Sets the single step for all spinboxes.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
step: The single step value.
|
|
143
|
+
"""
|
|
144
|
+
for spinbox in (self.x_spinbox, self.y_spinbox, self.z_spinbox):
|
|
145
|
+
spinbox.setSingleStep(step)
|
|
146
|
+
|
|
147
|
+
def set_name(self, name: str) -> None:
|
|
148
|
+
"""Sets the name of the widget.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
name: The new name of the widget.
|
|
152
|
+
"""
|
|
153
|
+
self._name = name
|
|
154
|
+
self._label.setText(name)
|
|
155
|
+
|
|
156
|
+
value = Property(Vec3, get_value, set_value)
|
|
157
|
+
name = Property(str, get_name, set_name)
|