q3dviewer 1.1.9__tar.gz → 1.2.1__tar.gz

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.
Files changed (43) hide show
  1. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/PKG-INFO +1 -1
  2. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/base_glwidget.py +0 -3
  3. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/__init__.py +1 -0
  4. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/cloud_item.py +5 -16
  5. q3dviewer-1.2.1/q3dviewer/custom_items/mesh_item.py +342 -0
  6. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/cloud_viewer.py +17 -5
  7. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/cloud_io.py +21 -0
  8. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/range_slider.py +7 -5
  9. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/PKG-INFO +1 -1
  10. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/SOURCES.txt +1 -0
  11. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/requires.txt +1 -0
  12. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/setup.py +2 -1
  13. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/README.md +0 -0
  14. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/Qt/__init__.py +0 -0
  15. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/__init__.py +0 -0
  16. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/base_item.py +0 -0
  17. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/axis_item.py +0 -0
  18. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/cloud_io_item.py +0 -0
  19. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/frame_item.py +0 -0
  20. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/gaussian_item.py +0 -0
  21. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/grid_item.py +0 -0
  22. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/image_item.py +0 -0
  23. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/line_item.py +0 -0
  24. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/text3d_item.py +0 -0
  25. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/custom_items/text_item.py +0 -0
  26. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/glwidget.py +0 -0
  27. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/__init__.py +0 -0
  28. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/example_viewer.py +0 -0
  29. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/film_maker.py +0 -0
  30. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/gaussian_viewer.py +0 -0
  31. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/lidar_calib.py +0 -0
  32. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/lidar_cam_calib.py +0 -0
  33. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/tools/ros_viewer.py +0 -0
  34. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/__init__.py +0 -0
  35. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/convert_ros_msg.py +0 -0
  36. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/gl_helper.py +0 -0
  37. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/helpers.py +0 -0
  38. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/utils/maths.py +0 -0
  39. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer/viewer.py +0 -0
  40. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/dependency_links.txt +0 -0
  41. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/entry_points.txt +0 -0
  42. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/q3dviewer.egg-info/top_level.txt +0 -0
  43. {q3dviewer-1.1.9 → q3dviewer-1.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: q3dviewer
3
- Version: 1.1.9
3
+ Version: 1.2.1
4
4
  Summary: A library designed for quickly deploying a 3D viewer.
5
5
  Home-page: https://github.com/scomup/q3dviewer
6
6
  Author: Liu Yang
@@ -399,8 +399,6 @@ class BaseGLWidget(QOpenGLWidget):
399
399
  points.append((x0 + dx, y0 + dy))
400
400
  points = sorted(points, key=lambda p: (p[0]-x0)**2 + (p[1]-y0)**2)
401
401
 
402
- print("points to check:", len(points))
403
-
404
402
  gl_y0 = height - y0 - 1
405
403
  z = 1.0
406
404
  for x, y in points:
@@ -411,7 +409,6 @@ class BaseGLWidget(QOpenGLWidget):
411
409
  z = glReadPixels(x, gl_y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
412
410
  z = np.frombuffer(z, dtype=np.float32)[0]
413
411
  if z != 1.0 and z != 0.0:
414
- print("dist to p:", np.sqrt((x - x0)**2 + (y - y0)**2))
415
412
  break
416
413
 
417
414
  if z == 1.0 or z == 0.0:
@@ -8,3 +8,4 @@ from q3dviewer.custom_items.text_item import Text2DItem
8
8
  from q3dviewer.custom_items.image_item import ImageItem
9
9
  from q3dviewer.custom_items.line_item import LineItem
10
10
  from q3dviewer.custom_items.text3d_item import Text3DItem
11
+ from q3dviewer.custom_items.mesh_item import MeshItem
@@ -42,8 +42,7 @@ class CloudItem(BaseItem):
42
42
  def __init__(self, size, alpha,
43
43
  color_mode='I',
44
44
  color='white',
45
- point_type='PIXEL',
46
- depth_test=False):
45
+ point_type='PIXEL'):
47
46
  super().__init__()
48
47
  self.STRIDE = 16 # stride of cloud array
49
48
  self.valid_buff_top = 0
@@ -70,7 +69,6 @@ class CloudItem(BaseItem):
70
69
  self.need_update_setting = True
71
70
  self.max_cloud_size = 300000000
72
71
  # Enable depth test when full opaque
73
- self.depth_test = depth_test
74
72
  self.path = os.path.dirname(__file__)
75
73
 
76
74
  def add_setting(self, layout):
@@ -126,13 +124,6 @@ class CloudItem(BaseItem):
126
124
  self.slider_v.rangeChanged.connect(self._on_range)
127
125
  layout.addWidget(self.slider_v)
128
126
 
129
- self.checkbox_depth_test = QCheckBox(
130
- "Show front points first (Depth Test)")
131
- self.checkbox_depth_test.setChecked(self.depth_test)
132
- self.checkbox_depth_test.stateChanged.connect(self.set_depthtest)
133
- self._on_color_mode(self.color_mode)
134
- layout.addWidget(self.checkbox_depth_test)
135
-
136
127
  def _on_range(self, lower, upper):
137
128
  self.vmin = lower
138
129
  self.vmax = upper
@@ -192,9 +183,6 @@ class CloudItem(BaseItem):
192
183
  self.size = size
193
184
  self.need_update_setting = True
194
185
 
195
- def set_depthtest(self, state):
196
- self.depth_test = state
197
-
198
186
  def clear(self):
199
187
  data = np.empty((0), self.data_type)
200
188
  self.set_data(data)
@@ -304,15 +292,16 @@ class CloudItem(BaseItem):
304
292
  def paint(self):
305
293
  self.update_render_buffer()
306
294
  self.update_setting()
295
+
307
296
  glEnable(GL_BLEND)
308
297
  glEnable(GL_PROGRAM_POINT_SIZE)
309
298
  glEnable(GL_POINT_SPRITE)
310
299
  glEnable(GL_DEPTH_TEST)
311
300
 
312
- if not self.depth_test:
313
- glDepthFunc(GL_ALWAYS) # Always pass depth test but still write depth
301
+ if self.alpha < 0.9:
302
+ glDepthFunc(GL_ALWAYS)
314
303
  else:
315
- glDepthFunc(GL_LESS) # Normal depth testing
304
+ glDepthFunc(GL_LESS)
316
305
 
317
306
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
318
307
  glUseProgram(self.program)
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
5
+ Distributed under MIT license. See LICENSE for more information.
6
+ """
7
+
8
+
9
+ import numpy as np
10
+ from q3dviewer.base_item import BaseItem
11
+ from OpenGL.GL import *
12
+ from OpenGL.GL import shaders
13
+ from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
14
+ import matplotlib.colors as mcolors
15
+ import os
16
+ from q3dviewer.utils import set_uniform, text_to_rgba
17
+
18
+
19
+
20
+ class MeshItem(BaseItem):
21
+ def __init__(self, color='lightblue', wireframe=False, enable_lighting=True):
22
+ super(MeshItem, self).__init__()
23
+ self.color = color
24
+ self.flat_rgb = text_to_rgba(color, flat=True)
25
+ self.wireframe = wireframe
26
+ self.enable_lighting = enable_lighting
27
+
28
+ # Mesh data
29
+ self.triangles = None
30
+ self.normals = None
31
+
32
+ # OpenGL objects
33
+ self.vao = None
34
+ self.vbo_vertices = None
35
+ self.vbo_normals = None
36
+ # self.vbo_colors = None
37
+ self.program = None
38
+
39
+ # Rendering parameters
40
+ self.line_width = 1.0
41
+ self.light_pos = [1.0, 1.0, 1.0]
42
+ self.light_color = [1.0, 1.0, 1.0]
43
+
44
+ # Phong lighting material properties
45
+ self.ambient_strength = 0.1
46
+ self.diffuse_strength = 1.2
47
+ self.specular_strength = 0.1
48
+ self.shininess = 32.0
49
+ # Alpha (opacity)
50
+ self.alpha = 1.0
51
+
52
+ # Buffer initialization flag
53
+ self.need_update_buffer = True
54
+ self.need_update_setting = True
55
+ self.path = os.path.dirname(__file__)
56
+
57
+
58
+ def add_setting(self, layout):
59
+ """Add UI controls for mesh visualization"""
60
+ # Wireframe toggle
61
+ self.wireframe_box = QCheckBox("Wireframe Mode")
62
+ self.wireframe_box.setChecked(self.wireframe)
63
+ self.wireframe_box.toggled.connect(self.update_wireframe)
64
+ layout.addWidget(self.wireframe_box)
65
+
66
+ # Enable lighting toggle
67
+ self.lighting_box = QCheckBox("Enable Lighting")
68
+ self.lighting_box.setChecked(self.enable_lighting)
69
+ self.lighting_box.toggled.connect(self.update_enable_lighting)
70
+ layout.addWidget(self.lighting_box)
71
+
72
+ # Line width control
73
+ line_width_label = QLabel("Line Width:")
74
+ layout.addWidget(line_width_label)
75
+ self.line_width_box = QDoubleSpinBox()
76
+ self.line_width_box.setRange(0.5, 5.0)
77
+ self.line_width_box.setSingleStep(0.5)
78
+ self.line_width_box.setValue(self.line_width)
79
+ self.line_width_box.valueChanged.connect(self.update_line_width)
80
+ layout.addWidget(self.line_width_box)
81
+
82
+ # Alpha control
83
+ alpha_label = QLabel("Alpha:")
84
+ layout.addWidget(alpha_label)
85
+ alpha_box = QDoubleSpinBox()
86
+ alpha_box.setRange(0.0, 1.0)
87
+ alpha_box.setSingleStep(0.05)
88
+ alpha_box.setValue(self.alpha)
89
+ alpha_box.valueChanged.connect(self.update_alpha)
90
+ layout.addWidget(alpha_box)
91
+
92
+
93
+ label_rgb = QLabel("Color:")
94
+ label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
95
+ layout.addWidget(label_rgb)
96
+ self.edit_rgb = QLineEdit()
97
+ self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
98
+ self.edit_rgb.setText(self.color)
99
+ self.edit_rgb.textChanged.connect(self._on_color)
100
+ layout.addWidget(self.edit_rgb)
101
+
102
+
103
+ # Material property controls for Phong lighting
104
+ if self.enable_lighting:
105
+ # Ambient strength control (slider 0-100 mapped to 0.0-1.0)
106
+ ambient_layout = QHBoxLayout()
107
+ ambient_label = QLabel("Ambient Strength:")
108
+ ambient_layout.addWidget(ambient_label)
109
+ self.ambient_slider = QSlider()
110
+ self.ambient_slider.setOrientation(1) # Qt.Horizontal
111
+ self.ambient_slider.setRange(0, 100)
112
+ self.ambient_slider.setValue(int(self.ambient_strength * 100))
113
+ self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
114
+ ambient_layout.addWidget(self.ambient_slider)
115
+ layout.addLayout(ambient_layout)
116
+
117
+ # Diffuse strength control (slider 0-200 mapped to 0.0-2.0)
118
+ diffuse_layout = QHBoxLayout()
119
+ diffuse_label = QLabel("Diffuse Strength:")
120
+ diffuse_layout.addWidget(diffuse_label)
121
+ self.diffuse_slider = QSlider()
122
+ self.diffuse_slider.setOrientation(1)
123
+ self.diffuse_slider.setRange(0, 200)
124
+ self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
125
+ self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
126
+ diffuse_layout.addWidget(self.diffuse_slider)
127
+ layout.addLayout(diffuse_layout)
128
+
129
+ # Specular strength control (slider 0-200 mapped to 0.0-2.0)
130
+ specular_layout = QHBoxLayout()
131
+ specular_label = QLabel("Specular Strength:")
132
+ specular_layout.addWidget(specular_label)
133
+ self.specular_slider = QSlider()
134
+ self.specular_slider.setOrientation(1)
135
+ self.specular_slider.setRange(0, 200)
136
+ self.specular_slider.setValue(int(self.specular_strength * 100))
137
+ self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
138
+ specular_layout.addWidget(self.specular_slider)
139
+ layout.addLayout(specular_layout)
140
+
141
+ # Shininess control (slider 1-256 mapped to 1-256)
142
+ shininess_layout = QHBoxLayout()
143
+ shininess_label = QLabel("Shininess:")
144
+ shininess_layout.addWidget(shininess_label)
145
+ self.shininess_slider = QSlider()
146
+ self.shininess_slider.setOrientation(1)
147
+ self.shininess_slider.setRange(1, 256)
148
+ self.shininess_slider.setValue(int(self.shininess))
149
+ self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
150
+ shininess_layout.addWidget(self.shininess_slider)
151
+ layout.addLayout(shininess_layout)
152
+
153
+ def _on_color(self, color):
154
+ try:
155
+ self.color = color
156
+ self.flat_rgb = text_to_rgba(color, flat=True)
157
+ self.need_update_setting = True
158
+ except ValueError:
159
+ pass
160
+
161
+
162
+ def update_wireframe(self, value):
163
+ self.wireframe = value
164
+
165
+ def update_enable_lighting(self, value):
166
+ self.enable_lighting = value
167
+ self.need_update_setting = True
168
+
169
+ def update_line_width(self, value):
170
+ self.line_width = value
171
+ self.need_update_setting = True
172
+
173
+ def update_ambient_strength(self, value):
174
+ self.ambient_strength = value
175
+ self.need_update_setting = True
176
+
177
+ def update_diffuse_strength(self, value):
178
+ self.diffuse_strength = value
179
+ self.need_update_setting = True
180
+
181
+ def update_specular_strength(self, value):
182
+ self.specular_strength = value
183
+ self.need_update_setting = True
184
+
185
+ def update_shininess(self, value):
186
+ self.shininess = value
187
+ self.need_update_setting = True
188
+
189
+ def update_alpha(self, value):
190
+ """Update mesh alpha (opacity)"""
191
+ self.alpha = float(value)
192
+ self.need_update_setting = True
193
+
194
+ def set_data(self, verts, faces):
195
+ """
196
+ verts: np.ndarray of shape (N, 3)
197
+ faces: np.ndarray of shape (M, 3) with uint32 indices
198
+ """
199
+ verts = np.asarray(verts, dtype=np.float32)
200
+ faces = np.asarray(faces, dtype=np.uint32)
201
+ triangles = verts[faces.flatten()]
202
+
203
+ self.triangles = np.asarray(triangles, dtype=np.float32)
204
+ self.normals = self.calculate_normals()
205
+ self.need_update_buffer = True
206
+
207
+ def calculate_normals(self):
208
+ if self.triangles is None or len(self.triangles) == 0:
209
+ return None
210
+
211
+ # Ensure we have complete triangles
212
+ num_vertices = len(self.triangles)
213
+ num_triangles = num_vertices // 3
214
+ if num_triangles == 0:
215
+ return None
216
+
217
+ # Reshape vertices into triangles (N, 3, 3) where N is number of triangles
218
+ vertices_reshaped = self.triangles[:num_triangles * 3].reshape(-1, 3, 3)
219
+
220
+ v0 = vertices_reshaped[:, 0, :]
221
+ v1 = vertices_reshaped[:, 1, :]
222
+ v2 = vertices_reshaped[:, 2, :]
223
+
224
+ # Calculate edges for all triangles at once
225
+ edge1 = v1 - v0
226
+ edge2 = v2 - v0
227
+
228
+ face_normals = np.cross(edge1, edge2)
229
+
230
+ norms = np.linalg.norm(face_normals, axis=1, keepdims=True)
231
+ norms[norms < 1e-6] = 1.0
232
+ face_normals = face_normals / norms
233
+
234
+ normals_per_vertex = np.repeat(face_normals[:, np.newaxis, :], 3, axis=1)
235
+ normals = normals_per_vertex.reshape(-1, 3)
236
+ return normals.astype(np.float32)
237
+
238
+ def initialize_gl(self):
239
+ """OpenGL initialization"""
240
+ vertex_shader = open(self.path + '/../shaders/mesh_vert.glsl', 'r').read()
241
+ fragment_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
242
+
243
+ program = shaders.compileProgram(
244
+ shaders.compileShader(vertex_shader, GL_VERTEX_SHADER),
245
+ shaders.compileShader(fragment_shader, GL_FRAGMENT_SHADER),
246
+ )
247
+ self.program = program
248
+
249
+ def update_render_buffer(self):
250
+ """Initialize OpenGL buffers"""
251
+ if not self.need_update_buffer:
252
+ return
253
+
254
+ # Generate VAO and VBOs
255
+ if self.vao is None:
256
+ self.vao = glGenVertexArrays(1)
257
+ self.vbo_vertices = glGenBuffers(1)
258
+ self.vbo_normals = glGenBuffers(1)
259
+ # self.vbo_colors = glGenBuffers(1)
260
+
261
+ glBindVertexArray(self.vao)
262
+
263
+ # Vertex buffer
264
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo_vertices)
265
+ glBufferData(GL_ARRAY_BUFFER, self.triangles.nbytes, self.triangles, GL_STATIC_DRAW)
266
+ glEnableVertexAttribArray(0)
267
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
268
+
269
+ # Normal buffer
270
+ if self.normals is not None:
271
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo_normals)
272
+ glBufferData(GL_ARRAY_BUFFER, self.normals.nbytes, self.normals, GL_STATIC_DRAW)
273
+ glEnableVertexAttribArray(1)
274
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, None)
275
+
276
+ # Color buffer
277
+ # if self.flat_rgbs is not None:
278
+ # glBindBuffer(GL_ARRAY_BUFFER, self.vbo_colors)
279
+ # glBufferData(GL_ARRAY_BUFFER, self.flat_rgbs.nbytes, self.flat_rgbs, GL_STATIC_DRAW)
280
+ # glEnableVertexAttribArray(2)
281
+ # glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, None)
282
+ #
283
+ glBindVertexArray(0)
284
+ self.need_update_buffer = False
285
+
286
+ def update_setting(self):
287
+ if (self.need_update_setting is False):
288
+ return
289
+ set_uniform(self.program, int(self.enable_lighting), 'if_light')
290
+ set_uniform(self.program, 1, 'two_sided')
291
+ set_uniform(self.program, np.array(self.light_color), 'light_color')
292
+ set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
293
+ set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
294
+ set_uniform(self.program, float(self.specular_strength), 'specular_strength')
295
+ set_uniform(self.program, float(self.shininess), 'shininess')
296
+ set_uniform(self.program, float(self.alpha), 'alpha')
297
+ set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
298
+ self.need_update_setting = False
299
+
300
+ def paint(self):
301
+ """Render the mesh using modern OpenGL with shaders"""
302
+ if self.triangles is None or len(self.triangles) == 0:
303
+ return
304
+ glUseProgram(self.program)
305
+ self.update_render_buffer()
306
+ self.update_setting()
307
+ view_matrix = self.glwidget().view_matrix
308
+ set_uniform(self.program, view_matrix, 'view')
309
+ project_matrix = self.glwidget().projection_matrix
310
+ set_uniform(self.program, project_matrix, 'projection')
311
+ view_pos = self.glwidget().center
312
+ set_uniform(self.program, np.array(view_pos), 'view_pos')
313
+
314
+
315
+ # Enable blending and depth testing
316
+ glEnable(GL_BLEND)
317
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
318
+ glEnable(GL_DEPTH_TEST)
319
+ glDisable(GL_CULL_FACE) # two-sided rendering
320
+
321
+ # Set line width
322
+ glLineWidth(self.line_width)
323
+
324
+ # Bind VAO and render
325
+ glBindVertexArray(self.vao)
326
+
327
+ if len(self.triangles) > 0:
328
+ # Render faces
329
+ if self.wireframe:
330
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
331
+ else:
332
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
333
+
334
+ # Draw triangles
335
+ glDrawArrays(GL_TRIANGLES, 0, len(self.triangles))
336
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
337
+
338
+ glBindVertexArray(0)
339
+ glDisable(GL_DEPTH_TEST)
340
+ glDisable(GL_BLEND)
341
+ glUseProgram(0)
342
+
@@ -49,13 +49,21 @@ class FileLoaderThread(QThread):
49
49
 
50
50
  def run(self):
51
51
  cloud_item = self.viewer['cloud']
52
+ mesh_item = self.viewer['mesh']
52
53
  for i, url in enumerate(self.files):
54
+ # if the file is a mesh file, use mesh_item to load
55
+ file_path = url.toLocalFile()
53
56
  file_path = url.toLocalFile()
54
57
  self.viewer.progress_dialog.set_file_name(file_path)
55
- cloud = cloud_item.load(file_path, append=(i > 0))
56
- center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
57
- self.viewer.glwidget.set_cam_position(center=center)
58
- self.progress.emit(int((i + 1) / len(self.files) * 100))
58
+ if url.toLocalFile().lower().endswith(('.stl')):
59
+ from q3dviewer.utils.cloud_io import load_stl
60
+ verts, faces = load_stl(file_path)
61
+ mesh_item.set_data(verts=verts, faces=faces)
62
+ else:
63
+ cloud = cloud_item.load(file_path, append=(i > 0))
64
+ center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
65
+ self.viewer.glwidget.set_cam_position(center=center)
66
+ self.progress.emit(int((i + 1) / len(self.files) * 100))
59
67
  self.finished.emit()
60
68
 
61
69
 
@@ -157,6 +165,8 @@ def print_help():
157
165
  help_msg = f"""
158
166
  {BOLD}Cloud Viewer Help:{END}
159
167
  {GREEN}• Drag and drop cloud files into the viewer to load them.{END}
168
+ {BLUE}- support .pcd, .ply, .las, .e57, for point clouds.{END}
169
+ {BLUE}- support .stl for mesh files.{END}
160
170
  {GREEN}• Measure distance between points:{END}
161
171
  {BLUE}- Hold Ctrl and left-click to select points on the cloud.{END}
162
172
  {BLUE}- Hold Ctrl and right-click to remove the last selected point.{END}
@@ -179,13 +189,15 @@ def main():
179
189
  grid_item = q3d.GridItem(size=1000, spacing=20)
180
190
  marker_item = q3d.Text3DItem() # Changed from CloudItem to Text3DItem
181
191
  text_item = q3d.Text2DItem(pos=(20, 40), text="", color='lime', size=16)
192
+ mesh_item = q3d.MeshItem() # Added MeshIOItem for mesh support
182
193
 
183
194
  viewer.add_items(
184
195
  {'marker': marker_item,
185
196
  'cloud': cloud_item,
186
197
  'grid': grid_item,
187
198
  'axis': axis_item,
188
- 'text': text_item})
199
+ 'text': text_item,
200
+ 'mesh': mesh_item})
189
201
 
