q3dviewer 1.2.3__py3-none-any.whl → 1.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
q3dviewer/__init__.py CHANGED
@@ -1,14 +1,5 @@
1
- import time
2
- t1 = time.time()
3
1
  from q3dviewer.custom_items import *
4
- t2 = time.time()
5
2
  from q3dviewer.glwidget import *
6
3
  from q3dviewer.viewer import *
7
4
  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)
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)
@@ -0,0 +1,66 @@
1
+ #version 430 core
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
+ layout(points) in;
9
+ layout(triangle_strip, max_vertices = 3) out;
10
+
11
+ // Input from vertex shader
12
+ in VS_OUT {
13
+ vec3 v0, v1, v2;
14
+ } gs_in[];
15
+
16
+ // Uniforms
17
+ uniform mat4 view;
18
+ uniform mat4 projection;
19
+
20
+ uniform int flat_rgb;
21
+
22
+ // Output to fragment shader
23
+ out vec3 FragPos;
24
+ out vec3 Normal;
25
+ out vec3 objectColor;
26
+
27
+ // Calculate normal from three vertices
28
+ vec3 calculateNormal(vec3 a, vec3 b, vec3 c) {
29
+ vec3 edge1 = b - a;
30
+ vec3 edge2 = c - a;
31
+ return normalize(cross(edge1, edge2));
32
+ }
33
+
34
+ void emitVertex(vec3 pos, vec3 normal, vec3 color) {
35
+ FragPos = pos;
36
+ Normal = normal;
37
+ objectColor = color;
38
+ gl_Position = projection * view * vec4(pos, 1.0);
39
+ EmitVertex();
40
+ }
41
+
42
+ void main()
43
+ {
44
+ vec3 v0 = gs_in[0].v0;
45
+ vec3 v1 = gs_in[0].v1;
46
+ vec3 v2 = gs_in[0].v2;
47
+
48
+ float eps = 0.0001;
49
+
50
+ // Use color from uniform
51
+ vec3 color = vec3(
52
+ float((uint(flat_rgb) & uint(0x00FF0000)) >> 16)/255.,
53
+ float((uint(flat_rgb) & uint(0x0000FF00)) >> 8)/255.,
54
+ float( uint(flat_rgb) & uint(0x000000FF))/255.
55
+ );
56
+
57
+ // Triangle: (v0, v1, v2)
58
+ vec3 normal = calculateNormal(v0, v1, v2);
59
+ // Skip degenerate triangles
60
+ if (length(normal) > eps) {
61
+ emitVertex(v0, normal, color);
62
+ emitVertex(v1, normal, color);
63
+ emitVertex(v2, normal, color);
64
+ EndPrimitive();
65
+ }
66
+ }
@@ -0,0 +1,31 @@
1
+ #version 430 core
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
+ // Triangle face attributes (per-instance) - 3 vertex positions
9
+ layout(location = 1) in vec3 v0;
10
+ layout(location = 2) in vec3 v1;
11
+ layout(location = 3) in vec3 v2;
12
+
13
+ // Uniforms
14
+ uniform mat4 view;
15
+ uniform mat4 projection;
16
+
17
+ // Outputs to fragment shader (via geometry shader)
18
+ out VS_OUT {
19
+ vec3 v0, v1, v2;
20
+ } vs_out;
21
+
22
+ void main()
23
+ {
24
+ // Pass vertex positions directly (no SSBO lookup needed)
25
+ vs_out.v0 = v0;
26
+ vs_out.v1 = v1;
27
+ vs_out.v2 = v2;
28
+
29
+ // Output dummy point (geometry shader will generate triangles)
30
+ gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
31
+ }
@@ -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
  Platform: UNKNOWN
9
10
  Classifier: Programming Language :: Python :: 3
10
11
  Classifier: License :: OSI Approved :: MIT License
