q3dviewer 1.2.3__tar.gz → 1.2.5__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.5}/PKG-INFO +24 -11
  2. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/README.md +21 -9
  3. q3dviewer-1.2.5/q3dviewer/__init__.py +5 -0
  4. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/__init__.py +2 -0
  5. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/cloud_item.py +22 -20
  6. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/mesh_item.py +155 -78
  7. q3dviewer-1.2.5/q3dviewer/custom_items/static_mesh_item.py +330 -0
  8. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/cloud_viewer.py +3 -2
  9. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/film_maker.py +7 -8
  10. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/helpers.py +12 -0
  11. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/PKG-INFO +24 -11
  12. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/SOURCES.txt +1 -0
  13. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/setup.py +3 -1
  14. q3dviewer-1.2.3/q3dviewer/__init__.py +0 -14
  15. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/Qt/__init__.py +0 -0
  16. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/base_glwidget.py +0 -0
  17. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/base_item.py +0 -0
  18. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/axis_item.py +0 -0
  19. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/cloud_io_item.py +0 -0
  20. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/frame_item.py +0 -0
  21. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/gaussian_item.py +0 -0
  22. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/grid_item.py +0 -0
  23. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/image_item.py +0 -0
  24. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/line_item.py +0 -0
  25. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/text3d_item.py +0 -0
  26. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/custom_items/text_item.py +0 -0
  27. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/glwidget.py +0 -0
  28. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/__init__.py +0 -0
  29. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/example_viewer.py +0 -0
  30. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/gaussian_viewer.py +0 -0
  31. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/lidar_calib.py +0 -0
  32. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/lidar_cam_calib.py +0 -0
  33. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/tools/ros_viewer.py +0 -0
  34. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/__init__.py +0 -0
  35. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/cloud_io.py +0 -0
  36. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/convert_ros_msg.py +0 -0
  37. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/gl_helper.py +0 -0
  38. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/maths.py +0 -0
  39. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/utils/range_slider.py +0 -0
  40. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer/viewer.py +0 -0
  41. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/dependency_links.txt +0 -0
  42. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/entry_points.txt +0 -0
  43. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/requires.txt +0 -0
  44. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/q3dviewer.egg-info/top_level.txt +0 -0
  45. {q3dviewer-1.2.3 → q3dviewer-1.2.5}/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.5
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
+
@@ -39,32 +39,34 @@ class CloudItem(BaseItem):
39
39
  - 'SPHERE': Draw each point as a sphere in 3D space.
40
40
  depth_test (bool): Whether to enable depth testing. If True, points closer to the camera will appear in front of farther ones.
