q3dviewer 1.2.0__tar.gz → 1.2.2__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.2.0 → q3dviewer-1.2.2}/PKG-INFO +1 -1
  2. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/__init__.py +1 -0
  3. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/cloud_item.py +1 -2
  4. q3dviewer-1.2.2/q3dviewer/custom_items/mesh_item.py +425 -0
  5. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/tools/cloud_viewer.py +25 -6
  6. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/utils/cloud_io.py +21 -0
  7. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer.egg-info/PKG-INFO +1 -1
  8. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer.egg-info/SOURCES.txt +1 -0
  9. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer.egg-info/requires.txt +1 -0
  10. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/setup.py +2 -1
  11. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/README.md +0 -0
  12. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/Qt/__init__.py +0 -0
  13. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/__init__.py +0 -0
  14. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/base_glwidget.py +0 -0
  15. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/base_item.py +0 -0
  16. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/axis_item.py +0 -0
  17. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/cloud_io_item.py +0 -0
  18. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/frame_item.py +0 -0
  19. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/gaussian_item.py +0 -0
  20. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/grid_item.py +0 -0
  21. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/image_item.py +0 -0
  22. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/line_item.py +0 -0
  23. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/text3d_item.py +0 -0
  24. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/custom_items/text_item.py +0 -0
  25. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/glwidget.py +0 -0
  26. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/tools/__init__.py +0 -0
  27. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/tools/example_viewer.py +0 -0
  28. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/tools/film_maker.py +0 -0
  29. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/tools/gaussian_viewer.py +0 -0
  30. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/tools/lidar_calib.py +0 -0
  31. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/tools/lidar_cam_calib.py +0 -0
  32. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/tools/ros_viewer.py +0 -0
  33. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/utils/__init__.py +0 -0
  34. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/utils/convert_ros_msg.py +0 -0
  35. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/utils/gl_helper.py +0 -0
  36. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/utils/helpers.py +0 -0
  37. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/utils/maths.py +0 -0
  38. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/utils/range_slider.py +0 -0
  39. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer/viewer.py +0 -0
  40. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer.egg-info/dependency_links.txt +0 -0
  41. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer.egg-info/entry_points.txt +0 -0
  42. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/q3dviewer.egg-info/top_level.txt +0 -0
  43. {q3dviewer-1.2.0 → q3dviewer-1.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: q3dviewer
3
- Version: 1.2.0
3
+ Version: 1.2.2
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
@@ -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
@@ -0,0 +1,425 @@
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, QComboBox
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
+ """
22
+ An OpenGL mesh item for rendering triangulated 3D surfaces.
23
+
24
+ Attributes:
25
+ color (str or tuple): The flat color to use when `color_mode` is 'FLAT'.
26
+ Accepts any valid matplotlib color (e.g., 'lightblue', 'red', '#FF4500', (1.0, 0.5, 0.0)).
27
+ wireframe (bool): If True, render the mesh in wireframe mode (edges only).
28
+ If False, render filled triangles. Default is False.
29
+ enable_lighting (bool): Whether to enable Phong lighting for the mesh.
30
+ If True, the mesh will be shaded based on light direction and material properties.
31
+ If False, the mesh will use flat shading with object colors only. Default is True.
32
+ color_mode (str): The coloring mode for mesh vertices.
33
+ - 'FLAT': Single flat color for all vertices (uses the `color` attribute).
34
+ - 'I': Color by intensity channel from per-vertex colors (rainbow gradient).
35
+ - 'RGB': Per-vertex RGB color from per-vertex color data.
36
+ alpha (float): The transparency of the mesh, in the range [0, 1],
37
+ where 0 is fully transparent and 1 is fully opaque. Default is 1.0.
38
+ line_width (float): The width of lines when rendering in wireframe mode.
39
+ Range is typically 0.5 to 5.0. Default is 1.0.
40
+
41
+ Material Properties (Phong Lighting):
42
+ ambient_strength (float): Ambient light contribution [0.0-1.0]. Default is 0.1.
43
+ diffuse_strength (float): Diffuse light contribution [0.0-2.0]. Default is 1.2.
44
+ specular_strength (float): Specular highlight contribution [0.0-2.0]. Default is 0.1.
45
+ shininess (float): Specular shininess exponent [1-256]. Higher values = smaller highlights. Default is 32.0.
46
+
47
+ Methods:
48
+ set_data(verts, faces, colors=None): Set mesh geometry and optional per-vertex colors.
49
+ - verts: np.ndarray of shape (N, 3) - vertex positions
50
+ - faces: np.ndarray of shape (M, 3) with uint32 indices - triangle indices
51
+ - colors: np.ndarray of shape (N,) with uint32 IRGB format (optional)
52
+ uint32 format: I (bits 24-31), R (bits 16-23), G (bits 8-15), B (bits 0-7)
53
+
54
+ Example:
55
+ # Create a simple triangle mesh with per-vertex colors
56
+ verts = np.array([[0,0,0], [1,0,0], [0,1,0]], dtype=np.float32)
57
+ faces = np.array([[0,1,2]], dtype=np.uint32)
58
+ colors = np.array([
59
+ (255 << 24) | (255 << 16) | (0 << 8) | 0, # Red, intensity=255
60
+ (200 << 24) | (0 << 16) | (255 << 8) | 0, # Green, intensity=200
61
+ (150 << 24) | (0 << 16) | (0 << 8) | 255 # Blue, intensity=150
62
+ ], dtype=np.uint32)
63
+
64
+ mesh = q3d.MeshItem(color='lightblue', color_mode='RGB', enable_lighting=True)
65
+ mesh.set_data(verts, faces, colors)
66
+ """
67
+ def __init__(self, color='lightblue', wireframe=False, enable_lighting=True, color_mode='FLAT'):
68
+ super(MeshItem, self).__init__()
69
+ self.color = color
70
+ self.flat_rgb = text_to_rgba(color, flat=True)
71
+ self.wireframe = wireframe
72
+ self.enable_lighting = enable_lighting
73
+
74
+ # Mesh data
75
+ self.triangles = None
76
+ self.normals = None
77
+ self.vertex_colors = None # Per-vertex colors (uint32 IRGB format)
78
+
79
+ self.mode_table = {'FLAT': 0, 'I': 1, 'RGB': 2}
80
+ self.color_mode = self.mode_table[color_mode]
81
+ self.vmin = 0
82
+ self.vmax = 255
83
+
84
+ # OpenGL objects
85
+ self.vao = None
86
+ self.vbo_vertices = None
87
+ self.vbo_normals = None
88
+ self.vbo_colors = None
89
+ self.program = None
90
+
91
+ # Rendering parameters
92
+ self.line_width = 1.0
93
+ self.light_pos = [1.0, 1.0, 1.0]
94
+ self.light_color = [1.0, 1.0, 1.0]
95
+
96
+ # Phong lighting material properties
97
+ self.ambient_strength = 0.1
98
+ self.diffuse_strength = 1.2
99
+ self.specular_strength = 0.1
100
+ self.shininess = 32.0
101
+ # Alpha (opacity)
102
+ self.alpha = 1.0
103
+
104
+ # Buffer initialization flag
105
+ self.need_update_buffer = True
106
+ self.need_update_setting = True
107
+ self.path = os.path.dirname(__file__)
108
+
109
+
110
+ def add_setting(self, layout):
111
+ """Add UI controls for mesh visualization"""
112
+ # Wireframe toggle
113
+ self.wireframe_box = QCheckBox("Wireframe Mode")
114
+ self.wireframe_box.setChecked(self.wireframe)
115
+ self.wireframe_box.toggled.connect(self.update_wireframe)
116
+ layout.addWidget(self.wireframe_box)
117
+
118
+ # Enable lighting toggle
119
+ self.lighting_box = QCheckBox("Enable Lighting")
120
+ self.lighting_box.setChecked(self.enable_lighting)
121
+ self.lighting_box.toggled.connect(self.update_enable_lighting)
122
+ layout.addWidget(self.lighting_box)
123
+
124
+ # Line width control
125
+ line_width_label = QLabel("Line Width:")
126
+ layout.addWidget(line_width_label)
127
+ self.line_width_box = QDoubleSpinBox()
128
+ self.line_width_box.setRange(0.5, 5.0)
129
+ self.line_width_box.setSingleStep(0.5)
130
+ self.line_width_box.setValue(self.line_width)
131
+ self.line_width_box.valueChanged.connect(self.update_line_width)
132
+ layout.addWidget(self.line_width_box)
133
+
134
+ # Alpha control
135
+ alpha_label = QLabel("Alpha:")
136
+ layout.addWidget(alpha_label)
137
+ alpha_box = QDoubleSpinBox()
138
+ alpha_box.setRange(0.0, 1.0)
139
+ alpha_box.setSingleStep(0.05)
140
+ alpha_box.setValue(self.alpha)
141
+ alpha_box.valueChanged.connect(self.update_alpha)
142
+ layout.addWidget(alpha_box)
143
+
144
+
145
+ # Color mode selection
146
+ label_color = QLabel("Color Mode:")
147
+ layout.addWidget(label_color)
148
+ self.combo_color = QComboBox()
149
+ self.combo_color.addItem("flat color")
150
+ self.combo_color.addItem("intensity")
151
+ self.combo_color.addItem("RGB")
152
+ self.combo_color.setCurrentIndex(self.color_mode)
153
+ self.combo_color.currentIndexChanged.connect(self._on_color_mode)
154
+ layout.addWidget(self.combo_color)
155
+
156
+ label_rgb = QLabel("Color:")
157
+ label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
158
+ layout.addWidget(label_rgb)
159
+ self.edit_rgb = QLineEdit()
160
+ self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
161
+ self.edit_rgb.setText(self.color)
162
+ self.edit_rgb.textChanged.connect(self._on_color)
163
+ layout.addWidget(self.edit_rgb)
164
+
165
+
166
+ # Material property controls for Phong lighting
167
+ if self.enable_lighting:
168
+ # Ambient strength control (slider 0-100 mapped to 0.0-1.0)
169
+ ambient_layout = QHBoxLayout()
170
+ ambient_label = QLabel("Ambient Strength:")
171
+ ambient_layout.addWidget(ambient_label)
172
+ self.ambient_slider = QSlider()
173
+ self.ambient_slider.setOrientation(1) # Qt.Horizontal
174
+ self.ambient_slider.setRange(0, 100)
175
+ self.ambient_slider.setValue(int(self.ambient_strength * 100))
176
+ self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
177
+ ambient_layout.addWidget(self.ambient_slider)
178
+ layout.addLayout(ambient_layout)
179
+
180
+ # Diffuse strength control (slider 0-200 mapped to 0.0-2.0)
181
+ diffuse_layout = QHBoxLayout()
182
+ diffuse_label = QLabel("Diffuse Strength:")
183
+ diffuse_layout.addWidget(diffuse_label)
184
+ self.diffuse_slider = QSlider()
185
+ self.diffuse_slider.setOrientation(1)
186
+ self.diffuse_slider.setRange(0, 200)
187
+ self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
188
+ self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
189
+ diffuse_layout.addWidget(self.diffuse_slider)
190
+ layout.addLayout(diffuse_layout)
191
+
192
+ # Specular strength control (slider 0-200 mapped to 0.0-2.0)
193
+ specular_layout = QHBoxLayout()
194
+ specular_label = QLabel("Specular Strength:")
195
+ specular_layout.addWidget(specular_label)
196
+ self.specular_slider = QSlider()
197
+ self.specular_slider.setOrientation(1)
198
+ self.specular_slider.setRange(0, 200)
199
+ self.specular_slider.setValue(int(self.specular_strength * 100))
200
+ self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
201
+ specular_layout.addWidget(self.specular_slider)
202
+ layout.addLayout(specular_layout)
203
+
204
+ # Shininess control (slider 1-256 mapped to 1-256)
205
+ shininess_layout = QHBoxLayout()
206
+ shininess_label = QLabel("Shininess:")
207
+ shininess_layout.addWidget(shininess_label)
208
+ self.shininess_slider = QSlider()
209
+ self.shininess_slider.setOrientation(1)
210
+ self.shininess_slider.setRange(1, 256)
211
+ self.shininess_slider.setValue(int(self.shininess))
212
+ self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
213
+ shininess_layout.addWidget(self.shininess_slider)
214
+ layout.addLayout(shininess_layout)
215
+
216
+ def _on_color(self, color):
217
+ try:
218
+ self.color = color
219
+ self.flat_rgb = text_to_rgba(color, flat=True)
220
+ self.need_update_setting = True
221
+ except ValueError:
222
+ pass
223
+
224
+ def _on_color_mode(self, index):
225
+ print(f"Color mode1 : {index}")
226
+ self.color_mode = index
227
+ self.edit_rgb.setVisible(index == self.mode_table['FLAT'])
228
+ self.need_update_setting = True
229
+
230
+ def update_wireframe(self, value):
231
+ self.wireframe = value
232
+
233
+ def update_enable_lighting(self, value):
234
+ self.enable_lighting = value
235
+ self.need_update_setting = True
236
+
237
+ def update_line_width(self, value):
238
+ self.line_width = value
239
+ self.need_update_setting = True
240
+
241
+ def update_ambient_strength(self, value):
242
+ self.ambient_strength = value
243
+ self.need_update_setting = True
244
+
245
+ def update_diffuse_strength(self, value):
246
+ self.diffuse_strength = value
247
+ self.need_update_setting = True
248
+
249
+ def update_specular_strength(self, value):
250
+ self.specular_strength = value
251
+ self.need_update_setting = True
252
+
253
+ def update_shininess(self, value):
254
+ self.shininess = value
255
+ self.need_update_setting = True
256
+
257
+ def update_alpha(self, value):
258
+ """Update mesh alpha (opacity)"""
259
+ self.alpha = float(value)
260
+ self.need_update_setting = True
261
+
262
+ def set_data(self, verts, faces, colors=None):
263
+ """
264
+ verts: np.ndarray of shape (N, 3)
265
+ faces: np.ndarray of shape (M, 3) with uint32 indices
266
+ colors: np.ndarray of shape (N,) with uint32 IRGB format (optional)
267
+ uint32 contains: I (bits 24-31), R (bits 16-23), G (bits 8-15), B (bits 0-7)
268
+ """
269
+ verts = np.asarray(verts, dtype=np.float32)
270
+ faces = np.asarray(faces, dtype=np.uint32)
271
+ triangles = verts[faces.flatten()]
272
+
273
+ if colors is not None:
274
+ colors = np.asarray(colors, dtype=np.uint32)
275
+ if len(colors) == len(verts):
276
+ # Expand per-vertex colors to per-triangle-vertex
277
+ self.vertex_colors = colors[faces.flatten()]
278
+ else:
279
+ self.vertex_colors = None
280
+ else:
281
+ self.vertex_colors = None
282
+
283
+ self.triangles = np.asarray(triangles, dtype=np.float32)
284
+ self.normals = self.calculate_normals()
285
+ self.need_update_buffer = True
286
+
287
+ def calculate_normals(self):
288
+ if self.triangles is None or len(self.triangles) == 0:
289
+ return None
290
+
291
+ # Ensure we have complete triangles
292
+ num_vertices = len(self.triangles)
293
+ num_triangles = num_vertices // 3
294
+ if num_triangles == 0:
295
+ return None
296
+
297
+ # Reshape vertices into triangles (N, 3, 3) where N is number of triangles
298
+ vertices_reshaped = self.triangles[:num_triangles * 3].reshape(-1, 3, 3)
299
+
300
+ v0 = vertices_reshaped[:, 0, :]
301
+ v1 = vertices_reshaped[:, 1, :]
302
+ v2 = vertices_reshaped[:, 2, :]
303
+
304
+ # Calculate edges for all triangles at once
305
+ edge1 = v1 - v0
306
+ edge2 = v2 - v0
307
+
308
+ face_normals = np.cross(edge1, edge2)
309
+
310
+ norms = np.linalg.norm(face_normals, axis=1, keepdims=True)
311
+ norms[norms < 1e-6] = 1.0
312
+ face_normals = face_normals / norms
313
+
314
+ normals_per_vertex = np.repeat(face_normals[:, np.newaxis, :], 3, axis=1)
315
+ normals = normals_per_vertex.reshape(-1, 3)
316
+ return normals.astype(np.float32)
317
+
318
+ def initialize_gl(self):
319
+ """OpenGL initialization"""
320
+ vertex_shader = open(self.path + '/../shaders/mesh_vert.glsl', 'r').read()
321
+ fragment_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
322
+
323
+ program = shaders.compileProgram(
324
+ shaders.compileShader(vertex_shader, GL_VERTEX_SHADER),
325
+ shaders.compileShader(fragment_shader, GL_FRAGMENT_SHADER),
326
+ )
327
+ self.program = program
328
+
329
+ def update_render_buffer(self):
330
+ """Initialize OpenGL buffers"""
331
+ if not self.need_update_buffer:
332
+ return
333
+
334
+ # Generate VAO and VBOs
335
+ if self.vao is None:
336
+ self.vao = glGenVertexArrays(1)
337
+ self.vbo_vertices = glGenBuffers(1)
338
+ self.vbo_normals = glGenBuffers(1)
339
+ self.vbo_colors = glGenBuffers(1)
340
+
341
+ glBindVertexArray(self.vao)
342
+
343
+ # Vertex buffer
344
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo_vertices)
345
+ glBufferData(GL_ARRAY_BUFFER, self.triangles.nbytes, self.triangles, GL_STATIC_DRAW)
346
+ glEnableVertexAttribArray(0)
347
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
348
+
349
+ # Normal buffer
350
+ if self.normals is not None:
351
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo_normals)
352
+ glBufferData(GL_ARRAY_BUFFER, self.normals.nbytes, self.normals, GL_STATIC_DRAW)
353
+ glEnableVertexAttribArray(1)
354
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, None)
355
+
356
+ # Color buffer (uint32 IRGB format)
357
+ if self.vertex_colors is not None:
358
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo_colors)
359
+ glBufferData(GL_ARRAY_BUFFER, self.vertex_colors.nbytes, self.vertex_colors, GL_STATIC_DRAW)
360
+ glEnableVertexAttribArray(2)
361
+ glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, 0, None)
362
+
363
+ glBindVertexArray(0)
364
+ self.need_update_buffer = False
365
+
366
+ def update_setting(self):
367
+ if (self.need_update_setting is False):
368
+ return
369
+ set_uniform(self.program, int(self.enable_lighting), 'if_light')
370
+ set_uniform(self.program, 1, 'two_sided')
371
+ set_uniform(self.program, np.array(self.light_color), 'light_color')
372
+ set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
373
+ set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
374
+ set_uniform(self.program, float(self.specular_strength), 'specular_strength')
375
+ set_uniform(self.program, float(self.shininess), 'shininess')
376
+ set_uniform(self.program, float(self.alpha), 'alpha')
377
+ set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
378
+ set_uniform(self.program, int(self.color_mode), 'color_mode')
379
+ set_uniform(self.program, float(self.vmin), 'vmin')
380
+ set_uniform(self.program, float(self.vmax), 'vmax')
381
+ self.need_update_setting = False
382
+
383
+ def paint(self):
384
+ """Render the mesh using modern OpenGL with shaders"""
385
+ if self.triangles is None or len(self.triangles) == 0:
386
+ return
387
+ glUseProgram(self.program)
388
+ self.update_render_buffer()
389
+ self.update_setting()
390
+ view_matrix = self.glwidget().view_matrix
391
+ set_uniform(self.program, view_matrix, 'view')
392
+ project_matrix = self.glwidget().projection_matrix
393
+ set_uniform(self.program, project_matrix, 'projection')
394
+ view_pos = self.glwidget().center
395
+ set_uniform(self.program, np.array(view_pos), 'view_pos')
396
+
397
+
398
+ # Enable blending and depth testing
399
+ glEnable(GL_BLEND)
400
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
401
+ glEnable(GL_DEPTH_TEST)
402
+ glDisable(GL_CULL_FACE) # two-sided rendering
403
+
404
+ # Set line width
405
+ glLineWidth(self.line_width)
406
+
407
+ # Bind VAO and render
408
+ glBindVertexArray(self.vao)
409
+
410
+ if len(self.triangles) > 0:
411
+ # Render faces
412
+ if self.wireframe:
413
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
414
+ else:
415
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
416
+
417
+ # Draw triangles
418
+ glDrawArrays(GL_TRIANGLES, 0, len(self.triangles))
419
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
420
+
421
+ glBindVertexArray(0)
422
+ glDisable(GL_DEPTH_TEST)
423
+ glDisable(GL_BLEND)
424
+ glUseProgram(0)
425
+
@@ -49,13 +49,26 @@ 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
+ # create colors, N, array
62
+ color = np.zeros((verts.shape[0],), dtype=np.uint32)
63
+ # random uint32 colors in IRGB format
64
+ color = np.random.randint(0, 0xFFFFFFFF, size=(verts.shape[0],), dtype=np.uint32)
65
+ mesh_item.set_data(verts=verts, faces=faces, colors=color)
66
+ break
67
+ else:
68
+ cloud = cloud_item.load(file_path, append=(i > 0))
69
+ center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
70
+ self.viewer.glwidget.set_cam_position(center=center)
71
+ self.progress.emit(int((i + 1) / len(self.files) * 100))
59
72
  self.finished.emit()