@@ -74,22 +75,34 @@ python3 -m q3dviewer.tools.cloud_viewer
74
75
  ```
75
76
 
76
77
  **Basic Operations**
77
- * Load files: Drag and drop point cloud files onto the window (multiple files are OK).
78
- * `M` key: Display the visualization settings screen for point clouds, background color, etc.
79
- * `Left mouse button` & `W, A, S, D` keys: Move the viewpoint on the horizontal plane.
80
- * `Z, X` keys: Move in the direction the screen is facing.
81
- * `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the screen center unchanged.
82
- * `Shift` + `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the camera position unchanged.
78
+
79
+ 📁 **Load Files** - Drag and drop files into the viewer
80
+ * Point clouds: .pcd, .ply, .las, .e57
81
+ * Mesh files: .stl
82
+
83
+ 📏 **Measure Distance** - Interactive point measurement
84
+ * `Ctrl + Left Click`: Add measurement point
85
+ * `Ctrl + Right Click`: Remove last point
86
+ * Total distance displayed automatically
87
+
88
+ 🎥 **Camera Controls** - Navigate the 3D scene
89
+ * `Double Click`: Set camera center to point
90
+ * `Right Drag`: Rotate view
91
+ * `Left Drag`: Pan view
92
+ * `Mouse Wheel`: Zoom in/out
93
+
94
+ ⚙️ **Settings** - Press `M` to open settings window
95
+ * Adjust visualization properties
83
96
 
84
97
  For example, you can download and view point clouds of Tokyo in LAS format from the following link:
85
98
 
86
99
  [Tokyo Point Clouds](https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024/resource/7807d6d1-29f3-4b36-b0c8-f7aa0ea2cff3)
87
100
 
88
- ![Cloud Viewer Screenshot](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/03c981c6-1aec-e5b9-4536-e07e1e56ff29.png)
101
+ ![Cloud Viewer Screenshot](imgs/tokyo.png)
89
102
 
90
- 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`.
103
+ **Mesh Support** - Starting from version 1.2.4, mesh files (.stl) are now supported.
91
104
 
92
- ![Cloud Viewer Settings](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/deeb996a-e419-58f4-6bc2-535099b1b73a.png)
105
+ ![Screenshot from 2026-02-04 18-32-04.png](imgs/mesh.png)
93
106
 
94
107
  ### 2. ROS Viewer
95
108
 
@@ -1,10 +1,10 @@
1
- q3dviewer/__init__.py,sha256=cjyfUE5zK6xohDGDQIWfb0DKkWChVznBd7CrVLg7whQ,376
1
+ q3dviewer/__init__.py,sha256=Nz48lhIxsGYMLvhOLQT0AXhn6d64wA4hdIz4d0qsVLc,172
2
2
  q3dviewer/base_glwidget.py,sha256=QxAuZzQSBbzTwpHHqYpiM-Jqv41E4YJmFG4KRF-HruY,15274
3
3
  q3dviewer/base_item.py,sha256=63MarHyoWszPL40ox-vPoOAQ1N4ypekOjoRARdPik-E,1755
4
4
  q3dviewer/glwidget.py,sha256=taewDTQUqmKOrDYz_ygghu00dlCnWzFRpDnEaTG25pA,6390
5
5
  q3dviewer/viewer.py,sha256=Vq3ucDlBcBBoiVVGmqG1sRjhLePl50heblx6wJpsc1A,2603
6
6
  q3dviewer/Qt/__init__.py,sha256=VJj7Ge6N_81__T9eHFl_YQpa1HyQrlLhMqC_9pUOYtc,2233
7
- q3dviewer/custom_items/__init__.py,sha256=kaaf84wOObfybJ8a12FqPMeg8ImTJWggA6g5nvpY2YY,621
7
+ q3dviewer/custom_items/__init__.py,sha256=BXZzyOnwwB81pthx39n6issyroaxUxpCJPeW2U-TaWU,689
8
8
  q3dviewer/custom_items/axis_item.py,sha256=-WM2urosqV847zpTpOtxdLjb7y9NJqFCH13qqodcCTg,2572
9
9
  q3dviewer/custom_items/cloud_io_item.py,sha256=Haz-SOUUCPDSHgmKyyyFfP7LXBSEiN4r8xmchQwCm-k,4721