190
202
  if args.path:
191
203
  pcd_fn = args.path
@@ -6,6 +6,27 @@ Distributed under MIT license. See LICENSE for more information.
6
6
  import numpy as np
7
7
 
8
8
 
9
+ def load_stl(file_path):
10
+ from stl import mesh as stlmesh
11
+ m = stlmesh.Mesh.from_file(file_path)
12
+ verts = m.vectors.reshape(-1, 3).astype(np.float32)
13
+ faces = np.arange(len(verts), dtype=np.uint32).reshape(-1, 3)
14
+ return verts, faces
15
+
16
+
17
+ def save_stl(verts, faces, save_path):
18
+ """Save the generated mesh as an STL file."""
19
+ from stl import mesh as stlmesh
20
+ from stl import Mode
21
+ verts = np.asarray(verts, dtype=np.float32)
22
+ faces = np.asarray(faces, dtype=np.uint32)
23
+ # Create the mesh
24
+ m = stlmesh.Mesh(np.zeros(faces.shape[0], dtype=stlmesh.Mesh.dtype))
25
+ m.vectors[:] = verts[faces].astype(np.float32)
26
+ # Save to file
27
+ m.save(save_path, mode=Mode.BINARY)
28
+
29
+
9
30
  def save_ply(cloud, save_path):