60
73
 
61
74
 
@@ -157,6 +170,8 @@ def print_help():
157
170
  help_msg = f"""
158
171
  {BOLD}Cloud Viewer Help:{END}
159
172
  {GREEN}• Drag and drop cloud files into the viewer to load them.{END}
173
+ {BLUE}- support .pcd, .ply, .las, .e57, for point clouds.{END}
174
+ {BLUE}- support .stl for mesh files.{END}
160
175
  {GREEN}• Measure distance between points:{END}
161
176
  {BLUE}- Hold Ctrl and left-click to select points on the cloud.{END}
162
177
  {BLUE}- Hold Ctrl and right-click to remove the last selected point.{END}
@@ -176,16 +191,20 @@ def main():
176
191
  viewer = CloudViewer(name='Cloud Viewer')
177
192
  cloud_item = q3d.CloudIOItem(size=1, alpha=0.1)
178
193
  axis_item = q3d.AxisItem(size=0.5, width=5)
194
+ axis_item.disable_setting()
179
195
  grid_item = q3d.GridItem(size=1000, spacing=20)
180
196
  marker_item = q3d.Text3DItem() # Changed from CloudItem to Text3DItem
181
197
  text_item = q3d.Text2DItem(pos=(20, 40), text="", color='lime', size=16)
198
+ text_item.disable_setting()
199
+ mesh_item = q3d.MeshItem() # Added MeshIOItem for mesh support
182
200
 
183
201
  viewer.add_items(
184
202
  {'marker': marker_item,
185
- 'cloud': cloud_item,
203
+ 'cloud': cloud_item,
204
+ 'mesh': mesh_item,
186
205
  'grid': grid_item,
187
206
  'axis': axis_item,
188
- 'text': text_item})
207
+ 'text': text_item,})
189
208
 
190
209
  if args.path:
191
210
  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']
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: q3dviewer
3
- Version: 1.2.0
3
+ Version: 1.2.2
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.2.0',
5
+ version='1.2.2',
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