10
10
  q3dviewer/custom_items/cloud_item.py,sha256=UzpkWiMYcX8kEndmRV1ScysFfOvElaKo60KV-uEO2A4,13484
@@ -13,7 +13,8 @@ q3dviewer/custom_items/gaussian_item.py,sha256=JMubpahkTPh0E8ShL3FLTahv0e35ODzjg
13
13
  q3dviewer/custom_items/grid_item.py,sha256=LDB_MYACoxld-xvz01_MfAf12vLcRkH7R_WtGHHdSgk,4945
14
14
  q3dviewer/custom_items/image_item.py,sha256=k7HNTqdL2ckTbxMx7A7eKaP4aksZ85-pBjNdbpm6PXM,5355
15
15
  q3dviewer/custom_items/line_item.py,sha256=rel-lx8AgjDY7qyIecHxHQZzaswRn2ZTiOIjB_0Mrqo,4444
16
- q3dviewer/custom_items/mesh_item.py,sha256=L6GzCcxiAkeYMLBUbbTvQgY0s-maCYQZqZqeMDtxuFE,17568
16
+ q3dviewer/custom_items/mesh_item.py,sha256=L8-EoiJMH8lXmSdLOFYQSF9biODzSC616AMY6KgVGMk,19807
17
+ q3dviewer/custom_items/static_mesh_item.py,sha256=MQwlEMuYX0wB5odmeiw1nsBhciWs1jcauUvcWRDVj4Q,12882
17
18
  q3dviewer/custom_items/text3d_item.py,sha256=DYBPXnCmMEzWDE1y523YsWSl91taXAdu0kdnhUcwE4A,5524
18
19
  q3dviewer/custom_items/text_item.py,sha256=toeGjBu7RtT8CMUuaDWnmXPnA1UKHhnCzUNeonGczSo,2703
19
20
  q3dviewer/shaders/cloud_frag.glsl,sha256=psKVt9qI6BW0bCqOk4lcKqUd6XgYGtdFigyN9OdYSNI,609
@@ -25,10 +26,12 @@ q3dviewer/shaders/mesh_frag.glsl,sha256=i9ljnO2kjLNGaR1TPQIK4-4iJ-JppJ5bCsOHg173
25
26
  q3dviewer/shaders/mesh_geom.glsl,sha256=HwNEZy7UAQm-PKEP-LnqHTlsSqZ5eqm49CYZSVylzXk,1964
26
27
  q3dviewer/shaders/mesh_vert.glsl,sha256=bFg7HXesdVg7UaGDuZ4g7IpRdSKcfc6jzTcxLjNXkt8,968
27
28
  q3dviewer/shaders/sort_by_key.glsl,sha256=M5RK6uRDp40vVH6XtBIrdJTcYatqXyZwd6kCzEa2DZg,1097
29
+ q3dviewer/shaders/triangle_mesh_geom.glsl,sha256=Uu9WUVbObAUBrtWamQ3trXbmi-Yn9HKWIJPICdjHv_4,1513
30
+ q3dviewer/shaders/triangle_mesh_vert.glsl,sha256=-VTbZ3zZpDI1krMXp7dY0Dz198XqEu7v6lJttO5jQZo,741
28
31
  q3dviewer/tools/__init__.py,sha256=01wG7BGM6VX0QyFBKsqPmyf2e-vrmV_N3-mo-VQ1VBg,20
29
- q3dviewer/tools/cloud_viewer.py,sha256=wjgQNrn9IzHXd8V2Y45ANm12_YZRaV3uL7F8HkylrN0,8185
32
+ q3dviewer/tools/cloud_viewer.py,sha256=94RABpCIn8sATw7I0l7f-Za1j8ulw2io2GICZ7yDRos,8221
30
33
  q3dviewer/tools/example_viewer.py,sha256=C867mLnCBjawS6LGgRsJ_c6-6wztfL9vOBQt85KbbdU,572
