q3dviewer 1.2.3__tar.gz → 1.2.4__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 (45) hide show
  1. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/PKG-INFO +24 -11
  2. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/README.md +21 -9
  3. q3dviewer-1.2.4/q3dviewer/__init__.py +5 -0
  4. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/__init__.py +2 -0
  5. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/mesh_item.py +118 -73
  6. q3dviewer-1.2.4/q3dviewer/custom_items/static_mesh_item.py +330 -0
  7. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/tools/cloud_viewer.py +3 -2
  8. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/tools/film_maker.py +7 -8
  9. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/utils/helpers.py +12 -0
  10. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer.egg-info/PKG-INFO +24 -11
  11. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer.egg-info/SOURCES.txt +1 -0
  12. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/setup.py +3 -1
  13. q3dviewer-1.2.3/q3dviewer/__init__.py +0 -14
  14. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/Qt/__init__.py +0 -0
  15. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/base_glwidget.py +0 -0
  16. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/base_item.py +0 -0
  17. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/axis_item.py +0 -0
  18. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/cloud_io_item.py +0 -0
  19. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/cloud_item.py +0 -0
  20. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/frame_item.py +0 -0
  21. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/gaussian_item.py +0 -0
  22. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/grid_item.py +0 -0
  23. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/image_item.py +0 -0
  24. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/line_item.py +0 -0
  25. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/text3d_item.py +0 -0
  26. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/custom_items/text_item.py +0 -0
  27. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/glwidget.py +0 -0
  28. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/tools/__init__.py +0 -0
  29. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/tools/example_viewer.py +0 -0
  30. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/tools/gaussian_viewer.py +0 -0
  31. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/tools/lidar_calib.py +0 -0
  32. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/tools/lidar_cam_calib.py +0 -0
  33. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/tools/ros_viewer.py +0 -0
  34. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/utils/__init__.py +0 -0
  35. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/utils/cloud_io.py +0 -0
  36. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/utils/convert_ros_msg.py +0 -0
  37. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/utils/gl_helper.py +0 -0
  38. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/utils/maths.py +0 -0
  39. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/utils/range_slider.py +0 -0
  40. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer/viewer.py +0 -0
  41. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer.egg-info/dependency_links.txt +0 -0
  42. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer.egg-info/entry_points.txt +0 -0
  43. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer.egg-info/requires.txt +0 -0
  44. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/q3dviewer.egg-info/top_level.txt +0 -0
  45. {q3dviewer-1.2.3 → q3dviewer-1.2.4}/setup.cfg +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: q3dviewer
3
- Version: 1.2.3
3
+ Version: 1.2.4
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
7
- License: UNKNOWN
7
+ Author-email: liu.yang@jp.panasonic.com
8
+ License: MIT
8
9
  Description:
9
10
  ![q3dviewer Logo](imgs/logo.png)
10
11
 
@@ -58,22 +59,34 @@ Description:
58
59
  ```
59
60
 
60
61
  **Basic Operations**
61
- * Load files: Drag and drop point cloud files onto the window (multiple files are OK).
62
- * `M` key: Display the visualization settings screen for point clouds, background color, etc.
63
- * `Left mouse button` & `W, A, S, D` keys: Move the viewpoint on the horizontal plane.
64
- * `Z, X` keys: Move in the direction the screen is facing.
65
- * `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the screen center unchanged.
66
- * `Shift` + `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the camera position unchanged.
62
+
63
+ 📁 **Load Files** - Drag and drop files into the viewer
64
+ * Point clouds: .pcd, .ply, .las, .e57
65
+ * Mesh files: .stl
66
+
67
+ 📏 **Measure Distance** - Interactive point measurement
68
+ * `Ctrl + Left Click`: Add measurement point
69
+ * `Ctrl + Right Click`: Remove last point
70
+ * Total distance displayed automatically
71
+
72
+ 🎥 **Camera Controls** - Navigate the 3D scene
73
+ * `Double Click`: Set camera center to point
74
+ * `Right Drag`: Rotate view
75
+ * `Left Drag`: Pan view
76
+ * `Mouse Wheel`: Zoom in/out
77
+
78
+ ⚙️ **Settings** - Press `M` to open settings window
79
+ * Adjust visualization properties
67
80
 
68
81
  For example, you can download and view point clouds of Tokyo in LAS format from the following link:
69
82
 
70
83
  [Tokyo Point Clouds](https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024/resource/7807d6d1-29f3-4b36-b0c8-f7aa0ea2cff3)
71
84
 
72
- ![Cloud Viewer Screenshot](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/03c981c6-1aec-e5b9-4536-e07e1e56ff29.png)
85
+ ![Cloud Viewer Screenshot](imgs/tokyo.png)
73
86
 
74
- Press `M` on your keyboard to display a menu on the screen, where you can modify visualization settings for each item. For example, you can adjust various settings such as shape, size, color, and transparency for `CloudItem`.
87
+ **Mesh Support** - Starting from version 1.2.4, mesh files (.stl) are now supported.
75
88
 
76
- ![Cloud Viewer Settings](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/deeb996a-e419-58f4-6bc2-535099b1b73a.png)
89
+ ![Screenshot from 2026-02-04 18-32-04.png](imgs/mesh.png)
77
90
 
78
91
  ### 2. ROS Viewer
79
92
 
@@ -51,22 +51,34 @@ python3 -m q3dviewer.tools.cloud_viewer
51
51
  ```