41
41
  """
42
+ # Class-level constants
43
+ STRIDE = 16 # Stride of cloud array in bytes
44
+ CAPACITY = 10000000 # Initial buffer capacity (10M points)
45
+ DATA_TYPE = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
46
+ MODE_TABLE = {'FLAT': 0, 'I': 1, 'RGB': 2, 'GRAY': 3}
47
+ POINT_TYPE_TABLE = {'PIXEL': 0, 'SQUARE': 1, 'SPHERE': 2}
48
+
42
49
  def __init__(self, size, alpha,
43
50
  color_mode='I',
44
51
  color='white',
45
52
  point_type='PIXEL'):
46
53
  super().__init__()
47
- self.STRIDE = 16 # stride of cloud array
48
54
  self.valid_buff_top = 0
49
55
  self.add_buff_loc = 0
50
56
  self.alpha = alpha
51
57
  self.size = size
52
58
  self.point_type = point_type
53
59
  self.mutex = threading.Lock()
54
- self.data_type = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
55
60
  self.color = color
56
61
  try:
57
62
  self.flat_rgb = text_to_rgba(color, flat=True)
58
63
  except ValueError:
59
64
  print(f"Invalid color: {color}, please use matplotlib color format")
60
65
  exit(1)
61
- self.mode_table = {'FLAT': 0, 'I': 1, 'RGB': 2, 'GRAY': 3}
62
- self.point_type_table = {'PIXEL': 0, 'SQUARE': 1, 'SPHERE': 2}
63
- self.color_mode = self.mode_table[color_mode]
64
- self.CAPACITY = 10000000 # 10MB * 3 (x,y,z, color) * 4
66
+ self.color_mode = self.MODE_TABLE[color_mode]
65
67
  self.vmin = 0
66
68
  self.vmax = 255
67
- self.buff = np.empty((0), self.data_type)
69
+ self.buff = np.empty((0), self.DATA_TYPE)
68
70
  self.wait_add_data = None
69
71
  self.need_update_setting = True
70
72
  self.max_cloud_size = 300000000
@@ -78,7 +80,7 @@ class CloudItem(BaseItem):
78
80
  combo_ptype.addItem("pixels")
79
81
  combo_ptype.addItem("flat squares")
80
82
  combo_ptype.addItem("spheres")
81
- combo_ptype.setCurrentIndex(self.point_type_table[self.point_type])
83
+ combo_ptype.setCurrentIndex(self.POINT_TYPE_TABLE[self.point_type])
82
84
  combo_ptype.currentIndexChanged.connect(self._on_point_type_selection)
83
85
  layout.addWidget(combo_ptype)
84
86
 
@@ -88,7 +90,7 @@ class CloudItem(BaseItem):
88
90
  self.box_size.setValue(int(self.size))
89
91
  self.box_size.setRange(0, 100)
90
92
  self.box_size.valueChanged.connect(self.set_size)
91
- self._on_point_type_selection(self.point_type_table[self.point_type])
93
+ self._on_point_type_selection(self.POINT_TYPE_TABLE[self.point_type])
92
94
  layout.addWidget(self.box_size)
93
95
 
94
96
  box_alpha = QDoubleSpinBox()
@@ -133,11 +135,11 @@ class CloudItem(BaseItem):
133
135
  self.color_mode = index
134
136
  self.edit_rgb.hide()
135
137
  self.slider_v.hide()
136
- if (index == self.mode_table['FLAT']): # flat color
138
+ if (index == self.MODE_TABLE['FLAT']): # flat color
137
139
  self.edit_rgb.show()
138
- elif (index == self.mode_table['I']): # flat color
140
+ elif (index == self.MODE_TABLE['I']): # intensity
139
141
  self.slider_v.show()
140
- elif (index == self.mode_table['GRAY']): # flat color
142
+ elif (index == self.MODE_TABLE['GRAY']): # grayscale
141
143
  self.slider_v.show()
142
144
 
143
145
  self.need_update_setting = True
@@ -145,15 +147,15 @@ class CloudItem(BaseItem):
145
147
  def set_color_mode(self, color_mode):
146
148
  if color_mode in {'FLAT', 'RGB', 'I', 'GRAY'}:
147
149
  try:
148
- self.combo_color.setCurrentIndex(self.mode_table[color_mode])
150
+ self.combo_color.setCurrentIndex(self.MODE_TABLE[color_mode])
149
151
  except:
150
- self.color_mode = self.mode_table[color_mode]
152
+ self.color_mode = self.MODE_TABLE[color_mode]
151
153
  self.need_update_setting = True
152
154
  else:
153
155
  print(f"Invalid color mode: {color_mode}")
154
156
 
155
157
  def _on_point_type_selection(self, index):
156
- self.point_type = list(self.point_type_table.keys())[index]
158
+ self.point_type = list(self.POINT_TYPE_TABLE.keys())[index]
157
159
  if self.point_type == 'PIXEL':
158
160
  self.box_size.setPrefix("Set size (pixel): ")
159
161
  else:
@@ -184,7 +186,7 @@ class CloudItem(BaseItem):
184
186
  self.need_update_setting = True
185
187
 
186
188
  def clear(self):
187
- data = np.empty((0), self.data_type)
189
+ data = np.empty((0), self.DATA_TYPE)
188
190
  self.set_data(data)
189
191
 
190
192
  def set_data(self, data, append=False):
@@ -193,7 +195,7 @@ class CloudItem(BaseItem):
193
195
 
194
196
  if data.dtype in {np.dtype('float32'), np.dtype('float64')}:
195
197
  if data.size == 0:
196
- data = np.empty((0), self.data_type)
198
+ data = np.empty((0), self.DATA_TYPE)
197
199
  elif data.ndim == 2 and data.shape[1] >= 3:
198
200
  xyz = data[:, :3]
199
201
  if data.shape[1] >= 4:
@@ -201,7 +203,7 @@ class CloudItem(BaseItem):
201
203
  else:
202
204
  color = np.zeros(data.shape[0], dtype=np.uint32)
203
205
  data = np.rec.fromarrays(
204
- [xyz, color[:data.shape[0]]], dtype=self.data_type)
206
+ [xyz, color[:data.shape[0]]], dtype=self.DATA_TYPE)
205
207
 
206
208
  with self.mutex:
207
209
  if append:
@@ -226,7 +228,7 @@ class CloudItem(BaseItem):
226
228
  set_uniform(self.program, float(self.vmin), 'vmin')
227
229
  set_uniform(self.program, float(self.alpha), 'alpha')
228
230
  set_uniform(self.program, int(self.size), 'point_size')
229
- set_uniform(self.program, int(self.point_type_table[self.point_type]), 'point_type')
231
+ set_uniform(self.program, int(self.POINT_TYPE_TABLE[self.point_type]), 'point_type')
230
232
  glUseProgram(0)
231
233
  self.need_update_setting = False
232
234
 
@@ -245,7 +247,7 @@ class CloudItem(BaseItem):
245
247
  buff_capacity += self.CAPACITY
246
248
  if Q3D_DEBUG is not None:
247
249
  print("[Cloud Item] Update capacity to %d" % buff_capacity)
248
- new_buff = np.empty((buff_capacity), self.data_type)
250
+ new_buff = np.empty((buff_capacity), self.DATA_TYPE)
249
251
  new_buff[:self.add_buff_loc] = self.buff[:self.add_buff_loc]
250
252
  new_buff[self.add_buff_loc:new_buff_top] = self.wait_add_data
251
253
  self.buff = new_buff
@@ -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.
@@ -25,15 +24,21 @@ class MeshItem(BaseItem):
25
24
  color (str or tuple): Accepts any valid matplotlib color (e.g., 'red', '#FF4500', (1.0, 0.5, 0.0)).
26
25
  wireframe (bool): If True, renders the mesh in wireframe mode.
27
26
  """