31
- q3dviewer/tools/film_maker.py,sha256=xLFgRhFWoMQ37qlvcu1lXWaTWXMNRYlRcZFfHW5JtmQ,16676
34
+ q3dviewer/tools/film_maker.py,sha256=v5En1rm8CS4I7J1mdtvy6AXrkGGOXuDxfvTBuvnLPMs,16640
32
35
  q3dviewer/tools/gaussian_viewer.py,sha256=vIwWmiFhjNmknrEkBLzt2yiegeH7LP3OeNjnGM6GzaI,1633
33
36
  q3dviewer/tools/lidar_calib.py,sha256=hHnsSaQh_Pkdh8tPntt0MgEW26nQyAdC_HQHq4I3sw8,10562
34
37
  q3dviewer/tools/lidar_cam_calib.py,sha256=4CDcZZiFZDeKo2Y2_lXF9tfbiF9dPsz0OjppQdxQsU4,11430
@@ -37,12 +40,12 @@ q3dviewer/utils/__init__.py,sha256=dwTNAAebTiKY4ygv2G1O-w6-TbJnmnNVO2UfJXvJhaQ,1
37
40
  q3dviewer/utils/cloud_io.py,sha256=OLmVQWbrnGrlZHPz3zdoVn79r50JhM6V0zV-KwogEU8,13732
38
41
  q3dviewer/utils/convert_ros_msg.py,sha256=lNbLIawJfwp3VzygdW3dUXkfSG8atg_CoZbQFmt8H70,3142
39
42
  q3dviewer/utils/gl_helper.py,sha256=dRY_kUqyPMr7NTcupUr6_VTvgnj53iE2C0Lk0-oFYsI,1435
40
- q3dviewer/utils/helpers.py,sha256=SqR4YTQZi13FKbkVUYgodXce1JJ_YmrHEIRkUmnIUas,3085
43
+ q3dviewer/utils/helpers.py,sha256=LMKm7R1OvBLoigxqTNN9pS1UlTw0865SFnMY7_IbgX8,3350
41
44
  q3dviewer/utils/maths.py,sha256=zHaPtvVZIuo8xepIXCMeSL9tpx8FahUrq0l4K1oXrBk,8834
42
45
  q3dviewer/utils/range_slider.py,sha256=Cs_xrwt6FCDVxGxan7r-ARd5ySwQ50xnCzcmz0dB_X0,4215
43
- q3dviewer-1.2.3.dist-info/LICENSE,sha256=81cMOyNfw8KLb1JnPYngGHJ5W83gSbZEBU9MEP3tl-E,1124
44
- q3dviewer-1.2.3.dist-info/METADATA,sha256=JjSE7g9HjmxRfl18Z79_EA90drP_EKhBVWuiMYk7yNM,8024
45
- q3dviewer-1.2.3.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
46
- q3dviewer-1.2.3.dist-info/entry_points.txt,sha256=EOjker7XYaBk70ffvNB_knPcfA33Bnlg21ZjEeM1EyI,362
47
- q3dviewer-1.2.3.dist-info/top_level.txt,sha256=HFFDCbGu28txcGe2HPc46A7EPaguBa_b5oH7bufmxHM,10
48
- q3dviewer-1.2.3.dist-info/RECORD,,
46
+ q3dviewer-1.2.4.dist-info/LICENSE,sha256=81cMOyNfw8KLb1JnPYngGHJ5W83gSbZEBU9MEP3tl-E,1124
47
+ q3dviewer-1.2.4.dist-info/METADATA,sha256=C89drDXpYSfUoEd_x005QqHp7Moc-ziC0vuo1XkWSu0,7776
48
+ q3dviewer-1.2.4.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
49
+ q3dviewer-1.2.4.dist-info/entry_points.txt,sha256=EOjker7XYaBk70ffvNB_knPcfA33Bnlg21ZjEeM1EyI,362
50
+ q3dviewer-1.2.4.dist-info/top_level.txt,sha256=HFFDCbGu28txcGe2HPc46A7EPaguBa_b5oH7bufmxHM,10
51
+ q3dviewer-1.2.4.dist-info/RECORD,,