10
31
  import meshio
11
32
  xyz = cloud['xyz']
@@ -37,19 +37,21 @@ class RangeSlider(QSlider):
37
37
  self.active_handle = "upper"
38
38
 
39
39
  def mouseMoveEvent(self, event):
40
- """Override to update handle positions."""
40
+ """Override to update handle positions, always clamp and int for cross-platform safety."""
41
41
  if event.buttons() != Qt.LeftButton:
42
42
  return
43
43
 
44
44
  pos = self.pixelPosToValue(event.pos())
45
+ minv, maxv = self.minimum(), self.maximum()
45
46
  if self.active_handle == "lower":
46
- self.lower_value = max(
47
- self.minimum(), min(pos, self.upper_value - 1))
47
+ self.lower_value = max(minv, min(pos, self.upper_value - 1))
48
+ self.lower_value = int(round(self.lower_value))
48
49
  QToolTip.showText(event.globalPos(), f"Lower: {self.lower_value:.1f}")
49
50
  elif self.active_handle == "upper":
50
- self.upper_value = min(
51
- self.maximum(), max(pos, self.lower_value + 1))
51
+ self.upper_value = min(maxv, max(pos, self.lower_value + 1))
52
+ self.upper_value = int(round(self.upper_value))
52
53
  QToolTip.showText(event.globalPos(), f"Upper: {self.upper_value:.1f}")