27
+ # Class-level constants
28
+ FACE_CAPACITY = 1000000 # Initial capacity for faces
29
+ BIG_INT = 2**31 - 1 # Sentinel value for dirty region tracking
30
+ FACE_INPUT_DTYPE = np.dtype([
31
+ ('key', np.int64),
32
+ ('vertices', np.float32, (12,)),
33
+ ('good', np.uint8)
34
+ ])
35
+
28
36
  def __init__(self, color='lightblue', wireframe=False):
29
37
  super(MeshItem, self).__init__()
30
38
  self.wireframe = wireframe
31
39
  self.color = color
32
40
  self.flat_rgb = text_to_rgba(color, flat=True)
33
41
 
34
- # Incremental buffer management
35
- self.FACE_CAPACITY = 1000000 # Initial capacity for faces
36
-
37
42
  # Faces buffer: N x 13 numpy array
38
43
  # Each row: [v0.x, v0.y, v0.z, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z, v3.x, v3.y, v3.z, good]
39
44
  self.faces = np.zeros((self.FACE_CAPACITY, 13), dtype=np.float32)
@@ -41,6 +46,12 @@ class MeshItem(BaseItem):
41
46
  # valid_f_top: pointer to end of valid faces
42
47
  self.valid_f_top = 0
43
48
 
49
+ # Dirty region tracking for efficient GPU updates
50
+ # dirty_min: start index of modified region (inclusive)
51
+ # dirty_max: end index of modified region (exclusive)
52
+ self.dirty_min = self.BIG_INT
53
+ self.dirty_max = 0
54
+
44
55
  # key2index: mapping from face_key to face buffer index
45
56
  self.key2index = {} # {face_key: face_index}
46
57
 
@@ -63,6 +74,7 @@ class MeshItem(BaseItem):
63
74
 
64
75
  # Settings flag
65
76
  self.need_update_setting = True
77
+ self.need_update_buffer = True
66
78
  self.path = os.path.dirname(__file__)
67
79
 
68
80
 
@@ -181,102 +193,151 @@ class MeshItem(BaseItem):
181
193
 
182
194
  def set_data(self, data):
183
195
  """
184
- Set complete mesh data at once.
185
-
186
196
  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)]
197
+ data: One of the following formats:
198
+ - Nx3 numpy array (N must be divisible by 3): vertex list -> static
199
+ - Nx9 numpy array: triangle list -> static
200
+ - Structured array with dtype [('key', int64), ('vertices', float32, (12,)), ('good', uint32)] -> incremental
190
201
  """
