import sys import numpy as np from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget from PySide6.QtOpenGLWidgets import QOpenGLWidget from PySide6.QtCore import Qt, QPoint from OpenGL.GL import * from OpenGL.GLU import * ##testing def create_cube(scale=1): vertices = np.array([ [0, 0, 0], [2, 0, 0], [2, 2, 0], [0, 2, 0], [0, 0, 2], [2, 0, 2], [2, 2, 2], [0, 2, 2] ]) * scale faces = np.array([ [0, 1, 2], [2, 3, 0], [4, 5, 6], [6, 7, 4], [0, 1, 5], [5, 4, 0], [2, 3, 7], [7, 6, 2], [0, 3, 7], [7, 4, 0], [1, 2, 6], [6, 5, 1] ]) return vertices, faces class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("OpenGL Cube Viewer") self.setGeometry(100, 100, 800, 600) self.opengl_widget = OpenGLWidget() central_widget = QWidget() layout = QVBoxLayout() layout.addWidget(self.opengl_widget) central_widget.setLayout(layout) self.setCentralWidget(central_widget) # Load cube data vertices, faces = create_cube() self.opengl_widget.load_interactor_mesh((vertices, faces)) class OpenGLWidget(QOpenGLWidget): def __init__(self, parent=None): super().__init__(parent) self.vertices = None self.faces = None self.selected_face = -1 self.scale_factor = 1 self.mesh_loaded = None self.interactor_loaded = None self.centroid = None self.stl_file = "out.stl" # Replace with your STL file path self.lastPos = QPoint() self.startPos = None self.endPos = None self.xRot = 180 self.yRot = 0 self.zoom = -2 self.sketch = [] self.gl_width = self.width() self.gl_height = self.height() def map_value_to_range(self, value, value_min=0, value_max=1920, range_min=-1, range_max=1): value = max(value_min, min(value_max, value)) mapped_value = ((value - value_min) / (value_max - value_min)) * (range_max - range_min) + range_min return mapped_value def load_stl(self, filename: str) -> object: try: stl_mesh = mesh.Mesh.from_file(filename) # Extract vertices vertices = np.concatenate([stl_mesh.v0, stl_mesh.v1, stl_mesh.v2]) # Calculate bounding box min_x, min_y, min_z = vertices.min(axis=0) max_x, max_y, max_z = vertices.max(axis=0) # Calculate centroid centroid_x = (min_x + max_x) / 2.0 centroid_y = (min_y + max_y) / 2.0 centroid_z = (min_z + max_z) / 2.0 self.mesh_loaded = stl_mesh.vectors self.centroid = (centroid_x, centroid_y, centroid_z) except FileNotFoundError: print(f"Error: File {filename} not found.") except Exception as e: print(f"Error loading {filename}: {e}") return None, (0, 0, 0) def load_interactor_mesh(self, simp_mesh): self.interactor_loaded = simp_mesh # Calculate centroid based on the average position of vertices centroid = np.mean(simp_mesh[0], axis=0) self.centroid = tuple(centroid) print(f"Centroid: {self.centroid}") self.update() def load_mesh_direct(self, mesh): try: stl_mesh = mesh # Extract vertices vertices = np.array(stl_mesh) # Calculate centroid based on the average position of vertices centroid = np.mean(vertices, axis=0) self.mesh_loaded = vertices self.centroid = tuple(centroid) print(f"Centroid: {self.centroid}") self.update() except Exception as e: print(e) def clear_mesh(self): self.mesh_loaded = None def initializeGL(self): glClearColor(0, 0, 0, 1) glEnable(GL_DEPTH_TEST) def resizeGL(self, width, height): glViewport(0, 0, width, height) glMatrixMode(GL_PROJECTION) glLoadIdentity() aspect = width / float(height) self.gl_width = self.width() self.gl_height = self.height() gluPerspective(45.0, aspect, 0.01, 1000.0) glMatrixMode(GL_MODELVIEW) def unproject(self, x, y, z, modelview, projection, viewport): mvp = np.dot(projection, modelview) mvp_inv = np.linalg.inv(mvp) ndc = np.array([(x - viewport[0]) / viewport[2] * 2 - 1, (y - viewport[1]) / viewport[3] * 2 - 1, 2 * z - 1, 1]) world = np.dot(mvp_inv, ndc) print("world undproj", world) return world[:3] / world[3] def draw_ray(self, ray_start, ray_end): glColor3f(1.0, 0.0, 0.0) # Set the color of the ray (red) glBegin(GL_LINES) glVertex3f(*ray_start) glVertex3f(*ray_end) glEnd() def mousePressEvent(self, event): if event.buttons() & Qt.RightButton: self.select_face(event) def select_face(self, event): x = event.position().x() y = event.position().y() modelview = glGetDoublev(GL_MODELVIEW_MATRIX) projection = glGetDoublev(GL_PROJECTION_MATRIX) viewport = glGetIntegerv(GL_VIEWPORT) # Unproject near and far points in world space ray_start = gluUnProject(x, y, 0.0, modelview, projection, viewport) ray_end = gluUnProject(x, y, 1.0, modelview, projection, viewport) ray_start = np.array(ray_start) ray_end = np.array(ray_end) ray_direction = ray_end - ray_start ray_direction /= np.linalg.norm(ray_direction) print(f"Ray start: {ray_start}") print(f"Ray end: {ray_end}") print(f"Ray direction: {ray_direction}") self.selected_face = self.check_intersection(ray_start, ray_end) print(f"Selected face: {self.selected_face}") self.update() def ray_box_intersection(self, ray_origin, ray_direction, box_min, box_max): inv_direction = 1 / (ray_direction + 1e-7) # Add small value to avoid division by zero t1 = (box_min - ray_origin) * inv_direction t2 = (box_max - ray_origin) * inv_direction t_min = np.max(np.minimum(t1, t2)) t_max = np.min(np.maximum(t1, t2)) print(f"min: {t_min}, max: {t_max}" ) return t_max >= t_min and t_max > 0 def check_intersection(self, ray_start, ray_end): # Get the current modelview matrix modelview = glGetDoublev(GL_MODELVIEW_MATRIX) # Transform vertices to camera space vertices_cam = [np.dot(modelview, np.append(v, 1))[:3] for v in self.interactor_loaded[0]] ray_direction = ray_end - ray_start ray_direction /= np.linalg.norm(ray_direction) print(f"Checking intersection with {len(self.interactor_loaded[1])} faces") for face_idx, face in enumerate(self.interactor_loaded[1]): v0, v1, v2 = [vertices_cam[i] for i in face] intersection = self.moller_trumbore(ray_start, ray_direction, v0, v1, v2) if intersection is not None: print(f"Intersection found with face {face_idx}") return face_idx print("No intersection found") return None def moller_trumbore(self, ray_origin, ray_direction, v0, v1, v2): epsilon = 1e-6 # Find vectors for two edges sharing v0 edge1 = v1 - v0 edge2 = v2 - v0 pvec = np.cross(ray_direction, edge2) det = np.dot(edge1, pvec) print(det) """if det < epsilon: return None""" inv_det = 1.0 / det tvec = ray_origin - v0 u = np.dot(tvec, pvec) * inv_det print("u", u ) if u < 0.0 or u > 1.0: return None qvec = np.cross(tvec, edge1) # Calculate v parameter and test bounds v = np.dot(ray_direction, qvec) * inv_det print("v", v) if v < 0.0 or u + v > 1.0: return None # Calculate t, ray intersects triangle t = np.dot(edge2, qvec) * inv_det print("t",t) if t > epsilon: return ray_origin + t * ray_direction return None def ray_triangle_intersection(self, ray_origin, ray_direction, v0, v1, v2): epsilon = 1e-5 edge1 = v1 - v0 edge2 = v2 - v0 h = np.cross(ray_direction, edge2) a = np.dot(edge1, h) print(f"Triangle vertices: {v0}, {v1}, {v2}") print(f"a: {a}") if abs(a) < epsilon: print("Ray is parallel to the triangle") return None # Ray is parallel to the triangle f = 1.0 / a s = ray_origin - v0 u = f * np.dot(s, h) print(f"u: {u}") if u < 0.0 or u > 1.0: print("u is out of range") return None q = np.cross(s, edge1) v = f * np.dot(ray_direction, q) print(f"v: {v}") if v < 0.0 or u + v > 1.0: print("v is out of range") return None t = f * np.dot(edge2, q) print(f"t: {t}") if t > epsilon: intersection_point = ray_origin + t * ray_direction print(f"Intersection point: {intersection_point}") return intersection_point print("t is too small") return None def paintGL(self): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glMatrixMode(GL_MODELVIEW) glLoadIdentity() # Apply camera transformation glTranslatef(0, 0, self.zoom) glRotatef(self.xRot, 1.0, 0.0, 0.0) glRotatef(self.yRot, 0.0, 1.0, 0.0) """# Apply model transformation glTranslatef(self.tx, self.ty, self.tz) glScalef(self.scale, self.scale, self.scale) glRotatef(self.model_xRot, 1.0, 0.0, 0.0) glRotatef(self.model_yRot, 0.0, 1.0, 0.0) glRotatef(self.model_zRot, 0.0, 0.0, 1.0)""" glColor3f(0.9, 0.8, 0.8) self.draw_area() if self.mesh_loaded is not None: # Adjust the camera for the STL mesh if self.centroid: glPushMatrix() # Save current transformation matrix glScalef(self.scale_factor, self.scale_factor, self.scale_factor) # Apply scaling cx, cy, cz = self.centroid gluLookAt(cx, cy, cz + 100, cx, cy, cz, 0, 1, 0) self.draw_mesh_direct(self.mesh_loaded) glPopMatrix() # Restore transformation matrix if self.interactor_loaded is not None: # Draw interactor mesh glPushMatrix() # Save current transformation matrix glScalef(self.scale_factor, self.scale_factor, self.scale_factor) # Apply scaling self.draw_interactor(self.interactor_loaded) glPopMatrix() # Restore transformation matrix if self.selected_face is not None: glColor3f(0.0, 1.0, 0.0) # Red color for selected face glBegin(GL_TRIANGLES) for vertex_idx in self.interactor_loaded[1][self.selected_face]: glVertex3fv(self.interactor_loaded[0][vertex_idx]) glEnd() # Flush the OpenGL pipeline and swap buffers if hasattr(self, 'ray_start') and hasattr(self, 'ray_end'): self.draw_ray(self.ray_start, self.ray_end) glFlush() def draw_stl(self, vertices): glEnable(GL_LIGHTING) glEnable(GL_LIGHT0) glEnable(GL_DEPTH_TEST) glEnable(GL_COLOR_MATERIAL) glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE) glLightfv(GL_LIGHT0, GL_POSITION, (0, 1, 1, 0)) glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.6, 0.6, 0.6, 1.0)) glBegin(GL_TRIANGLES) for triangle in vertices: for vertex in triangle: glVertex3fv(vertex) glEnd() self.update() def draw_interactor(self, simp_mesh: tuple): vertices, faces = simp_mesh glEnable(GL_LIGHTING) glEnable(GL_LIGHT0) glEnable(GL_DEPTH_TEST) glEnable(GL_COLOR_MATERIAL) glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE) glLightfv(GL_LIGHT0, GL_POSITION, (0, 0.6, 0.6, 0)) glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.4, 0.4, 0.4, 0.6)) # Draw the faces glDisable(GL_LIGHTING) glColor3f(0.2, 0.0, 0.0) # Set face color to red (or any color you prefer) glBegin(GL_TRIANGLES) for face in faces: for vertex_index in face: glVertex3fv(vertices[vertex_index]) glEnd() # Draw the lines (edges of the triangles) glColor3f(0.0, 1.0, 0.0) # Set line color to green (or any color you prefer) glBegin(GL_LINES) for face in faces: for i in range(len(face)): glVertex3fv(vertices[face[i]]) glVertex3fv(vertices[face[(i + 1) % len(face)]]) glEnd() glEnable(GL_LIGHTING) # Re-enable lighting if further drawing requires it def draw_mesh_direct(self, points): glEnable(GL_LIGHTING) glEnable(GL_LIGHT0) glEnable(GL_DEPTH_TEST) glEnable(GL_COLOR_MATERIAL) glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE) glLightfv(GL_LIGHT0, GL_POSITION, (0, 0.6, 0.6, 0)) glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.4, 0.4, 0.4, 0.6)) glDisable(GL_LIGHTING) glBegin(GL_TRIANGLES) for vertex in points: glVertex3fv(vertex) glEnd() # Draw the lines (edges of the triangles) #glDisable(GL_LIGHTING) # Disable lighting to avoid affecting the line color glColor3f(0.0, 0.0, 0.0) # Set line color to black (or any color you prefer) glBegin(GL_LINES) for i in range(0, len(points), 3): glVertex3fv(points[i]) glVertex3fv(points[i + 1]) glVertex3fv(points[i + 1]) glVertex3fv(points[i + 2]) glVertex3fv(points[i + 2]) glVertex3fv(points[i]) glEnd() glEnable(GL_LIGHTING) # Re-enable lighting if further drawing requires it def draw_area(self): glColor3f(0.5, 0.5, 0.5) # Gray color glBegin(GL_LINES) for x in range(0, self.width(), 1): x_ndc = self.map_value_to_range(x, 0, value_max=self.width(), range_min=-self.gl_width, range_max=self.gl_width) glVertex2f(x_ndc, -self.gl_height) # Start from y = -1 glVertex2f(x_ndc, self.gl_height) # End at y = 1 for y in range(0, self.height(), 1): y_ndc = self.map_value_to_range(y, 0, value_max=self.height(), range_min=-self.gl_height, range_max=self.gl_height) glVertex2f(-self.gl_width, y_ndc) # Start from x = -1 glVertex2f(self.gl_width, y_ndc) # End at x = 1 glEnd() def mouseMoveEvent(self, event): dx = event.x() - self.lastPos.x() dy = event.y() - self.lastPos.y() if event.buttons() & Qt.MouseButton.LeftButton : self.xRot += 0.5 * dy self.yRot += 0.5 * dx self.lastPos = event.pos() self.update() def wheelEvent(self, event): delta = event.angleDelta().y() self.zoom += delta / 200 self.update() def aspect_ratio(self): return self.width() / self.height() * (1.0 / abs(self.zoom)) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())