54
+ # Always emit clamped, int values
53
55
  self.rangeChanged.emit(self.lower_value, self.upper_value)
54
56
  self.update()
55
57
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: q3dviewer
3
- Version: 1.1.9
3
+ Version: 1.2.1
4
4
  Summary: A library designed for quickly deploying a 3D viewer.
5
5
  Home-page: https://github.com/scomup/q3dviewer
6
6
  Author: Liu Yang
@@ -21,6 +21,7 @@ q3dviewer/custom_items/gaussian_item.py
21
21
  q3dviewer/custom_items/grid_item.py
22
22
  q3dviewer/custom_items/image_item.py
23
23
  q3dviewer/custom_items/line_item.py
24
+ q3dviewer/custom_items/mesh_item.py
24
25
  q3dviewer/custom_items/text3d_item.py
25
26
  q3dviewer/custom_items/text_item.py
26
27
  q3dviewer/tools/__init__.py
@@ -5,6 +5,7 @@ laspy
5
5
  matplotlib
6
6
  meshio
7
7
  numpy
8
+ numpy-stl
8
9
  pye57
9
10
  pypcd4
10
11
  pyside6
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='q3dviewer',
5
- version='1.1.9',
5
+ version='1.2.1',
6
6
  author="Liu Yang",
7
7
  description="A library designed for quickly deploying a 3D viewer.",
8
8
  long_description=open("README.md").read(),
@@ -29,6 +29,7 @@ setup(
29
29
  'imageio',
30
30
  'imageio[ffmpeg]',
31
31
  'matplotlib',
32
+ 'numpy-stl',
32
33
  ],
33
34
  entry_points={
34
35
  'console_scripts': [
File without changes
File without changes
File without changes