191
- self.clear_mesh()
192
-
193
- if isinstance(data, dict):
194
- # Use dict format directly (from get_mesh_data/get_incremental_mesh_data)
202
+ if not isinstance(data, np.ndarray):
203
+ raise ValueError("Data must be a numpy array")
204
+
205
+ # Structured array format -> use incremental path (has keys for updates)
206
+ if data.dtype == self.FACE_INPUT_DTYPE:
195
207
  self.set_incremental_data(data)
196
208
  return
197
-
198
- # Check if Nx3 array
209
+
210
+ # Nx3 or Nx9 format -> use static path (more efficient, no key overhead)
211
+ if data.ndim == 2 and data.shape[1] in [3, 9]:
212
+ self.set_static_data(data)
213
+ return
214
+
215
+ raise ValueError("Data must be Nx3, Nx9, or structured array format")
199
216
 
217
+ def set_static_data(self, data):
218
+ """
219
+ Efficiently set static mesh data without key2index overhead.
220
+ For static meshes that don't need incremental updates.
221
+
222
+ Args:
223
+ data: numpy array in one of these formats:
224
+ - Nx3: vertex list (N must be divisible by 3)
225
+ - Nx9: triangle list
226
+ """
200
227
  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
228
+ raise ValueError("Data must be a numpy array")
229
+
230
+ if data.ndim != 2:
231
+ raise ValueError(f"Data must be 2D array, got {data.ndim}D")
232
+ self.clear_mesh()
233
+ # Handle Nx3 format
234
+ if data.shape[1] == 3:
235
+ if data.shape[0] % 3 != 0:
236
+ raise ValueError(f"Nx3 format requires N divisible by 3, got N={data.shape[0]}")
237
+
238
+ num_faces = data.shape[0] // 3
239
+ faces = np.zeros((num_faces, 13), dtype=np.float32)
240
+
241
+ # Reshape to (num_faces, 9) for efficient copying
242
+ tmp = data.reshape(num_faces, 9)
243
+ faces[:, 0:3] = tmp[:, 0:3] # v0
244
+ faces[:, 3:6] = tmp[:, 3:6] # v1
245
+ faces[:, 6:9] = tmp[:, 6:9] # v2
246
+ faces[:, 9:12] = tmp[:, 6:9] # v3 = v2 (degenerate)
247
+ faces[:, 12] = 1.0 # good=1.0
248
+
249
+ # Handle Nx9 format
250
+ elif data.shape[1] == 9:
251
+ num_faces = data.shape[0]
252
+ faces = np.zeros((num_faces, 13), dtype=np.float32)
253
+
254
+ faces[:, 0:9] = data # Copy all 9 vertices
255
+ faces[:, 9:12] = data[:, 6:9] # v3 = v2 (degenerate)
256
+ faces[:, 12] = 1.0 # good=1.0
257
+
258
+ else:
259
+ raise ValueError(f"Data shape must be Nx3 or Nx9, got Nx{data.shape[1]}")
260
+
261
+ # Replace faces buffer (static data, no key management)
262
+ self.clear_mesh()
221
263
  self.faces = faces
222
- self.valid_f_top = N
264
+ self.valid_f_top = num_faces
265
+
266
+ # Mark entire buffer as dirty
267
+ self.dirty_min = 0
268
+ self.dirty_max = self.valid_f_top
269
+ self.need_update_buffer = True
223
270
 
224
271
 
225
272
  def set_incremental_data(self, fs):
226
273
  """
227
274
  Incrementally update mesh with new face data.
228
275
  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)
276
+ fs: Structured numpy array with dtype:
277
+ [('key', np.int64), ('vertices', np.float32, (12,)), ('good', np.uint32)]
278
+ - key: unique identifier for the face
279
+ - vertices: 12 floats representing 4 vertices (v0, v1, v2, v3)
280
+ - good: 0 or 1, whether to render this face
235
281
  Updates:
236
282
  - faces: updates existing faces or appends new ones
237
283
  - key2index: tracks face_key -> face_index mapping
238
284
  """
239
- if not fs:
285
+ if fs is None or len(fs) == 0:
240
286
  return
