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.
@@ -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)