52
52
 
53
53
  **Basic Operations**
54
- * Load files: Drag and drop point cloud files onto the window (multiple files are OK).
55
- * `M` key: Display the visualization settings screen for point clouds, background color, etc.
56
- * `Left mouse button` & `W, A, S, D` keys: Move the viewpoint on the horizontal plane.
57
- * `Z, X` keys: Move in the direction the screen is facing.
58
- * `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the screen center unchanged.
59
- * `Shift` + `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the camera position unchanged.
54
+
55
+ 📁 **Load Files** - Drag and drop files into the viewer
56
+ * Point clouds: .pcd, .ply, .las, .e57
57
+ * Mesh files: .stl
58
+
59
+ 📏 **Measure Distance** - Interactive point measurement
60
+ * `Ctrl + Left Click`: Add measurement point
61
+ * `Ctrl + Right Click`: Remove last point
62
+ * Total distance displayed automatically
63
+
64
+ 🎥 **Camera Controls** - Navigate the 3D scene
65
+ * `Double Click`: Set camera center to point
66
+ * `Right Drag`: Rotate view
67
+ * `Left Drag`: Pan view
68
+ * `Mouse Wheel`: Zoom in/out
69
+
70
+ ⚙️ **Settings** - Press `M` to open settings window
71
+ * Adjust visualization properties
60
72
 
61
73
  For example, you can download and view point clouds of Tokyo in LAS format from the following link:
62
74
 
63
75
  [Tokyo Point Clouds](https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024/resource/7807d6d1-29f3-4b36-b0c8-f7aa0ea2cff3)
64
76
 
65
- ![Cloud Viewer Screenshot](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/03c981c6-1aec-e5b9-4536-e07e1e56ff29.png)
77
+ ![Cloud Viewer Screenshot](imgs/tokyo.png)
66
78
 
67
- Press `M` on your keyboard to display a menu on the screen, where you can modify visualization settings for each item. For example, you can adjust various settings such as shape, size, color, and transparency for `CloudItem`.
79
+ **Mesh Support** - Starting from version 1.2.4, mesh files (.stl) are now supported.
68
80
 
69
- ![Cloud Viewer Settings](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/deeb996a-e419-58f4-6bc2-535099b1b73a.png)
81
+ ![Screenshot from 2026-02-04 18-32-04.png](imgs/mesh.png)
70
82
 
71
83
  ### 2. ROS Viewer
72
84
 
@@ -0,0 +1,5 @@
1
+ from q3dviewer.custom_items import *
2
+ from q3dviewer.glwidget import *
3
+ from q3dviewer.viewer import *
4
+ from q3dviewer.base_item import *
5
+ from q3dviewer.base_glwidget import *
@@ -8,4 +8,6 @@ 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.static_mesh_item import StaticMeshItem
11
12
  from q3dviewer.custom_items.mesh_item import MeshItem
13
+
@@ -17,7 +17,6 @@ from q3dviewer.utils import set_uniform, text_to_rgba
17
17
  import time
18
18
 
19
19
 
20
-
21
20
  class MeshItem(BaseItem):
22
21
  """
23
22
  A OpenGL mesh item for rendering 3D triangular meshes.
@@ -63,6 +62,7 @@ class MeshItem(BaseItem):
63
62
 
64
63
  # Settings flag
65
64
  self.need_update_setting = True
65
+ self.need_update_buffer = True
66
66
  self.path = os.path.dirname(__file__)
67
67
 
68
68
 
@@ -181,102 +181,144 @@ class MeshItem(BaseItem):
181
181
 
182
182
  def set_data(self, data):
183
183
  """
184
- Set complete mesh data at once.
185
-
186
184
  Args:
