diff --git a/drawing_modules/draw_widget2d.py b/drawing_modules/draw_widget2d.py index f7435ed..b7dc005 100644 --- a/drawing_modules/draw_widget2d.py +++ b/drawing_modules/draw_widget2d.py @@ -635,8 +635,10 @@ class SketchWidget(QWidget): return self.width() / self.height() * (1.0 / abs(self.zoom)) def clear_sketch(self): - self.points = [] - self.update() + self.slv_points_main = [] + self.slv_lines_main = [] + self.reset_buffers() + self.solv = SolverSystem() # Example usage diff --git a/drawing_modules/gl_widget.py b/drawing_modules/gl_widget.py index 2a0ea4c..6bfe56e 100644 --- a/drawing_modules/gl_widget.py +++ b/drawing_modules/gl_widget.py @@ -1,15 +1,71 @@ +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 * -from stl import mesh + +##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.scale_factor = 0.001 + 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() @@ -19,13 +75,13 @@ class OpenGLWidget(QOpenGLWidget): self.yRot = 0 self.zoom = -2 self.sketch = [] - self.gl_width = self.width() / 100 - self.gl_height = self.height() / 100 + 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: @@ -46,14 +102,23 @@ class OpenGLWidget(QOpenGLWidget): 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: @@ -83,40 +148,236 @@ class OpenGLWidget(QOpenGLWidget): glViewport(0, 0, width, height) glMatrixMode(GL_PROJECTION) glLoadIdentity() + aspect = width / float(height) - self.gl_width = self.width() / 1000 - self.gl_height = self.height() / 1000 + self.gl_width = self.width() + self.gl_height = self.height() - gluPerspective(45.0, aspect, 0.01, 10.0) + 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) - glColor3f(0.9, 0.8, 0.8) + """# 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 + # 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) - else: - glClearColor(0.0, 0.0, 0.0, 1.0) # Set the clear color (black with full opacity) - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # Clear the color and depth buffers + 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) @@ -135,6 +396,40 @@ class OpenGLWidget(QOpenGLWidget): 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) @@ -169,23 +464,21 @@ class OpenGLWidget(QOpenGLWidget): 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(), 20): + 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(), 20): + 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() @@ -204,3 +497,8 @@ class OpenGLWidget(QOpenGLWidget): 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()) \ No newline at end of file diff --git a/drawing_modules/vtk_widget.py b/drawing_modules/vtk_widget.py new file mode 100644 index 0000000..46656dc --- /dev/null +++ b/drawing_modules/vtk_widget.py @@ -0,0 +1,447 @@ +import sys + +import numpy as np +import vtk +from PySide6 import QtCore, QtWidgets +from PySide6.QtCore import Signal +from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor + + +class VTKWidget(QtWidgets.QWidget): + face_data = Signal(dict) + + def __init__(self, parent=None): + super().__init__(parent) + self.vtk_widget = QVTKRenderWindowInteractor(self) + + # Create layout and add VTK widget + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.vtk_widget) + self.setLayout(layout) + + # Create VTK pipeline + self.renderer = vtk.vtkRenderer() + self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer) + self.interactor = self.vtk_widget.GetRenderWindow().GetInteractor() + + # Set up the camera + self.camera = self.renderer.GetActiveCamera() + self.camera.SetPosition(5, 5, 5) + self.camera.SetFocalPoint(0, 0, 0) + + # Set up picking + self.picker = vtk.vtkCellPicker() + self.picker.SetTolerance(0.0005) + + # Create a mapper and actor for picked cells + self.picked_mapper = vtk.vtkDataSetMapper() + self.picked_actor = vtk.vtkActor() + self.picked_actor.SetMapper(self.picked_mapper) + self.picked_actor.GetProperty().SetColor(1.0, 0.0, 0.0) # Red color for picked faces + self.renderer.AddActor(self.picked_actor) + + # Set up interactor style + self.style = vtk.vtkInteractorStyleTrackballCamera() + self.interactor.SetInteractorStyle(self.style) + + # Add observer for mouse clicks + self.interactor.AddObserver("RightButtonPressEvent", self.on_click) + + def create_cube_mesh(self): + cube_source = vtk.vtkCubeSource() + print(cube_source) + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputConnection(cube_source.GetOutputPort()) + actor = vtk.vtkActor() + actor.SetMapper(mapper) + self.renderer.AddActor(actor) + + def simplify_mesh(self, input_mesh, target_reduction): + # Create the quadric decimation filter + decimate = vtk.vtkDecimatePro() + decimate.SetInputData(input_mesh) + + # Set the reduction factor (0 to 1, where 1 means maximum reduction) + decimate.SetTargetReduction(target_reduction) + + # Optional: Preserve topology (if needed) + decimate.PreserveTopologyOn() + + # Perform the decimation + decimate.Update() + + return decimate.GetOutput() + + def combine_coplanar_faces(self, input_polydata, tolerance=0.001): + # Clean the polydata to merge duplicate points + clean = vtk.vtkCleanPolyData() + clean.SetInputData(input_polydata) + clean.SetTolerance(tolerance) + clean.Update() + + # Generate normals and merge coplanar polygons + normals = vtk.vtkPolyDataNormals() + normals.SetInputConnection(clean.GetOutputPort()) + normals.SplittingOff() # Disable splitting of sharp edges + normals.ConsistencyOn() # Ensure consistent polygon ordering + normals.AutoOrientNormalsOn() # Automatically orient normals + normals.ComputePointNormalsOff() # We only need face normals + normals.ComputeCellNormalsOn() # Compute cell normals + normals.Update() + + return normals.GetOutput() + + def poisson_reconstruction(self, points): + # Create a polydata object from points + point_polydata = vtk.vtkPolyData() + point_polydata.SetPoints(points) + + # Create a surface reconstruction filter + surf = vtk.vtkSurfaceReconstructionFilter() + surf.SetInputData(point_polydata) + surf.Update() + + # Create a contour filter to extract the surface + cf = vtk.vtkContourFilter() + cf.SetInputConnection(surf.GetOutputPort()) + cf.SetValue(0, 0.0) + cf.Update() + + # Reverse normals + reverse = vtk.vtkReverseSense() + reverse.SetInputConnection(cf.GetOutputPort()) + reverse.ReverseCellsOn() + reverse.ReverseNormalsOn() + reverse.Update() + + return reverse.GetOutput() + + def load_interactor_mesh(self, simp_mesh): + vertices, faces = simp_mesh + self.load_custom_mesh(vertices, faces) + + def load_custom_mesh(self, vertices, faces): + ### Load meshes by own module + # Create a vtkPoints object and store the points in it + points = vtk.vtkPoints() + for vertex in vertices: + points.InsertNextPoint(vertex) + + # Create a vtkCellArray to store the faces + cells = vtk.vtkCellArray() + for face in faces: + triangle = vtk.vtkTriangle() + triangle.GetPointIds().SetId(0, face[0]) + triangle.GetPointIds().SetId(1, face[1]) + triangle.GetPointIds().SetId(2, face[2]) + cells.InsertNextCell(triangle) + + # Create a polydata object + polydata = vtk.vtkPolyData() + polydata.SetPoints(points) + polydata.SetPolys(cells) + + # Create mapper and actor + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(polydata) # Make sure this line is present + actor = vtk.vtkActor() + actor.SetMapper(mapper) + + # Add to renderer + self.renderer.AddActor(actor) + + # Force an update of the pipeline + mapper.Update() + self.vtk_widget.GetRenderWindow().Render() + + def create_simplified_outline(self, polydata): + # 1. Extract the outer surface + surface_filter = vtk.vtkDataSetSurfaceFilter() + surface_filter.SetInputData(polydata) + surface_filter.Update() + + # 2. Extract feature edges (only boundary edges) + feature_edges = vtk.vtkFeatureEdges() + feature_edges.SetInputConnection(surface_filter.GetOutputPort()) + feature_edges.BoundaryEdgesOn() + feature_edges.FeatureEdgesOff() + feature_edges.NonManifoldEdgesOff() + feature_edges.ManifoldEdgesOff() + feature_edges.Update() + + # 3. Clean the edges to merge duplicate points + cleaner = vtk.vtkCleanPolyData() + cleaner.SetInputConnection(feature_edges.GetOutputPort()) + cleaner.Update() + + # 4. Optional: Smooth the outline + smooth = vtk.vtkSmoothPolyDataFilter() + smooth.SetInputConnection(cleaner.GetOutputPort()) + smooth.SetNumberOfIterations(15) + smooth.SetRelaxationFactor(0.1) + smooth.FeatureEdgeSmoothingOff() + smooth.BoundarySmoothingOn() + smooth.Update() + + return smooth + + def render_from_points_direct_with_faces(self, points): + # Create a vtkPoints object and store the points in it + vtk_points = vtk.vtkPoints() + for point in points: + vtk_points.InsertNextPoint(point) + + # Create a vtkCellArray to store the triangles + triangles = vtk.vtkCellArray() + + # Assuming points are organized as triplets forming triangles + for i in range(0, len(points), 3): + triangle = vtk.vtkTriangle() + triangle.GetPointIds().SetId(0, i) + triangle.GetPointIds().SetId(1, i + 1) + triangle.GetPointIds().SetId(2, i + 2) + triangles.InsertNextCell(triangle) + + # Create a polydata object + polydata = vtk.vtkPolyData() + polydata.SetPoints(vtk_points) + polydata.SetPolys(triangles) + + # Optional: Merge duplicate points + cleaner = vtk.vtkCleanPolyData() + cleaner.SetInputData(polydata) + cleaner.Update() + + # Optional: Combine coplanar faces + normals = vtk.vtkPolyDataNormals() + normals.SetInputConnection(cleaner.GetOutputPort()) + normals.SplittingOff() + normals.ConsistencyOn() + normals.AutoOrientNormalsOn() + normals.ComputePointNormalsOff() + normals.ComputeCellNormalsOn() + normals.Update() + + # Create a mapper and actor + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputConnection(normals.GetOutputPort()) + + actor = vtk.vtkActor() + actor.SetMapper(mapper) + actor.GetProperty().SetColor(1, 1, 1) # Set color (white in this case) + actor.GetProperty().EdgeVisibilityOn() # Show edges + actor.GetProperty().SetLineWidth(2) # Set line width + + feature_edges = self.create_simplified_outline(polydata) + + # Create a mapper for the feature edges + edge_mapper = vtk.vtkPolyDataMapper() + # Already wiht output + edge_mapper.SetInputConnection(feature_edges.GetOutputPort()) + + # Create an actor for the feature edges + edge_actor = vtk.vtkActor() + edge_actor.SetMapper(edge_mapper) + + # Set the properties of the edge actor + edge_actor.GetProperty().SetColor(1, 0, 0) # Set color (red in this case) + edge_actor.GetProperty().SetLineWidth(2) # Set line width + + # Optionally, if you want to keep the original mesh visible: + # (assuming you have the original mesh mapper and actor set up) + self.renderer.AddActor(actor) # Add the original mesh actor + # Add the edge actor to the renderer + self.renderer.AddActor(edge_actor) + + # Force an update of the pipeline + mapper.Update() + self.vtk_widget.GetRenderWindow().Render() + + # Print statistics + print(f"Original points: {len(points)}") + print(f"Number of triangles: {triangles.GetNumberOfCells()}") + print(f"Final number of points: {normals.GetOutput().GetNumberOfPoints()}") + print(f"Final number of cells: {normals.GetOutput().GetNumberOfCells()}") + + def render_from_points_direct(self, points): + ### Rendermethod for SDF mesh (output) + # Create a vtkPoints object and store the points in it + vtk_points = vtk.vtkPoints() + for point in points: + vtk_points.InsertNextPoint(point) + + # Create a polydata object + point_polydata = vtk.vtkPolyData() + point_polydata.SetPoints(vtk_points) + + # Surface reconstruction + surf = vtk.vtkSurfaceReconstructionFilter() + surf.SetInputData(point_polydata) + surf.Update() + + # Create a contour filter to extract the surface + cf = vtk.vtkContourFilter() + cf.SetInputConnection(surf.GetOutputPort()) + cf.SetValue(0, 0.0) + cf.Update() + + # Reverse the normals + reverse = vtk.vtkReverseSense() + reverse.SetInputConnection(cf.GetOutputPort()) + reverse.ReverseCellsOn() + reverse.ReverseNormalsOn() + reverse.Update() + + # Get the reconstructed mesh + reconstructed_mesh = reverse.GetOutput() + + """# Simplify the mesh + target_reduction = 1 # Adjust this value as needed + simplified_mesh = self.simplify_mesh(reconstructed_mesh, target_reduction) + + combinded_faces = self.combine_coplanar_faces(simplified_mesh, 0.001)""" + + # Create a mapper and actor for the simplified mesh + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(reconstructed_mesh) + + actor = vtk.vtkActor() + actor.SetMapper(mapper) + actor.GetProperty().SetColor(1, 1, 1) # Set color (white in this case) + actor.GetProperty().EdgeVisibilityOn() # Show edges + actor.GetProperty().SetLineWidth(2) # Set line width + + # Add the actor to the renderer + self.renderer.AddActor(actor) + + # Force an update of the pipeline + #mapper.Update() + self.vtk_widget.GetRenderWindow().Render() + + # Print statistics + print(f"Original points: {len(points)}") + print( + f"Reconstructed mesh: {reconstructed_mesh.GetNumberOfPoints()} points, {reconstructed_mesh.GetNumberOfCells()} cells") + """print( + f"Simplified mesh: {simplified_mesh.GetNumberOfPoints()} points, {simplified_mesh.GetNumberOfCells()} cells")""" + + def on_click(self, obj, event): + click_pos = self.interactor.GetEventPosition() + + # Perform pick + self.picker.Pick(click_pos[0], click_pos[1], 0, self.renderer) + + # Get picked cell + cell_id = self.picker.GetCellId() + + if cell_id != -1: + print(f"Picked face ID: {cell_id}") + + # Get the polydata and the picked cell + polydata = self.picker.GetActor().GetMapper().GetInput() + cell = polydata.GetCell(cell_id) + + # Project2D + renderer = self.vtk_widget.GetRenderWindow().GetRenderers().GetFirstRenderer() + camera = renderer.GetActiveCamera() + + # Get cell type + cell_type = cell.GetCellType() + print(f"Cell type: {cell_type}") + + # Get points of the cell + points = cell.GetPoints() + num_points = points.GetNumberOfPoints() + print(f"Number of points in the cell: {num_points}") + + vec_points = [] + + # Get coordinates of each point + for i in range(num_points): + point = points.GetPoint(i) + print(f"Point {i}: {point}") + vec_points.append(point) + + # Get normal of the cell (if it's a polygon) + if cell_type == vtk.VTK_TRIANGLE: + normal = [0, 0, 0] + vtk.vtkPolygon.ComputeNormal(points, normal) + print(f"Face normal: {normal}") + + # Get cell data + cell_data = polydata.GetCellData() + if cell_data: + num_arrays = cell_data.GetNumberOfArrays() + print(f"Number of cell data arrays: {num_arrays}") + for i in range(num_arrays): + array = cell_data.GetArray(i) + array_name = array.GetName() + num_components = array.GetNumberOfComponents() + value = [0] * num_components + array.GetTuple(cell_id, value) + print(f"Cell data '{array_name}': {value}") + + # Get point data (average of all points in the cell) + point_data = polydata.GetPointData() + if point_data: + num_arrays = point_data.GetNumberOfArrays() + print(f"Number of point data arrays: {num_arrays}") + for i in range(num_arrays): + array = point_data.GetArray(i) + array_name = array.GetName() + num_components = array.GetNumberOfComponents() + avg_value = np.zeros(num_components) + for j in range(num_points): + point_id = cell.GetPointId(j) + value = [0] * num_components + array.GetTuple(point_id, value) + avg_value += np.array(value) + avg_value /= num_points + print(f"Average point data '{array_name}': {avg_value}") + + if num_points and cell_data: + face_orient = {'cell_data': cell_data, 'points': vec_points } + print(face_orient) + self.face_data.emit(face_orient) + + # Highlight picked face (your existing code) + ids = vtk.vtkIdTypeArray() + ids.SetNumberOfComponents(1) + ids.InsertNextValue(cell_id) + + selection_node = vtk.vtkSelectionNode() + selection_node.SetFieldType(vtk.vtkSelectionNode.CELL) + selection_node.SetContentType(vtk.vtkSelectionNode.INDICES) + selection_node.SetSelectionList(ids) + + selection = vtk.vtkSelection() + selection.AddNode(selection_node) + + extract_selection = vtk.vtkExtractSelection() + extract_selection.SetInputData(0, polydata) + extract_selection.SetInputData(1, selection) + extract_selection.Update() + + self.picked_mapper.SetInputData(extract_selection.GetOutput()) + self.vtk_widget.GetRenderWindow().Render() + + def start(self): + self.interactor.Initialize() + self.interactor.Start() + + +class MainWindow(QtWidgets.QMainWindow): + def __init__(self, parent=None): + super().__init__(parent) + self.vtk_widget = VTKWidget() + self.setCentralWidget(self.vtk_widget) + self.setWindowTitle("VTK Mesh Viewer") + self.vtk_widget.create_cube_mesh() + self.show() + self.vtk_widget.start() + + +if __name__ == "__main__": + app = QtWidgets.QApplication(sys.argv) + window = MainWindow() + sys.exit(app.exec()) diff --git a/drawing_modules/vysta_widget.py b/drawing_modules/vysta_widget.py new file mode 100644 index 0000000..c3f8d51 --- /dev/null +++ b/drawing_modules/vysta_widget.py @@ -0,0 +1,111 @@ +import sys + +import numpy as np +import pyvista as pv +from pyvista.plotting.opts import ElementType +from pyvistaqt import QtInteractor +from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget + + +class PyVistaWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + # Create the PyVista plotter + self.plotter = QtInteractor(self) + self.plotter.background_color = "darkgray" + + # Create a layout and add the PyVista widget + layout = QVBoxLayout() + layout.addWidget(self.plotter.interactor) + self.setLayout(layout) + + # Set up the picker + #self.plotter.enable_cell_picking(callback=self.on_cell_pick, show=True) + self.plotter.enable_element_picking(callback=self.on_cell_pick, show=True, mode="face", left_clicking=True) + + def on_cell_pick(self, element): + if element is not None: + mesh = self.plotter.mesh # Get the current mesh + print(mesh) + print(element) + + """# Get the face data + face = mesh.extract_cells(element) + + # Compute face normal + face.compute_normals(cell_normals=True, inplace=True) + normal = face.cell_data['Normals'][0] + + # Get the points of the face + points = face.points + + print(f"Picked face ID: {face_id}") + print(f"Face normal: {normal}") + print("Face points:") + for point in points: + print(point)""" + else: + print("No face was picked or the picked element is not a face.") + def create_simplified_outline(self, mesh, camera): + # Project 3D to 2D + points_2d = self.plotter.map_to_2d(mesh.points) + + # Detect silhouette edges (simplified approach) + edges = mesh.extract_feature_edges(feature_angle=90, boundary_edges=False, non_manifold_edges=False) + + # Project edges to 2D + edge_points_2d = self.plotter.map_to_2d(edges.points) + + # Create 2D outline + self.plotter.add_lines(edge_points_2d, color='black', width=2) + self.plotter.render() + + def mesh_from_points(self, points): + # Convert points to numpy array if not already + points = np.array(points) + + # Create faces array + num_triangles = len(points) // 3 + faces = np.arange(len(points)).reshape(num_triangles, 3) + faces = np.column_stack((np.full(num_triangles, 3), faces)) # Add 3 as first column + + # Create PyVista PolyData + mesh = pv.PolyData(points, faces) + + # Optional: Merge duplicate points + mesh = mesh.clean() + + # Optional: Compute normals + mesh = mesh.compute_normals(point_normals=False, cell_normals=True, consistent_normals=True) + edges = mesh.extract_feature_edges(30, non_manifold_edges=False) + + # Clear any existing meshes + self.plotter.clear() + + # Add the mesh to the plotter + self.plotter.add_mesh(mesh, pickable=True, color='white', show_edges=True, line_width=2, pbr=True, metallic=0.8, roughness=0.1, diffuse=1) + self.plotter.add_mesh(edges, color="red", line_width=10) + + # Reset the camera to fit the new mesh + self.plotter.reset_camera() + + # Update the render window + self.plotter.update() + + # Print statistics + print(f"Original points: {len(points)}") + print(f"Number of triangles: {num_triangles}") + print(f"Final number of points: {mesh.n_points}") + print(f"Final number of cells: {mesh.n_cells}") + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("PyVista in PySide6") + self.setGeometry(100, 100, 800, 600) + + + + diff --git a/main.py b/main.py index 4758028..9006f51 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,14 @@ import uuid - import names from PySide6.QtCore import Qt, QPoint from PySide6.QtWidgets import QApplication, QMainWindow, QSizePolicy, QInputDialog from Gui import Ui_fluencyCAD # Import the generated GUI module -from drawing_modules.gl_widget import OpenGLWidget +from drawing_modules.vtk_widget import VTKWidget +from drawing_modules.vysta_widget import PyVistaWidget from drawing_modules.draw_widget2d import SketchWidget from sdf import * from python_solvespace import SolverSystem, ResultFlag +from mesh_modules import simple_mesh # main, draw_widget, gl_widget @@ -19,11 +20,11 @@ class MainWindow(QMainWindow): self.ui = Ui_fluencyCAD() self.ui.setupUi(self) - self.openGLWidget = OpenGLWidget() + self.custom_3D_Widget = VTKWidget() layout = self.ui.gl_box.layout() - layout.addWidget(self.openGLWidget) + layout.addWidget(self.custom_3D_Widget) size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) - #self.openGLWidget.setSizePolicy(size_policy) + #self.custom_3D_Widget.setSizePolicy(size_policy) self.sketchWidget = SketchWidget() layout2 = self.ui.sketch_tab.layout() # Get the layout of self.ui.gl_canvas @@ -40,10 +41,10 @@ class MainWindow(QMainWindow): #self.ui.pb_apply_code.pressed.connect(self.check_current_tab) self.ui.sketch_list.currentItemChanged.connect(self.on_item_changed) - self.ui.sketch_list.itemChanged.connect(self.view_update) + self.ui.sketch_list.itemChanged.connect(self.draw_mesh) ### Sketches - self.ui.pb_origin_wp.pressed.connect(self.add_wp_origin) + self.ui.pb_origin_wp.pressed.connect(self.add_new_sketch) self.ui.pb_nw_sktch.pressed.connect(self.add_sketch) self.ui.pb_del_sketch.pressed.connect(self.del_sketch) @@ -65,7 +66,8 @@ class MainWindow(QMainWindow): self.sketchWidget.constrain_done.connect(self.draw_op_complete) - def add_wp_origin(self): + def add_new_sketch(self): + self.sketchWidget.clear_sketch() self.sketchWidget.create_workplane() def act_line_mode(self): @@ -124,13 +126,24 @@ class MainWindow(QMainWindow): self.sketchWidget.mouse_mode = None self.sketchWidget.reset_buffers() - def view_update(self): - print("Update") + def calc_sketch_projection_3d(self, depth): + name = self.ui.sketch_list.currentItem().text() + #print("selected_for disp", name) + model = self.model['sketch'][name]['sketch_points'] + #print("sketch points from model", model) + simp_mesh = simple_mesh.generate_mesh(model, depth) + print("Generated model", simp_mesh) + self.custom_3D_Widget. load_interactor_mesh(simp_mesh) #draw_interactor(simp_mesh) + + def draw_mesh(self): + #print("Update") name = self.ui.body_list.currentItem().text() - print("selected_for disp", name) + #print("selected_for disp", name) model = self.model['operation'][name]['sdf_object'] - mesh = model.generate(samples=2**12) - self.openGLWidget.load_mesh_direct(mesh) + mesh = model.generate() + #print("Mesh sdf", mesh) + #self.custom_3D_Widget.render_from_points_direct_with_faces(mesh) + self.custom_3D_Widget.render_from_points_direct_with_faces(mesh) def on_item_changed(self, current_item, previous_item): if current_item: @@ -138,16 +151,25 @@ class MainWindow(QMainWindow): #self.view_update() print(f"Selected item: {name}") - def add_sketch(self): - points_for_poly = [] - name = f"sketch-{str(names.get_first_name())}" + def convert_points_for_sdf(self): + points_for_sdf = [] for point_to_poly in self.sketchWidget.slv_points_main: - points_for_poly.append(self.translate_points_tup(point_to_poly['ui_point'])) + points_for_sdf.append(self.translate_points_tup(point_to_poly['ui_point'])) + + return points_for_sdf + + def add_sketch(self): + + name = f"sketch-{str(names.get_first_name())}" + points_for_sdf = self.convert_points_for_sdf() element = { 'id': name, - 'type': 'polygon', - 'sketch_points': points_for_poly, + 'type': 'sketch', + 'point_list': self.sketchWidget.slv_points_main, + 'line_list': self.sketchWidget.slv_lines_main, + 'sketch_points': points_for_sdf, + 'solver': self.sketchWidget.solv } self.model['sketch'][element['id']] = element @@ -162,10 +184,15 @@ class MainWindow(QMainWindow): def edit_sketch(self): name = self.ui.sketch_list.currentItem().text() - self.sketchWidget.clear_sketch() - points = self.model['sketch'][name]['sketch_points'] - print("points", points) - self.sketchWidget.set_points(points) + #self.sketchWidget.clear_sketch() + + self.sketchWidget.slv_points_main = self.model['sketch'][name]['point_list'] + self.sketchWidget.slv_lines_main = self.model['sketch'][name]['line_list'] + self.sketchWidget.solv = self.model['sketch'][name]['solver'] + + self.sketchWidget.update() + print("model",self.model) + print("widget", self.sketchWidget.slv_points_main) def del_sketch(self): print("Deleting") @@ -217,7 +244,7 @@ class MainWindow(QMainWindow): self.ui.body_list.takeItem(row) # Remove the item from the list widget self.model['operation'].pop(item_name) # Remove the item from the operation dictionary print(f"Removed operation: {item_name}") - self.openGLWidget.clear_mesh() + self.custom_3D_Widget.clear_mesh() def translate_points_tup(self, point: QPoint): """QPoints from Display to mesh data @@ -240,8 +267,11 @@ class MainWindow(QMainWindow): length, ok = QInputDialog.getDouble(self, 'Extrude Length', 'Enter a mm value:', decimals=2) #TODO : Implement cancel + + #Create and draw Interactor geo = Geometry() f = geo.extrude_shape(points, length) + name_op = f"extrd-{name}" element = { 'id': name_op, @@ -251,11 +281,11 @@ class MainWindow(QMainWindow): print(element) self.model['operation'][name_op] = element - self.ui.body_list.addItem(name_op) items = self.ui.body_list.findItems(name_op, Qt.MatchExactly)[0] self.ui.body_list.setCurrentItem(items) - self.view_update() + self.draw_mesh() + #self.calc_sketch_projection_3d(length) def send_cut(self): name = self.ui.body_list.currentItem().text() @@ -278,13 +308,13 @@ class MainWindow(QMainWindow): self.ui.body_list.addItem(name_op) items = self.ui.sketch_list.findItems(name_op, Qt.MatchExactly) self.ui.body_list.setCurrentItem(items[-1]) - self.view_update() + self.draw_mesh() else: print("mindestens 2!") def load_and_render(self, file): - self.openGLWidget.load_stl(file) - self.openGLWidget.update() + self.custom_3D_Widget.load_stl(file) + self.custom_3D_Widget.update() class Geometry: def distance(self, p1, p2): diff --git a/mesh_modules/simple_mesh.py b/mesh_modules/simple_mesh.py new file mode 100644 index 0000000..e7a181c --- /dev/null +++ b/mesh_modules/simple_mesh.py @@ -0,0 +1,50 @@ +import numpy as np +from scipy.spatial import ConvexHull +from stl import mesh + + +def generate_mesh(points, depth): + """ + Generate a mesh by extruding a 2D shape along the Z-axis. + + :param points: List of (x, y) tuples representing the 2D shape. + :param depth: Extrusion depth along the Z-axis. + :return: Tuple of vertices and faces. + """ + # Convert points to a numpy array + points_2d = np.array(points) + + # Get the convex hull of the points to ensure they form a proper polygon + hull = ConvexHull(points_2d) + hull_points = points_2d[hull.vertices] + + # Generate the top and bottom faces + bottom_face = np.hstack((hull_points, np.zeros((hull_points.shape[0], 1)))) + top_face = np.hstack((hull_points, np.ones((hull_points.shape[0], 1)) * depth)) + + # Combine top and bottom vertices + vertices_array = np.vstack((bottom_face, top_face)) + + # Create faces + faces = [] + + # Bottom face triangulation (counter-clockwise) + for i in range(len(hull_points) - 2): + faces.append([0, i + 2, i + 1]) + + # Top face triangulation (counter-clockwise, with an offset) + top_offset = len(hull_points) + for i in range(len(hull_points) - 2): + faces.append([top_offset, top_offset + i + 1, top_offset + i + 2]) + + # Side faces (ensure counter-clockwise order) + for i in range(len(hull_points)): + next_i = (i + 1) % len(hull_points) + faces.append([i, top_offset + i, top_offset + next_i]) + faces.append([i, top_offset + next_i, next_i]) + + # Convert vertices to the desired format: list of tuples + vertices = [tuple(vertex) for vertex in vertices_array] + + return vertices, faces +