287
+
288
+ if not isinstance(fs, np.ndarray) or fs.dtype.names is None:
289
+ raise ValueError("fs must be a structured numpy array with fields: key, vertices, good")
241
290
 
242
291
  # Ensure enough capacity in faces buffer
243
- # wasted cases are better than frequent expansions
244
292
  while self.valid_f_top + len(fs) > len(self.faces):
245
293
  self._expand_face_buffer()
246
294
 
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
-
295
+ # Prepare face data: convert structured array to Nx13 format
296
+ n_faces = len(fs)
297
+ face_data = np.zeros((n_faces, 13), dtype=np.float32)
298
+
299
+ # Copy vertices (12 floats -> positions 0:12)
300
+ face_data[:, :12] = fs['vertices']
301
+
302
+ # Copy good flag (position 12)
303
+ face_data[:, 12] = fs['good'].astype(np.float32)
304
+
305
+ # Extract keys
306
+ keys = fs['key']
307
+
308
+ # Optimization: Separate updates from new insertions
309
+ update_mask = np.array([key in self.key2index for key in keys], dtype=bool)
310
+ new_mask = ~update_mask
311
+
263
312
  # 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
313
+ if np.any(update_mask):
314
+ update_keys = keys[update_mask]
315
+ update_indices = np.array([self.key2index[key] for key in update_keys], dtype=np.int32)
316
+ self.faces[update_indices] = face_data[update_mask]
317
+
318
+ # Update dirty region for modified faces
319
+ self.dirty_min = min(self.dirty_min, int(np.min(update_indices)))
320
+ self.dirty_max = max(self.dirty_max, int(np.max(update_indices) + 1))
321
+ self.need_update_buffer = True
268
322
 
269
323
  # 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
-
324
+ if np.any(new_mask):
325
+ new_keys = keys[new_mask]
326
+ new_face_data = face_data[new_mask]
327
+ n_new = len(new_keys)
328
+
329
+ # Update dirty region for new faces
330
+ start_index = self.valid_f_top
331
+ # Insert data
332
+ self.faces[start_index: start_index + n_new] = new_face_data
333
+
275
334
  # Update key2index mapping for new faces
276
335
  for i, face_key in enumerate(new_keys):
277
- self.key2index[face_key] = self.valid_f_top + i
278
-
336
+ self.key2index[face_key] = start_index + i
279
337
  self.valid_f_top += n_new
338
+ self.dirty_min = min(self.dirty_min, start_index)
339
+ self.dirty_max = max(self.dirty_max, start_index + n_new)
340
+ self.need_update_buffer = True
280
341
 
281
342
  def _expand_face_buffer(self):
282
343
  """Expand the faces buffer when capacity is reached"""
@@ -288,6 +349,8 @@ class MeshItem(BaseItem):
288
349
  def clear_mesh(self):
289
350
  """Clear all mesh data and reset buffers"""
290
351
  self.valid_f_top = 0
352
+ self.dirty_min = self.BIG_INT
353
+ self.dirty_max = 0
291
354
  self.key2index.clear()
292
355
  if hasattr(self, 'indices_array'):
293
356
  self.indices_array = np.array([], dtype=np.uint32)
@@ -366,13 +429,27 @@ class MeshItem(BaseItem):
366
429
  glBindBuffer(GL_ARRAY_BUFFER, 0)
367
430
  self._gpu_face_capacity = len(self.faces)
368
431
 
369
- # 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)
432
+ # Upload faces to VBO (only dirty region)
433
+ if self.need_update_buffer:
434
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
435
+
436
+ # Calculate the range to upload [dirty_min, dirty_max)
437
+ start_index = int(self.dirty_min)
438
+ end_index = int(self.dirty_max)
439
+ count = end_index - start_index
440
+
441
+ # Upload only the modified region
442
+ glBufferSubData(GL_ARRAY_BUFFER,
443
+ start_index * 13 * 4, # offset in bytes
444
+ count * 13 * 4, # size in bytes
445
+ self.faces[start_index:end_index])
446
+
447
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
448
+ self.need_update_buffer = False
449
+
450
+ # Reset dirty region
451
+ self.dirty_min = self.BIG_INT
452
+ self.dirty_max = 0
376
453
 
377
454
  def update_setting(self):
378
455
  """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.5
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.5',
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