187
- data: is Nx3 numpy array (N must be divisible by 3) or dict
188
- if is dict, uses the dict format:
189
- [face_key: (v0.x, v0.y, v0.z, ..., v3.z, good)]
185
+ data: One of the following formats:
186
+ - Nx3 numpy array (N must be divisible by 3): vertex list -> static
187
+ - Nx9 numpy array: triangle list -> static
188
+ - Structured array with dtype [('key', int64), ('vertices', float32, (12,)), ('good', uint32)] -> incremental
190
189
  """
191
- self.clear_mesh()
192
-
193
- if isinstance(data, dict):
194
- # Use dict format directly (from get_mesh_data/get_incremental_mesh_data)
190
+ if not isinstance(data, np.ndarray):
191
+ raise ValueError("Data must be a numpy array")
192
+
193
+ want_dtype = np.dtype([
194
+ ('key', np.int64),
195
+ ('vertices', np.float32, (12,)),
196
+ ('good', np.uint32)
197
+ ])
198
+
199
+ # Structured array format -> use incremental path (has keys for updates)
200
+ if data.dtype == want_dtype:
195
201
  self.set_incremental_data(data)
196
202
  return
197
-
198
- # Check if Nx3 array
203
+
204
+ # Nx3 or Nx9 format -> use static path (more efficient, no key overhead)
205
+ if data.ndim == 2 and data.shape[1] in [3, 9]:
206
+ self.set_static_data(data)
207
+ return
208
+
209
+ raise ValueError("Data must be Nx3, Nx9, or structured array format")
199
210
 
211
+ def set_static_data(self, data):
212
+ """
213
+ Efficiently set static mesh data without key2index overhead.
214
+ For static meshes that don't need incremental updates.
215
+
216
+ Args:
217
+ data: numpy array in one of these formats:
218
+ - Nx3: vertex list (N must be divisible by 3)
219
+ - Nx9: triangle list
220
+ """
200
221
  if not isinstance(data, np.ndarray):
201
- raise ValueError("Invalid data type")
202
-
203
- good_format = False
204
- if data.ndim == 2 and \
205
- data.shape[1] == 3 and \
206
- data.shape[0] % 3 == 0:
207
- good_format = True
208
-
209
- if not good_format:
210
- raise ValueError("Invalid data shape")
211
-
212
- # Convert to Nx13 numpy array
213
- N = data.shape[0] // 3
214
- faces = np.zeros((N, 13), dtype=np.float32)
215
- tmp = data.reshape(N, 9)
216
- faces[:, 0:3] = tmp[:, 0:3] # copy v0
217
- faces[:, 3:6] = tmp[:, 3:6] # copy v1
218
- faces[:, 6:9] = tmp[:, 6:9] # copy v2
219
- faces[:, 9:12] = tmp[:, 6:9] # copy v3 from v2 (degenerate quad)
220
- faces[:, 12] = 1.0 # set good=1.0
222
+ raise ValueError("Data must be a numpy array")
223
+
224
+ if data.ndim != 2:
225
+ raise ValueError(f"Data must be 2D array, got {data.ndim}D")
226
+ self.clear_mesh()
227
+ # Handle Nx3 format
228
+ if data.shape[1] == 3:
229
+ if data.shape[0] % 3 != 0:
230
+ raise ValueError(f"Nx3 format requires N divisible by 3, got N={data.shape[0]}")
231
+
232
+ num_faces = data.shape[0] // 3
233
+ faces = np.zeros((num_faces, 13), dtype=np.float32)
234
+
235
+ # Reshape to (num_faces, 9) for efficient copying
236
+ tmp = data.reshape(num_faces, 9)
237
+ faces[:, 0:3] = tmp[:, 0:3] # v0
238
+ faces[:, 3:6] = tmp[:, 3:6] # v1
239
+ faces[:, 6:9] = tmp[:, 6:9] # v2
240
+ faces[:, 9:12] = tmp[:, 6:9] # v3 = v2 (degenerate)
241
+ faces[:, 12] = 1.0 # good=1.0
242
+
243
+ # Handle Nx9 format
244
+ elif data.shape[1] == 9:
245
+ num_faces = data.shape[0]
246
+ faces = np.zeros((num_faces, 13), dtype=np.float32)
247
+
248
+ faces[:, 0:9] = data # Copy all 9 vertices
249
+ faces[:, 9:12] = data[:, 6:9] # v3 = v2 (degenerate)
250
+ faces[:, 12] = 1.0 # good=1.0
251
+
252
+ else:
253
+ raise ValueError(f"Data shape must be Nx3 or Nx9, got Nx{data.shape[1]}")
254
+
255
+ # Replace faces buffer (static data, no key management)
256
+ self.clear_mesh()
221
257
  self.faces = faces
222
- self.valid_f_top = N
258
+ self.valid_f_top = num_faces
259
+ self.need_update_buffer = True
223
260
 
224
261
 
225
262
  def set_incremental_data(self, fs):
226
263
  """
227
264
  Incrementally update mesh with new face data.
228
265
  Args:
229
- fs: Dict {face_key: (v0.x, v0.y, v0.z, v1.x, v1.y, v1.z,
230
- v2.x, v2.y, v2.z, v3.x, v3.y, v3.z, good), ...}
231
- 13-tuple with vertex positions and good flag (0.0 or 1.0)
232
- If good==1:
233
- Triangle 1: (v0, v1, v3)
234
- Triangle 2: (v0, v3, v2)
266
+ fs: Structured numpy array with dtype:
267
+ [('key', np.int64), ('vertices', np.float32, (12,)), ('good', np.uint32)]
268
+ - key: unique identifier for the face
269
+ - vertices: 12 floats representing 4 vertices (v0, v1, v2, v3)
270
+ - good: 0 or 1, whether to render this face
235
271
  Updates:
236
272
  - faces: updates existing faces or appends new ones
237
273
  - key2index: tracks face_key -> face_index mapping
