q3dviewer 1.2.2__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 (46) hide show
  1. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/PKG-INFO +24 -11
  2. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/README.md +21 -9
  3. q3dviewer-1.2.4/q3dviewer/__init__.py +5 -0
  4. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/__init__.py +2 -0
  5. q3dviewer-1.2.4/q3dviewer/custom_items/mesh_item.py +487 -0
  6. q3dviewer-1.2.4/q3dviewer/custom_items/static_mesh_item.py +330 -0
  7. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/glwidget.py +26 -1
  8. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/cloud_viewer.py +67 -57
  9. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/film_maker.py +7 -8
  10. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/cloud_io.py +46 -19
  11. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/helpers.py +12 -0
  12. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/PKG-INFO +24 -11
  13. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/SOURCES.txt +1 -0
  14. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/requires.txt +0 -1
  15. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/setup.py +3 -2
  16. q3dviewer-1.2.2/q3dviewer/__init__.py +0 -14
  17. q3dviewer-1.2.2/q3dviewer/custom_items/mesh_item.py +0 -425
  18. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/Qt/__init__.py +0 -0
  19. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/base_glwidget.py +0 -0
  20. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/base_item.py +0 -0
  21. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/axis_item.py +0 -0
  22. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/cloud_io_item.py +0 -0
  23. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/cloud_item.py +0 -0
  24. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/frame_item.py +0 -0
  25. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/gaussian_item.py +0 -0
  26. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/grid_item.py +0 -0
  27. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/image_item.py +0 -0
  28. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/line_item.py +0 -0
  29. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/text3d_item.py +0 -0
  30. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/custom_items/text_item.py +0 -0
  31. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/__init__.py +0 -0
  32. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/example_viewer.py +0 -0
  33. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/gaussian_viewer.py +0 -0
  34. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/lidar_calib.py +0 -0
  35. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/lidar_cam_calib.py +0 -0
  36. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/tools/ros_viewer.py +0 -0
  37. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/__init__.py +0 -0
  38. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/convert_ros_msg.py +0 -0
  39. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/gl_helper.py +0 -0
  40. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/maths.py +0 -0
  41. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/utils/range_slider.py +0 -0
  42. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer/viewer.py +0 -0
  43. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/dependency_links.txt +0 -0
  44. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/entry_points.txt +0 -0
  45. {q3dviewer-1.2.2 → q3dviewer-1.2.4}/q3dviewer.egg-info/top_level.txt +0 -0
  46. {q3dviewer-1.2.2 → 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.2
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
+
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
5
+ Distributed under MIT license. See LICENSE for more information.
6
+ """
7
+
8
+
9
+ import numpy as np
10
+ from q3dviewer.base_item import BaseItem
11
+ from OpenGL.GL import *
12
+ from OpenGL.GL import shaders
13
+ from q3dviewer.Qt.QtWidgets import QLabel, QCheckBox, QDoubleSpinBox, QSlider, QHBoxLayout, QLineEdit
14
+
15
+ import os
16
+ from q3dviewer.utils import set_uniform, text_to_rgba
17
+ import time
18
+
19
+
20
+ class MeshItem(BaseItem):
21
+ """
22
+ A OpenGL mesh item for rendering 3D triangular meshes.
23
+ Attributes:
24
+ color (str or tuple): Accepts any valid matplotlib color (e.g., 'red', '#FF4500', (1.0, 0.5, 0.0)).
25
+ wireframe (bool): If True, renders the mesh in wireframe mode.
26
+ """
27
+ def __init__(self, color='lightblue', wireframe=False):
28
+ super(MeshItem, self).__init__()
29
+ self.wireframe = wireframe
30
+ self.color = color
31
+ self.flat_rgb = text_to_rgba(color, flat=True)
32
+
33
+ # Incremental buffer management
34
+ self.FACE_CAPACITY = 1000000 # Initial capacity for faces
35
+
36
+ # Faces buffer: N x 13 numpy array
37
+ # 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]
38
+ self.faces = np.zeros((self.FACE_CAPACITY, 13), dtype=np.float32)
39
+
40
+ # valid_f_top: pointer to end of valid faces
41
+ self.valid_f_top = 0
42
+
43
+ # key2index: mapping from face_key to face buffer index
44
+ self.key2index = {} # {face_key: face_index}
45
+
46
+ # OpenGL objects
47
+ self.vao = None
48
+ self.vbo = None
49
+ self.program = None
50
+ self._gpu_face_capacity = 0 # Track GPU buffer capacity
51
+
52
+ # Fixed rendering parameters (not adjustable via UI)
53
+ self.enable_lighting = True
54
+ self.line_width = 1.0
55
+ self.light_pos = [1.0, 1.0, 1.0]
56
+ self.light_color = [1.0, 1.0, 1.0]
57
+ self.ambient_strength = 0.1
58
+ self.diffuse_strength = 1.2
59
+ self.specular_strength = 0.1
60
+ self.shininess = 32.0
61
+ self.alpha = 1.0
62
+
63
+ # Settings flag
64
+ self.need_update_setting = True
65
+ self.need_update_buffer = True
66
+ self.path = os.path.dirname(__file__)
67
+
68
+
69
+ def add_setting(self, layout):
70
+ """Add UI controls for mesh visualization"""
71
+ # Only keep wireframe toggle - all other parameters are fixed
72
+ self.wireframe_box = QCheckBox("Wireframe Mode")
73
+ self.wireframe_box.setChecked(self.wireframe)
74
+ self.wireframe_box.toggled.connect(self.update_wireframe)
75
+ layout.addWidget(self.wireframe_box)
76
+
77
+ # Enable lighting toggle
78
+ self.lighting_box = QCheckBox("Enable Lighting")
79
+ self.lighting_box.setChecked(self.enable_lighting)
80
+ self.lighting_box.toggled.connect(self.update_enable_lighting)
81
+ layout.addWidget(self.lighting_box)
82
+
83
+ label_rgb = QLabel("Color:")
84
+ label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
85
+ layout.addWidget(label_rgb)
86
+ self.edit_rgb = QLineEdit()
87
+ self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
88
+ self.edit_rgb.setText(self.color)
89
+ self.edit_rgb.textChanged.connect(self._on_color)
90
+ layout.addWidget(self.edit_rgb)
91
+
92
+ # Material property controls for Phong lighting
93
+ if self.enable_lighting:
94
+ # Ambient strength control (slider 0-100 mapped to 0.0-1.0)
95
+ ambient_layout = QHBoxLayout()
96
+ ambient_label = QLabel("Ambient Strength:")
97
+ ambient_layout.addWidget(ambient_label)
98
+ self.ambient_slider = QSlider()
99
+ self.ambient_slider.setOrientation(1) # Qt.Horizontal
100
+ self.ambient_slider.setRange(0, 100)
101
+ self.ambient_slider.setValue(int(self.ambient_strength * 100))
102
+ self.ambient_slider.valueChanged.connect(lambda v: self.update_ambient_strength(v / 100.0))
103
+ ambient_layout.addWidget(self.ambient_slider)
104
+ layout.addLayout(ambient_layout)
105
+
106
+ # Diffuse strength control (slider 0-200 mapped to 0.0-2.0)
107
+ diffuse_layout = QHBoxLayout()
108
+ diffuse_label = QLabel("Diffuse Strength:")
109
+ diffuse_layout.addWidget(diffuse_label)
110
+ self.diffuse_slider = QSlider()
111
+ self.diffuse_slider.setOrientation(1)
112
+ self.diffuse_slider.setRange(0, 200)
113
+ self.diffuse_slider.setValue(int(self.diffuse_strength * 100))
114
+ self.diffuse_slider.valueChanged.connect(lambda v: self.update_diffuse_strength(v / 100.0))
115
+ diffuse_layout.addWidget(self.diffuse_slider)
116
+ layout.addLayout(diffuse_layout)
117
+
118
+ # Specular strength control (slider 0-200 mapped to 0.0-2.0)
119
+ specular_layout = QHBoxLayout()
120
+ specular_label = QLabel("Specular Strength:")
121
+ specular_layout.addWidget(specular_label)
122
+ self.specular_slider = QSlider()
123
+ self.specular_slider.setOrientation(1)
124
+ self.specular_slider.setRange(0, 200)
125
+ self.specular_slider.setValue(int(self.specular_strength * 100))
126
+ self.specular_slider.valueChanged.connect(lambda v: self.update_specular_strength(v / 100.0))
127
+ specular_layout.addWidget(self.specular_slider)
128
+ layout.addLayout(specular_layout)
129
+
130
+ # Shininess control (slider 1-256 mapped to 1-256)
131
+ shininess_layout = QHBoxLayout()
132
+ shininess_label = QLabel("Shininess:")
133
+ shininess_layout.addWidget(shininess_label)
134
+ self.shininess_slider = QSlider()
135
+ self.shininess_slider.setOrientation(1)
136
+ self.shininess_slider.setRange(1, 256)
137
+ self.shininess_slider.setValue(int(self.shininess))
138
+ self.shininess_slider.valueChanged.connect(lambda v: self.update_shininess(float(v)))
139
+ shininess_layout.addWidget(self.shininess_slider)
140
+ layout.addLayout(shininess_layout)
141
+
142
+ def _on_color(self, color):
143
+ try:
144
+ self.color = color
145
+ self.flat_rgb = text_to_rgba(color, flat=True)
146
+ self.need_update_setting = True
147
+ except ValueError:
148
+ pass
149
+
150
+ def update_wireframe(self, value):
151
+ self.wireframe = value
152
+
153
+ def update_enable_lighting(self, value):
154
+ self.enable_lighting = value
155
+ self.need_update_setting = True
156
+
157
+ def update_line_width(self, value):
158
+ self.line_width = value
159
+ self.need_update_setting = True
160
+
161
+ def update_ambient_strength(self, value):
162
+ self.ambient_strength = value
163
+ self.need_update_setting = True
164
+
165
+ def update_diffuse_strength(self, value):
166
+ self.diffuse_strength = value
167
+ self.need_update_setting = True
168
+
169
+ def update_specular_strength(self, value):
170
+ self.specular_strength = value
171
+ self.need_update_setting = True
172
+
173
+ def update_shininess(self, value):
174
+ self.shininess = value
175
+ self.need_update_setting = True
176
+
177
+ def update_alpha(self, value):
178
+ """Update mesh alpha (opacity)"""
179
+ self.alpha = float(value)
180
+ self.need_update_setting = True
181
+
182
+ def set_data(self, data):
183
+ """
184
+ Args:
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
189
+ """
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:
201
+ self.set_incremental_data(data)
202
+ return
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")
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
+ """
221
+ if not isinstance(data, np.ndarray):
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()
257
+ self.faces = faces
258
+ self.valid_f_top = num_faces
259
+ self.need_update_buffer = True
260
+
261
+
262
+ def set_incremental_data(self, fs):
263
+ """
264
+ Incrementally update mesh with new face data.
265
+ Args:
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
271
+ Updates:
272
+ - faces: updates existing faces or appends new ones
273
+ - key2index: tracks face_key -> face_index mapping
274
+ """
275
+ if fs is None or len(fs) == 0:
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")
280
+
281
+ # Ensure enough capacity in faces buffer
282
+ while self.valid_f_top + len(fs) > len(self.faces):
283
+ self._expand_face_buffer()
284
+
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
+
302
+ # Batch update existing faces
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]
307
+
308
+ # Batch insert new faces
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
+
317
+ # Update key2index mapping for new faces
318
+ for i, face_key in enumerate(new_keys):
319
+ self.key2index[face_key] = self.valid_f_top + i
320
+ self.valid_f_top += n_new
321
+ self.need_update_buffer = True
322
+
323
+ def _expand_face_buffer(self):
324
+ """Expand the faces buffer when capacity is reached"""
325
+ new_capacity = len(self.faces) + self.FACE_CAPACITY
326
+ new_buffer = np.zeros((new_capacity, 13), dtype=np.float32)
327
+ new_buffer[:len(self.faces)] = self.faces
328
+ self.faces = new_buffer
329
+
330
+ def clear_mesh(self):
331
+ """Clear all mesh data and reset buffers"""
332
+ self.valid_f_top = 0
333
+ self.key2index.clear()
334
+ if hasattr(self, 'indices_array'):
335
+ self.indices_array = np.array([], dtype=np.uint32)
336
+
337
+ def initialize_gl(self):
338
+ """OpenGL initialization"""
339
+ # Use instanced mesh shaders with geometry shader for GPU-side triangle generation
340
+ vert_shader = open(self.path + '/../shaders/mesh_vert.glsl', 'r').read()
341
+ geom_shader = open(self.path + '/../shaders/mesh_geom.glsl', 'r').read()
342
+ frag_shader = open(self.path + '/../shaders/mesh_frag.glsl', 'r').read()
343
+ try:
344
+ program = shaders.compileProgram(
345
+ shaders.compileShader(vert_shader, GL_VERTEX_SHADER),
346
+ shaders.compileShader(geom_shader, GL_GEOMETRY_SHADER),
347
+ shaders.compileShader(frag_shader, GL_FRAGMENT_SHADER),
348
+ )
349
+ self.program = program
350
+ except Exception as e:
351
+ raise
352
+
353
+ def update_render_buffer(self):
354
+ """
355
+ Update GPU buffer with face data (no separate vertex buffer).
356
+ Each face contains embedded vertex positions (13 floats).
357
+ Geometry shader generates triangles on GPU from face vertices.
358
+ Dynamically resizes GPU buffer when Python buffer expands.
359
+ """
360
+ if self.valid_f_top == 0:
361
+ return
362
+
363
+ # Initialize buffers on first call
364
+ if self.vao is None:
365
+ self.vao = glGenVertexArrays(1)
366
+ self.vbo = glGenBuffers(1)
367
+ self._gpu_face_capacity = 0
368
+
369
+ # Check if we need to reallocate VBO for faces
370
+ if self._gpu_face_capacity < len(self.faces):
371
+ glBindVertexArray(self.vao)
372
+ glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
373
+ glBufferData(GL_ARRAY_BUFFER,
374
+ self.faces.nbytes,
375
+ None,
376
+ GL_DYNAMIC_DRAW)
377
+
378
+ # Setup face attributes (per-instance)
379
+ # Face data: [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]
380
+ # 13 floats = 52 bytes stride
381
+
382
+ # v0 (location 1) - vec3
383
+ glEnableVertexAttribArray(1)
384
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(0))
385
+ glVertexAttribDivisor(1, 1)
386
+
387
+ # v1 (location 2) - vec3
388
+ glEnableVertexAttribArray(2)
389
+ glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(12))
390
+ glVertexAttribDivisor(2, 1)
391
+
392
+ # v2 (location 3) - vec3
393
+ glEnableVertexAttribArray(3)
394
+ glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(24))
395
+ glVertexAttribDivisor(3, 1)
396
+
397
+ # v3 (location 4) - vec3
398
+ glEnableVertexAttribArray(4)
399
+ glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(36))
400
+ glVertexAttribDivisor(4, 1)
401
+
402
+ # good flag (location 5) - float
403
+ glEnableVertexAttribArray(5)
404
+ glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, 52, ctypes.c_void_p(48))
405
+ glVertexAttribDivisor(5, 1)
406
+
407
+ glBindVertexArray(0)
408
+ glBindBuffer(GL_ARRAY_BUFFER, 0)
409
+ self._gpu_face_capacity = len(self.faces)
410
+
411
+ # Upload faces to VBO
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)
421
+
422
+ def update_setting(self):
423
+ """Set fixed rendering parameters (called once during initialization)"""
424
+ if not self.need_update_setting:
425
+ return
426
+ # Set fixed uniforms for instanced shaders
427
+ set_uniform(self.program, int(self.enable_lighting), 'if_light')
428
+ set_uniform(self.program, 1, 'two_sided')
429
+
430
+ set_uniform(self.program, np.array(self.light_color, dtype=np.float32), 'light_color')
431
+ set_uniform(self.program, float(self.ambient_strength), 'ambient_strength')
432
+ set_uniform(self.program, float(self.diffuse_strength), 'diffuse_strength')
433
+ set_uniform(self.program, float(self.specular_strength), 'specular_strength')
434
+ set_uniform(self.program, float(self.shininess), 'shininess')
435
+ set_uniform(self.program, float(self.alpha), 'alpha')
436
+ set_uniform(self.program, int(self.flat_rgb), 'flat_rgb')
437
+ self.need_update_setting = False
438
+
439
+ def paint(self):
440
+ """
441
+ Render the mesh using instanced rendering with geometry shader.
442
+ Each face instance is rendered as a point, geometry shader generates 2 triangles.
443
+ GPU filters faces based on good flag.
444
+ """
445
+ if self.valid_f_top == 0:
446
+ return
447
+
448
+ glUseProgram(self.program)
449
+
450
+ self.update_render_buffer()
451
+ self.update_setting()
452
+
453
+ view_matrix = self.glwidget().view_matrix
454
+ set_uniform(self.program, view_matrix, 'view')
455
+ project_matrix = self.glwidget().projection_matrix
456
+ set_uniform(self.program, project_matrix, 'projection')
457
+ view_pos = self.glwidget().center
458
+ set_uniform(self.program, np.array(view_pos), 'view_pos')
459
+
460
+ # Enable blending and depth testing
461
+ glEnable(GL_BLEND)
462
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
463
+ glEnable(GL_DEPTH_TEST)
464
+ glDisable(GL_CULL_FACE) # two-sided rendering
465
+
466
+ # Set line width
467
+ glLineWidth(self.line_width)
468
+
469
+ # Bind VAO (vertex positions are now in VBO attributes)
470
+ glBindVertexArray(self.vao)
471
+
472
+ if self.wireframe:
473
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
474
+ else:
475
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
476
+
477
+ # Draw using instanced rendering
478
+ # Input: POINTS (one per face instance)
479
+ # Geometry shader generates 2 triangles (6 vertices) per point
480
+ glDrawArraysInstanced(GL_POINTS, 0, 1, self.valid_f_top)
481
+
482
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
483
+ glBindVertexArray(0)
484
+ glDisable(GL_DEPTH_TEST)
485
+ glDisable(GL_BLEND)
486
+ glUseProgram(0)
487
+