238
274
  """
239
- if not fs:
275
+ if fs is None or len(fs) == 0:
240
276
  return
277
+
278
+ if not isinstance(fs, np.ndarray) or fs.dtype.names is None:
279
+ raise ValueError("fs must be a structured numpy array with fields: key, vertices, good")
241
280
 
242
281
  # Ensure enough capacity in faces buffer
243
- # wasted cases are better than frequent expansions
244
282
  while self.valid_f_top + len(fs) > len(self.faces):
245
283
  self._expand_face_buffer()
246
284
 
247
- # Optimization: Separate updates from new insertions to avoid
248
- # dictionary lookup performance degradation during growth
249
- update_idxs = [] # [idx, ...]
250
- update_data = [] # [face_data, ...]
251
- new_keys = [] # [key, ...]
252
- new_data = [] # [face_data, ...]
253
-
254
- for face_key, face_data in fs.items():
255
- face_idx = self.key2index.get(face_key)
256
- if face_idx is not None:
257
- update_idxs.append(face_idx)
258
- update_data.append(face_data)
259
- else:
260
- new_keys.append(face_key)
261
- new_data.append(face_data)
262
-
285
+ # Prepare face data: convert structured array to Nx13 format
286
+ n_faces = len(fs)
287
+ face_data = np.zeros((n_faces, 13), dtype=np.float32)
288
+
289
+ # Copy vertices (12 floats -> positions 0:12)
290
+ face_data[:, :12] = fs['vertices']
291
+
292
+ # Copy good flag (position 12)
293
+ face_data[:, 12] = fs['good'].astype(np.float32)
294
+
295
+ # Extract keys
296
+ keys = fs['key']
297
+
298
+ # Optimization: Separate updates from new insertions
299
+ update_mask = np.array([key in self.key2index for key in keys], dtype=bool)
300
+ new_mask = ~update_mask
301
+
263
302
  # Batch update existing faces
264
- if update_data:
265
- indices = np.array(update_idxs, dtype=np.int32)
266
- data = np.array(update_data, dtype=np.float32)
267
- self.faces[indices] = data
303
+ if np.any(update_mask):
304
+ update_keys = keys[update_mask]
305
+ update_indices = np.array([self.key2index[key] for key in update_keys], dtype=np.int32)
306
+ self.faces[update_indices] = face_data[update_mask]
268
307
 
269
308
  # Batch insert new faces
270
- if new_data:
271
- n_new = len(new_data)
272
- data = np.array(new_data, dtype=np.float32)
273
- self.faces[self.valid_f_top: self.valid_f_top + n_new] = data
274
-
309
+ if np.any(new_mask):
310
+ new_keys = keys[new_mask]
311
+ new_face_data = face_data[new_mask]
312
+ n_new = len(new_keys)
313
+
314
+ # Insert data
315
+ self.faces[self.valid_f_top: self.valid_f_top + n_new] = new_face_data
316
+
275
317
  # Update key2index mapping for new faces
276
318
  for i, face_key in enumerate(new_keys):
277
319
  self.key2index[face_key] = self.valid_f_top + i
278
-
279
320
  self.valid_f_top += n_new
321
+ self.need_update_buffer = True
280
322
 
281
323
  def _expand_face_buffer(self):
282
324
  """Expand the faces buffer when capacity is reached"""
@@ -367,12 +409,15 @@ class MeshItem(BaseItem):
367
409
  self._gpu_face_capacity = len(self.faces)
368
410
 
369
411
  # Upload faces to VBO
370
- glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
371
- glBufferSubData(GL_ARRAY_BUFFER,
372
- 0,
373
- self.valid_f_top * 13 * 4, # 13 floats * 4 bytes per face
374
- self.faces[:self.valid_f_top])
375
- glBindBuffer(GL_ARRAY_BUFFER, 0)
412
+ if self.need_update_buffer:
413
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
414
+ glBufferSubData(GL_ARRAY_BUFFER,
415
+ 0,
416
+ self.valid_f_top * 13 * 4, # 13 floats * 4 bytes per face
417
+ self.faces[:self.valid_f_top])
418
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
419
+ self.need_update_buffer = False
420
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
376
421
 
377
422
  def update_setting(self):
378
423
  """Set fixed rendering parameters (called once during initialization)"""
@@ -0,0 +1,330 @@
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
+ import numpy as np
9
+ from q3dviewer.base_item import BaseItem
10
+ from OpenGL.GL import *
11
+ from OpenGL.GL import shaders
12
+ from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
13
+
14
+ import os
15
+ from q3dviewer.utils import set_uniform, text_to_rgba
16
+
17
+
18
+ class StaticMeshItem(BaseItem):
19
+ """
20
+ A OpenGL mesh item for rendering static 3D triangular meshes.
21
+ Optimized for static geometry with triangle-only rendering.
22
+ Data format: Nx9 numpy array (3 vertices per triangle, no good flag needed)
23
+
24
+ Attributes:
25
+ color (str or tuple): Accepts any valid matplotlib color (e.g., 'red', '#FF4500', (1.0, 0.5, 0.0)).
26
+ wireframe (bool): If True, renders the mesh in wireframe mode.
27
+ """
28
+ def __init__(self, color='lightblue', wireframe=False):
29
+ super(StaticMeshItem, self).__init__()
30
+ self.wireframe = wireframe
31
+ self.color = color
32
+ self.flat_rgb = text_to_rgba(color, flat=True)
33
+
34
+ # Static mesh buffer: N x 9 numpy array
35
+ # Each row: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z]
36
+ self.vertices = None
37
+ self.num_triangles = 0
38
+
39
+ # OpenGL objects
40
+ self.vao = None
41
+ self.vbo = None
42
+ self.program = None
43
+
44
+ # Fixed rendering parameters
45
+ self.enable_lighting = True
46
+ self.line_width = 1.0
47
+ self.light_color = [1.0, 1.0, 1.0]
48
+ self.ambient_strength = 0.1
49
+ self.diffuse_strength = 1.2
50
+ self.specular_strength = 0.1
51
+ self.shininess = 32.0
52
+ self.alpha = 1.0
53
+
54
+ # Settings flag
55
+ self.need_update_setting = True
56
+ self.need_update_buffer = True
57
+ self.path = os.path.dirname(__file__)
58
+
59
+ def add_setting(self, layout):
60
+ """Add UI controls for mesh visualization"""
61
+ # Wireframe toggle
62
+ self.wireframe_box = QCheckBox("Wireframe Mode")
63
+ self.wireframe_box.setChecked(self.wireframe)
64
+ self.wireframe_box.toggled.connect(self.update_wireframe)
65
+ layout.addWidget(self.wireframe_box)
66
+
67
+ # Enable lighting toggle
68
+ self.lighting_box = QCheckBox("Enable Lighting")
69
+ self.lighting_box.setChecked(self.enable_lighting)
70
+ self.lighting_box.toggled.connect(self.update_enable_lighting)
71
+ layout.addWidget(self.lighting_box)
72
+
73
+ # Color setting
74
+ label_rgb = QLabel("Color:")
75
+ label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
76
+ layout.addWidget(label_rgb)
77
+ self.edit_rgb = QLineEdit()
78
+ self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
79
+ self.edit_rgb.setText(self.color)
80
+ self.edit_rgb.textChanged.connect(self._on_color)
81
+ layout.addWidget(self.edit_rgb)
82
+
83
+ # Material property controls for Phong lighting
84
+ if self.enable_lighting:
85
+ # Ambient strength control
86
+ ambient_layout = QHBoxLayout()
87
+ ambient_label = QLabel("Ambient Strength:")
88
+ ambient_layout.addWidget(ambient_label)
89
+ self.ambient_slider = QSlider()
90
+ self.ambient_slider.setOrientation(1) # Qt.Horizontal
91
+ self.ambient_slider.setRange(0, 100)
92
+ self.ambient_slider.setValue(int(self.ambient_strength * 100))
93
+ self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
94
+ ambient_layout.addWidget(self.ambient_slider)
95
+ layout.addLayout(ambient_layout)
96
+
97
+ # Diffuse strength control
98
+ diffuse_layout = QHBoxLayout()
99
+ diffuse_label = QLabel("Diffuse Strength:")
100
+ diffuse_layout.addWidget(diffuse_label)
101
+ self.diffuse_slider = QSlider()
102
+ self.diffuse_slider.setOrientation(1)
103
+ self.diffuse_slider.setRange(0, 200)
104
+ self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
105
+ self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
106
+ diffuse_layout.addWidget(self.diffuse_slider)
107
+ layout.addLayout(diffuse_layout)
108
+
109
+ # Specular strength control
110
+ specular_layout = QHBoxLayout()
111
+ specular_label = QLabel("Specular Strength:")
112
+ specular_layout.addWidget(specular_label)
113
+ self.specular_slider = QSlider()
114
+ self.specular_slider.setOrientation(1)
115
+ self.specular_slider.setRange(0, 100)
116
+ self.specular_slider.setValue(int(self.specular_strength * 100))
117
+ self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
118
+ specular_layout.addWidget(self.specular_slider)
119
+ layout.addLayout(specular_layout)
120
+
121
+ # Shininess control
122
+ shininess_layout = QHBoxLayout()
123
+ shininess_label = QLabel("Shininess:")
124
+ shininess_layout.addWidget(shininess_label)
125
+ self.shininess_slider = QSlider()
126
+ self.shininess_slider.setOrientation(1)
127
+ self.shininess_slider.setRange(1, 256)
128
+ self.shininess_slider.setValue(int(self.shininess))
129
+ self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
130
+ shininess_layout.addWidget(self.shininess_slider)
131
+ layout.addLayout(shininess_layout)
132
+
133
+ def _on_color(self, color):
134
+ try:
135
+ self.color = color
136
+ self.flat_rgb = text_to_rgba(color, flat=True)
137
+ self.need_update_setting = True
138
+ except ValueError:
139
+ pass
140
+
141
+ def update_wireframe(self, value):
142
+ self.wireframe = value
143
+
144
+ def update_enable_lighting(self, value):
145
+ self.enable_lighting = value
146
+ self.need_update_setting = True
147
+
148
+ def update_line_width(self, value):
149
+ self.line_width = value
150
+ self.need_update_setting = True
151
+
152
+ def update_ambient_strength(self, value):
153
+ self.ambient_strength = value
154
+ self.need_update_setting = True
155
+
156
+ def update_diffuse_strength(self, value):
157
+ self.diffuse_strength = value
158
+ self.need_update_setting = True
159
+
160
+ def update_specular_strength(self, value):
161
+ self.specular_strength = value
162
+ self.need_update_setting = True
163
+
164
+ def update_shininess(self, value):
165
+ self.shininess = value
166
+ self.need_update_setting = True
167
+
168
+ def update_alpha(self, value):
169
+ """Update mesh alpha (opacity)"""
170
+ self.alpha = float(value)
171
+ self.need_update_setting = True
172
+
173
+ def set_data(self, data):
174
+ """
175
+ Set complete mesh data at once.
176
+
177
+ Args:
178
+ data: Nx9 numpy array where N is the number of triangles
179
+ Each row: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z]
180
+ """
181
+ print("Setting static mesh data with {} triangles".format(len(data)))
182
+ if not isinstance(data, np.ndarray):
183
+ raise ValueError("Data must be a numpy array")
184
+
185
+ # Check shape
186
+ if data.ndim != 2 or data.shape[1] != 9:
187
+ # Try to reshape if it's Nx3 (vertex list)
188
+ if data.ndim == 2 and data.shape[1] == 3 and data.shape[0] % 3 == 0:
189
+ data = data.reshape(-1, 9)
190
+ else:
191
+ raise ValueError(f"Invalid data shape {data.shape}. Expected Nx9 or (N*3)x3")
192
+
193
+ self.vertices = data.astype(np.float32)
194
+ self.num_triangles = len(self.vertices)
195
+ self.need_update_buffer = True
196
+
197
+ def clear_mesh(self):
198
+ """Clear all mesh data"""
199
+ self.vertices = None
200
+ self.num_triangles = 0
201
+ self.need_update_buffer = True
202
+
203
+ def initialize_gl(self):
204
+ """OpenGL initialization - load triangle shader"""
205
+ frag_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
206
+
207
+ try:
208
+ vert_shader = open(self.path + '/../shaders/triangle_mesh_vert.glsl', 'r').read()
209
+ geom_shader = open(self.path + '/../shaders/triangle_mesh_geom.glsl', 'r').read()
210
+ self.program = shaders.compileProgram(
211
+ shaders.compileShader(vert_shader, GL_VERTEX_SHADER),
212
+ shaders.compileShader(geom_shader, GL_GEOMETRY_SHADER),
213
+ shaders.compileShader(frag_shader, GL_FRAGMENT_SHADER),
214
+ )
215
+ except Exception as e:
216
+ print(f"Error compiling static mesh shader: {e}")
217
+ raise
218
+
219
+ def update_render_buffer(self):
220
+ """
221
+ Update GPU buffer with triangle data.
222
+ Each triangle: 9 floats (3 vertices x 3 coordinates)
223
+ """
224
+ if self.num_triangles == 0 or self.vertices is None:
225
+ return
226
+
227
+ # Initialize buffers on first call
228
+ if self.vao is None:
229
+ self.vao = glGenVertexArrays(1)
230
+ self.vbo = glGenBuffers(1)
231
+
232
+ if not self.need_update_buffer:
233
+ return
234
+
235
+ glBindVertexArray(self.vao)
236
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
237
+
238
+ # Upload all triangle data
239
+ glBufferData(GL_ARRAY_BUFFER,
240
+ self.vertices.nbytes,
241
+ self.vertices,
242
+ GL_STATIC_DRAW)
243
+
244
+ # Setup vertex attributes
245
+ # Triangle: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z]
246
+ # 9 floats = 36 bytes stride
247
+ stride = 36
248
+
249
+ # v0 (location 1) - vec3
250
+ glEnableVertexAttribArray(1)
251
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(0))
252
+ glVertexAttribDivisor(1, 1)
253
+
254
+ # v1 (location 2) - vec3
255
+ glEnableVertexAttribArray(2)
256
+ glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(12))
257
+ glVertexAttribDivisor(2, 1)
258
+
259
+ # v2 (location 3) - vec3
260
+ glEnableVertexAttribArray(3)
261
+ glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(24))
262
+ glVertexAttribDivisor(3, 1)
263
+
264
+ glBindVertexArray(0)
265
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
266
+ self.need_update_buffer = False
267
+
268
+ def update_setting(self):
269
+ """Set rendering parameters"""
270
+ if not self.need_update_setting:
271
+ return
272
+
273
+ set_uniform(self.program, int(self.enable_lighting), 'if_light')
274
+ set_uniform(self.program, 1, 'two_sided')
275
+ set_uniform(self.program, np.array(self.light_color, dtype=np.float32), 'light_color')
276
+ set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
277
+ set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
278
+ set_uniform(self.program, float(self.specular_strength), 'specular_strength')
279
+ set_uniform(self.program, float(self.shininess), 'shininess')
280
+ set_uniform(self.program, float(self.alpha), 'alpha')
281
+ set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
282
+ self.need_update_setting = False
283
+
284
+ def paint(self):
285
+ """
286
+ Render the static mesh using instanced rendering with geometry shader.
287
+ Each triangle instance is rendered as a point, geometry shader generates 1 triangle.
288
+ """
289
+ if self.num_triangles == 0 or self.vertices is None:
290
+ return
291
+
292
+ glUseProgram(self.program)
293
+
294
+ self.update_render_buffer()
295
+ self.update_setting()
296
+
297
+ view_matrix = self.glwidget().view_matrix
298
+ set_uniform(self.program, view_matrix, 'view')
299
+ project_matrix = self.glwidget().projection_matrix
300
+ set_uniform(self.program, project_matrix, 'projection')
301
+ view_pos = self.glwidget().center
302
+ set_uniform(self.program, np.array(view_pos), 'view_pos')
303
+
304
+ # Enable blending and depth testing
305
+ glEnable(GL_BLEND)
306
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
307
+ glEnable(GL_DEPTH_TEST)
308
+ glDisable(GL_CULL_FACE) # two-sided rendering
309
+
310
+ # Set line width
311
+ glLineWidth(self.line_width)
312
+
313
+ # Bind VAO
314
+ glBindVertexArray(self.vao)
315
+
316
+ if self.wireframe:
317
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
318
+ else:
319
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
320
+
321
+ # Draw using instanced rendering
322
+ # Input: POINTS (one per triangle instance)
323
+ # Geometry shader generates 1 triangle (3 vertices) per point
324
+ glDrawArraysInstanced(GL_POINTS, 0, 1, self.num_triangles)
325
+
326
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
327
+ glBindVertexArray(0)
328
+ glDisable(GL_DEPTH_TEST)
329
+ glDisable(GL_BLEND)
330
+ glUseProgram(0)
@@ -10,6 +10,7 @@ import q3dviewer as q3d
10
10
  from q3dviewer.Qt.QtWidgets import QVBoxLayout, QDialog, QLabel
11
11
  from q3dviewer.Qt.QtCore import QThread, Signal, Qt
12
12
  from q3dviewer import GLWidget
13
+ from q3dviewer.utils.helpers import get_version
13
14
 
14
15
  class ProgressWindow(QDialog):
15
16
  def __init__(self, parent=None):
@@ -186,7 +187,7 @@ def print_help():
186
187
 
187
188
  # Print title and table without border
188
189
  console.print()
189
- console.print("[bold magenta]☁️ Cloud Viewer Help[/bold magenta]\n")
190
+ console.print(f"[bold magenta]☁️ Cloud Viewer ({get_version()}) Help[/bold magenta]\n")
190
191
  console.print(table)
191
192
  console.print()
192
193
 
@@ -205,7 +206,7 @@ def main():
205
206
  marker_item = q3d.Text3DItem() # Changed from CloudItem to Text3DItem
206
207
  text_item = q3d.Text2DItem(pos=(20, 40), text="", color='lime', size=16)
207
208
  text_item.disable_setting()
208
- mesh_item = q3d.MeshItem() # Added MeshIOItem for mesh support
209
+ mesh_item = q3d.StaticMeshItem()
209
210
 
210
211
  viewer.add_items(
211
212
  {'marker': marker_item,
@@ -12,7 +12,7 @@ from q3dviewer.Qt.QtCore import QTimer
12
12
  from q3dviewer.Qt.QtGui import QKeyEvent
13
13
  from q3dviewer.Qt import QtCore
14
14
  from q3dviewer import GLWidget
15
- from q3dviewer.tools.cloud_viewer import ProgressDialog, FileLoaderThread
15
+ from q3dviewer.tools.cloud_viewer import FileLoaderThread, ProgressWindow
16
16
 
17
17
  import imageio.v2 as imageio
18
18
  import os
@@ -386,20 +386,19 @@ class CMMViewer(q3d.Viewer):
386
386
  """
387
387
  Overwrite the drop event to open the cloud file.
388
388
  """
389
- self.progress_dialog = ProgressDialog(self)
390
- self.progress_dialog.show()
389
+ self.progress_window = ProgressWindow(self)
390
+ self.progress_window.show()
391
391
  files = event.mimeData().urls()
392
392
  self.progress_thread = FileLoaderThread(self, files)
393
- self['cloud'].load(files[0].toLocalFile(), append=False)
394
393
  self.progress_thread.progress.connect(self.file_loading_progress)
395
394
  self.progress_thread.finished.connect(self.file_loading_finished)
396
395
  self.progress_thread.start()
397
396
 
398
- def file_loading_progress(self, value):
399
- self.progress_dialog.set_value(value)
397
+ def file_loading_progress(self, current, total, file_name):
398
+ self.progress_window.update_progress(current, total, file_name)
400
399
 
401
400
  def file_loading_finished(self):
402
- self.progress_dialog.close()
401
+ self.progress_window.close()
403
402
 
404
403
  def open_cloud_file(self, file, append=False):
405
404
  cloud_item = self['cloud']
@@ -417,7 +416,7 @@ def main():
417
416
  args = parser.parse_args()
418
417
  app = q3d.QApplication(['Film Maker'])
419
418
  viewer = CMMViewer(name='Film Maker', update_interval=30)
420
- cloud_item = q3d.CloudIOItem(size=1, point_type='SPHERE', alpha=0.5, depth_test=True)
419
+ cloud_item = q3d.CloudIOItem(size=1, point_type='SPHERE', alpha=0.5)
421
420
  grid_item = q3d.GridItem(size=1000, spacing=20)
422
421
 
423
422
  viewer.add_items(
@@ -7,6 +7,18 @@ import numpy as np
7
7
  from OpenGL.GL import *
8
8
 
9
9
 
10
+ def get_version():
11
+ """
12
+ Get the version of q3dviewer package.
13
+ """
14
+ try:
15
+ from importlib.metadata import version
16
+ return version('q3dviewer')
17
+ except Exception:
18
+ # Fallback if package is not installed
19
+ return 'unknown'
20
+
21
+
10
22
  def rainbow(scalars, scalar_min=0, scalar_max=255):
11
23
  range = scalar_max - scalar_min
12
24
  values = 1.0 - (scalars - scalar_min) / range
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: q3dviewer
3
- Version: 1.2.3
3
+ Version: 1.2.4
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
7
- License: UNKNOWN
7
+ Author-email: liu.yang@jp.panasonic.com
8
+ License: MIT
8
9
  Description:
9
10
  ![q3dviewer Logo](imgs/logo.png)
10
11
 
@@ -58,22 +59,34 @@ Description:
58
59
  ```
59
60
 
60
61
  **Basic Operations**
61
- * Load files: Drag and drop point cloud files onto the window (multiple files are OK).
62
- * `M` key: Display the visualization settings screen for point clouds, background color, etc.
63
- * `Left mouse button` & `W, A, S, D` keys: Move the viewpoint on the horizontal plane.
64
- * `Z, X` keys: Move in the direction the screen is facing.
65
- * `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the screen center unchanged.
66
- * `Shift` + `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the camera position unchanged.
62
+
63
+ 📁 **Load Files** - Drag and drop files into the viewer
64
+ * Point clouds: .pcd, .ply, .las, .e57
65
+ * Mesh files: .stl
66
+
67
+ 📏 **Measure Distance** - Interactive point measurement
68
+ * `Ctrl + Left Click`: Add measurement point
69
+ * `Ctrl + Right Click`: Remove last point
70
+ * Total distance displayed automatically
71
+
72
+ 🎥 **Camera Controls** - Navigate the 3D scene
73
+ * `Double Click`: Set camera center to point
74
+ * `Right Drag`: Rotate view
75
+ * `Left Drag`: Pan view
76
+ * `Mouse Wheel`: Zoom in/out
77
+
78
+ ⚙️ **Settings** - Press `M` to open settings window
79
+ * Adjust visualization properties
67
80
 
68
81
  For example, you can download and view point clouds of Tokyo in LAS format from the following link:
69
82
 
70
83
  [Tokyo Point Clouds](https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024/resource/7807d6d1-29f3-4b36-b0c8-f7aa0ea2cff3)
71
84
 
72
- ![Cloud Viewer Screenshot](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/03c981c6-1aec-e5b9-4536-e07e1e56ff29.png)
85
+ ![Cloud Viewer Screenshot](imgs/tokyo.png)
73
86
 
74
- Press `M` on your keyboard to display a menu on the screen, where you can modify visualization settings for each item. For example, you can adjust various settings such as shape, size, color, and transparency for `CloudItem`.
87
+ **Mesh Support** - Starting from version 1.2.4, mesh files (.stl) are now supported.
75
88
 
76
- ![Cloud Viewer Settings](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/deeb996a-e419-58f4-6bc2-535099b1b73a.png)
89
+ ![Screenshot from 2026-02-04 18-32-04.png](imgs/mesh.png)
77
90
 
78
91
  ### 2. ROS Viewer
79
92
 
@@ -22,6 +22,7 @@ q3dviewer/custom_items/grid_item.py
22
22
  q3dviewer/custom_items/image_item.py
23
23
  q3dviewer/custom_items/line_item.py
24
24
  q3dviewer/custom_items/mesh_item.py
25
+ q3dviewer/custom_items/static_mesh_item.py
25
26
  q3dviewer/custom_items/text3d_item.py
26
27
  q3dviewer/custom_items/text_item.py
27
28
  q3dviewer/tools/__init__.py
@@ -2,8 +2,10 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='q3dviewer',
5
- version='1.2.3',
5
+ version='1.2.4',
6
6
  author="Liu Yang",
7
+ author_email="liu.yang@jp.panasonic.com",
8
+ license="MIT",
7
9
  description="A library designed for quickly deploying a 3D viewer.",
8
10
  long_description=open("README.md").read(),
9
11
  long_description_content_type="text/markdown",
@@ -1,14 +0,0 @@
1
- import time
2
- t1 = time.time()
3
- from q3dviewer.custom_items import *
4
- t2 = time.time()
5
- from q3dviewer.glwidget import *
6
- from q3dviewer.viewer import *
7
- from q3dviewer.base_item import *
8
- from q3dviewer.base_glwidget import *
9
- t3 = time.time()
10
-
11
- from q3dviewer.Qt import Q3D_DEBUG
12
- if Q3D_DEBUG:
13
- print("Import custom items: ", t2 - t1)
14
- print("Import base q3dviewer: ", t3 - t2)
File without changes
File without changes