diff --git a/2d_draw.py b/2d_draw.py deleted file mode 100644 index f2b6ffd..0000000 --- a/2d_draw.py +++ /dev/null @@ -1,81 +0,0 @@ -import sys - -from OpenGL.raw.GL.VERSION.GL_1_0 import glClearColor -from PySide6.QtCore import Qt -from PySide6.QtOpenGLWidgets import QOpenGLWidget -from PySide6.QtWidgets import QDoubleSpinBox, QPushButton, QVBoxLayout, QApplication, QWidget -from OpenGL.GL import * -from OpenGL.GLUT import * - -class GLWidget(QOpenGLWidget): - def __init__(self, parent=None): - super(GLWidget, self).__init__(parent) - self.shapes = [] - self.extruded_shapes = [] - - def initializeGL(self): - glClearColor(1, 1, 1, 1) - - def paintGL(self): - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - glLoadIdentity() - glColor3f(1, 0, 0) - - for shape in self.shapes: - glBegin(GL_LINE_LOOP) - glVertex2f(shape[0], shape[1]) # Access x and y coordinates of the point - glEnd() - - def resizeGL(self, w, h): - glViewport(0, 0, w, h) - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - glOrtho(0, w, h, 0, -1, 1) - glMatrixMode(GL_MODELVIEW) - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton: - pos = event.pos() - x = int(pos.x()) - y = int(pos.y()) - self.shapes.append((x, y)) # Append coordinate tuple (x, y) - self.update() - def extrude_shapes(self, height): - self.extruded_shapes = [] - for shape in self.shapes: - extruded_shape = [] - for point in shape: - extruded_shape.append([point[0], point[1]]) - for point in shape: - extruded_shape.append([point[0], point[1] + height]) - self.extruded_shapes.append(extruded_shape) - -class MainWindow(QWidget): - def __init__(self): - super(MainWindow, self).__init__() - self.gl_widget = GLWidget() - self.height_spin_box = QDoubleSpinBox() - self.height_spin_box.setRange(0, 100) - self.height_spin_box.setValue(10) - self.extrude_button = QPushButton("Extrude") - self.extrude_button.clicked.connect(self.extrude_shapes) - - layout = QVBoxLayout() - layout.addWidget(self.gl_widget) - layout.addWidget(self.height_spin_box) - layout.addWidget(self.extrude_button) - - self.setLayout(layout) - - def extrude_shapes(self): - height = self.height_spin_box.value() - self.gl_widget.extrude_shapes(height) - self.gl_widget.update() - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = MainWindow() - window.setGeometry(100, 100, 800, 600) - window.setWindowTitle("Extrude Shapes") - window.show() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/2dtest.py b/2dtest.py deleted file mode 100644 index 4f7d97c..0000000 --- a/2dtest.py +++ /dev/null @@ -1,62 +0,0 @@ -import sys -from PySide6.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton -from PySide6.QtGui import QPainter, QPen -from PySide6.QtCore import Qt - -class CADWindow(QWidget): - def __init__(self): - super().__init__() - self.setWindowTitle("CAD") - self.resize(800, 600) - - # Create a label for the dimensions input - self.dimension_label = QLabel("Dimensions:", self) - self.dimension_label.move(10, 10) - - # Create a line edit for the user to enter the dimensions - self.dimension_input = QLineEdit() - self.dimension_input.setPlaceholderText("Enter dimensions...") - self.dimension_input.setFixedWidth(300) - self.dimension_input.move(10, 40) - - # Create a button for the user to confirm the dimensions - self.confirm_button = QPushButton("Confirm", self) - self.confirm_button.clicked.connect(self.confirm_dimensions) - self.confirm_button.move(10, 70) - - # Create a label for the canvas - self.canvas_label = QLabel("Canvas:", self) - self.canvas_label.move(400, 10) - - # Create a canvas widget for the user to draw on - self.canvas = QWidget() - self.canvas.setFixedSize(300, 300) - self.canvas.move(400, 40) - - # Set the default dimensions of the canvas - self.dimensions = (300, 300) - - def confirm_dimensions(self): - # Get the dimensions from the line edit and convert to a tuple - dimensions = self.dimension_input.text().split(",") - dimensions = [int(d) for d in dimensions] - self.dimensions = dimensions - - # Resize the canvas widget to match the new dimensions - self.canvas.setFixedSize(*self.dimensions) - - def paintEvent(self, event): - # Get a painter object for painting on the canvas - painter = QPainter(self.canvas) - - # Set the pen color and width - painter.pen = QPen(Qt.black, 2) - - # Draw a line across the canvas using the dimensions from the user input - painter.drawLine(0, 0, *self.dimensions) - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = CADWindow() - window.show() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/3d_windows.py b/3d_windows.py deleted file mode 100644 index 45eae5c..0000000 --- a/3d_windows.py +++ /dev/null @@ -1,38 +0,0 @@ -from PySide6 import QtGui -from PySide6.Qt3DCore import Qt3DCore -from PySide6.Qt3DExtras import Qt3DExtras -from PySide6.QtWidgets import QApplication, QMainWindow -import sys - -class My3DWindow(QMainWindow): - def __init__(self): - super(My3DWindow, self).__init__() - - # Create the 3D scene - scene = Qt3DCore.QEntity() - - # Create a 3D view container - view = Qt3DExtras.Qt3DWindow() - view.defaultFrameGraph().setClearColor(QtGui.QColor(0, 0, 0, 0)) - - # Set root entity for the scene - view.setRootEntity(scene) - - # Create a 3D transform - transform = Qt3DCore.QTransform() - transform.setScale3D(QtGui.QVector3D(1.0, 1.0, 1.0)) - - # Set components for the mesh entity - mesh_entity.addComponent(material) - mesh_entity.addComponent(transform) - - # Add the mesh entity to the scene - scene.addComponent(mesh_entity) - - self.setCentralWidget(view) - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = My3DWindow() - window.show() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/Station_alpha_ripped.glb b/Station_alpha_ripped.glb deleted file mode 100644 index 77bfe3b..0000000 Binary files a/Station_alpha_ripped.glb and /dev/null differ diff --git a/drawing_modules/draw_widget2d.py b/drawing_modules/draw_widget2d.py index b7dc005..be796b1 100644 --- a/drawing_modules/draw_widget2d.py +++ b/drawing_modules/draw_widget2d.py @@ -2,6 +2,7 @@ import math import re from copy import copy +import numpy as np from PySide6.QtWidgets import QApplication, QWidget, QMessageBox, QInputDialog from PySide6.QtGui import QPainter, QPen, QColor from PySide6.QtCore import Qt, QPoint, QPointF, Signal @@ -47,6 +48,81 @@ class SketchWidget(QWidget): def create_workplane(self): self.wp = self.solv.create_2d_base() + def create_workplane_projected(self): + self.wp = self.solv.create_2d_base() + + def create_proj_lines(self, lines): + """Lines as orientation projected from the sketch""" + + for line in lines: + for point in line: + x, y, z = point + + point = self.solv.add_point_2d(x, y, self.wp) + + relation_point = {} # Reinitialize the dictionary + handle_nr = self.get_handle_nr(str(point)) + relation_point['handle_nr'] = handle_nr + relation_point['solv_handle'] = point + relation_point['ui_point'] = QPoint(x, y) + + self.slv_points_main.append(relation_point) + + print("points", self.slv_points_main) + print("lines", self.slv_lines_main) + print("lines", lines) + + + def find_duplicate_points_2d(self, edges): + points = [] + seen = set() + duplicates = [] + + for edge in edges: + for point in edge: + # Extract only x and y coordinates + point_2d = (point[0], point[1]) + if point_2d in seen: + if point_2d not in duplicates: + duplicates.append(point_2d) + else: + seen.add(point_2d) + points.append(point_2d) + + return duplicates + + def normal_to_quaternion(self, normal): + normal = np.array(normal) + #normal = normal / np.linalg.norm(normal) + + axis = np.cross([0, 0, 1], normal) + if np.allclose(axis, 0): + axis = np.array([1, 0, 0]) + else: + axis = axis / np.linalg.norm(axis) # Normalize the axis + + angle = np.arccos(np.dot([0, 0, 1], normal)) + + qw = np.cos(angle / 2) + sin_half_angle = np.sin(angle / 2) + qx, qy, qz = axis * sin_half_angle # This will now work correctly + + return qw, qx, qy, qz + + def create_workplane_space(self, points, normal): + print("edges", points) + origin = self.find_duplicate_points_2d(points) + print(origin) + x, y = origin[0] + origin = QPoint(x, y) + + origin_handle = self.get_handle_from_ui_point(origin) + qw, qx, qy, qz = self.normal_to_quaternion(normal) + + slv_normal = self.solv.add_normal_3d(qw, qx, qy, qz) + self.wp = self.solv.add_work_plane(origin_handle, slv_normal) + print(self.wp) + def get_handle_nr(self, input_str: str) -> int: # Define the regex pattern to extract the handle number pattern = r"handle=(\d+)" diff --git a/drawing_modules/vtk_widget.py b/drawing_modules/vtk_widget.py index 46656dc..0dec7a8 100644 --- a/drawing_modules/vtk_widget.py +++ b/drawing_modules/vtk_widget.py @@ -5,6 +5,7 @@ import vtk from PySide6 import QtCore, QtWidgets from PySide6.QtCore import Signal from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor +from vtkmodules.util.numpy_support import vtk_to_numpy, numpy_to_vtk class VTKWidget(QtWidgets.QWidget): @@ -12,6 +13,10 @@ class VTKWidget(QtWidgets.QWidget): def __init__(self, parent=None): super().__init__(parent) + self.access_selected_points = [] + self.selected_normal = None + self.selected_edges = [] + self.cell_normals = None self.vtk_widget = QVTKRenderWindowInteractor(self) # Create layout and add VTK widget @@ -26,12 +31,12 @@ class VTKWidget(QtWidgets.QWidget): # Set up the camera self.camera = self.renderer.GetActiveCamera() - self.camera.SetPosition(5, 5, 5) + self.camera.SetPosition(5, 5, 100) self.camera.SetFocalPoint(0, 0, 0) # Set up picking self.picker = vtk.vtkCellPicker() - self.picker.SetTolerance(0.0005) + self.picker.SetTolerance(0.005) # Create a mapper and actor for picked cells self.picked_mapper = vtk.vtkDataSetMapper() @@ -47,78 +52,109 @@ class VTKWidget(QtWidgets.QWidget): # Add observer for mouse clicks self.interactor.AddObserver("RightButtonPressEvent", self.on_click) - def create_cube_mesh(self): - cube_source = vtk.vtkCubeSource() - print(cube_source) + @staticmethod + def compute_normal_from_lines(line1, line2): + vec1 = line1[1] - line1[0] + vec2 = line2[1] - line2[0] + normal = np.cross(vec1, vec2) + print(normal) + normal = normal / np.linalg.norm(normal) + return normal + + def load_interactor_mesh(self, edges): + # Create vtkPoints to store all points + points = vtk.vtkPoints() + + # Create vtkCellArray to store the lines + lines = vtk.vtkCellArray() + + for edge in edges: + # Add points for this edge + point_id1 = points.InsertNextPoint(edge[0]) + point_id2 = points.InsertNextPoint(edge[1]) + + # Create a line using the point IDs + line = vtk.vtkLine() + line.GetPointIds().SetId(0, point_id1) + line.GetPointIds().SetId(1, point_id2) + + # Add the line to the cell array + lines.InsertNextCell(line) + + # Create vtkPolyData to store the geometry + polydata = vtk.vtkPolyData() + polydata.SetPoints(points) + polydata.SetLines(lines) + + # Verify that the polydata is not empty + if polydata.GetNumberOfPoints() == 0 or polydata.GetNumberOfCells() == 0: + print("Error: PolyData is empty") + sys.exit(1) + + # Create a mapper and actor mapper = vtk.vtkPolyDataMapper() - mapper.SetInputConnection(cube_source.GetOutputPort()) + mapper.SetInputData(polydata) + actor = vtk.vtkActor() actor.SetMapper(mapper) + actor.GetProperty().SetColor(0.0, 0.0, 1.0) # Set color to red + actor.GetProperty().SetLineWidth(2) # Set line width + + # Add the actor to the scene self.renderer.AddActor(actor) + #self.renderer.SetBackground(0.1, 0.2, 0.4) # Set background color - def simplify_mesh(self, input_mesh, target_reduction): - # Create the quadric decimation filter - decimate = vtk.vtkDecimatePro() - decimate.SetInputData(input_mesh) + # Render and interact + # Force an update of the pipeline + # mapper.Update() + self.vtk_widget.GetRenderWindow().Render() - # Set the reduction factor (0 to 1, where 1 means maximum reduction) - decimate.SetTargetReduction(target_reduction) + def render_from_points_direct_with_faces(self, vertices, faces): + points = vtk.vtkPoints() + for i in range(vertices.shape[0]): + points.InsertNextPoint(vertices[i]) - # Optional: Preserve topology (if needed) - decimate.PreserveTopologyOn() + # Create a vtkCellArray to store the triangles + triangles = vtk.vtkCellArray() + for i in range(faces.shape[0]): + triangle = vtk.vtkTriangle() + triangle.GetPointIds().SetId(0, faces[i, 0]) + triangle.GetPointIds().SetId(1, faces[i, 1]) + triangle.GetPointIds().SetId(2, faces[i, 2]) + triangles.InsertNextCell(triangle) - # Perform the decimation - decimate.Update() + # Create a polydata object + polydata = vtk.vtkPolyData() + polydata.SetPoints(points) + polydata.SetPolys(triangles) - return decimate.GetOutput() + # Calculate normals + normalGenerator = vtk.vtkPolyDataNormals() + normalGenerator.SetInputData(polydata) + normalGenerator.ComputePointNormalsOn() + normalGenerator.ComputeCellNormalsOn() + normalGenerator.Update() - 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() + self.cell_normals = vtk_to_numpy(normalGenerator.GetOutput().GetCellData().GetNormals()) - # 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() + # Create a mapper and actor + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(polydata) - return normals.GetOutput() + 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 - 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() + # (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 - # 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) + # Force an update of the pipeline + #mapper.Update() + self.vtk_widget.GetRenderWindow().Render() def load_custom_mesh(self, vertices, faces): ### Load meshes by own module @@ -154,277 +190,114 @@ class VTKWidget(QtWidgets.QWidget): 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 + # Get picked cell ID cell_id = self.picker.GetCellId() if cell_id != -1: - print(f"Picked face ID: {cell_id}") + print(f"Picked cell 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() + # Ensure it's a line + if cell.GetCellType() == vtk.VTK_LINE: + # Get the two points of the line + point_id1 = cell.GetPointId(0) + point_id2 = cell.GetPointId(1) - # Get cell type - cell_type = cell.GetCellType() - print(f"Cell type: {cell_type}") + proj_point1 = polydata.GetPoint(point_id1) + proj_point2 = polydata.GetPoint(point_id2) - # Get points of the cell - points = cell.GetPoints() - num_points = points.GetNumberOfPoints() - print(f"Number of points in the cell: {num_points}") + self.access_selected_points.append((proj_point1, proj_point2)) - vec_points = [] + point1 = np.array(proj_point1) + point2 = np.array(proj_point2) - # Get coordinates of each point - for i in range(num_points): - point = points.GetPoint(i) - print(f"Point {i}: {point}") - vec_points.append(point) + print(f"Line starts at: {point1}") + print(f"Line ends at: {point2}") - # 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}") + # Store this line for later use if needed + self.selected_edges.append((point1, point2)) - # 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}") + if len(self.selected_edges) == 2: + # Compute the normal from the two selected edges + edge1 = self.selected_edges[0][1] - self.selected_edges[0][0] + edge2 = self.selected_edges[1][1] - self.selected_edges[1][0] + self.selected_normal = np.cross(edge1, edge2) + self.selected_normal = self.selected_normal / np.linalg.norm(self.selected_normal) + print("Computed normal:", self.selected_normal) - # 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}") + # Create a transform for projection + transform = vtk.vtkTransform() + transform.Identity() - if num_points and cell_data: - face_orient = {'cell_data': cell_data, 'points': vec_points } - print(face_orient) - self.face_data.emit(face_orient) + # Compute rotation to align normal with Z-axis + z_axis = np.array([0, 0, 1]) + rotation_axis = np.cross(self.selected_normal, z_axis) + rotation_angle = np.arccos(np.dot(self.selected_normal, z_axis)) * 180 / np.pi - # Highlight picked face (your existing code) - ids = vtk.vtkIdTypeArray() - ids.SetNumberOfComponents(1) - ids.InsertNextValue(cell_id) + if np.linalg.norm(rotation_axis) > 1e-6: # Avoid division by zero + transform.RotateWXYZ(rotation_angle, rotation_axis[0], rotation_axis[1], rotation_axis[2]) - selection_node = vtk.vtkSelectionNode() - selection_node.SetFieldType(vtk.vtkSelectionNode.CELL) - selection_node.SetContentType(vtk.vtkSelectionNode.INDICES) - selection_node.SetSelectionList(ids) + # Apply scaling to flatten + transform.Scale(1, 1, 0) - selection = vtk.vtkSelection() - selection.AddNode(selection_node) + # Apply the inverse rotation to bring it back to original orientation + transform.RotateWXYZ(-rotation_angle, rotation_axis[0], rotation_axis[1], rotation_axis[2]) - extract_selection = vtk.vtkExtractSelection() - extract_selection.SetInputData(0, polydata) - extract_selection.SetInputData(1, selection) - extract_selection.Update() + # Apply the transform to the polydata + transformFilter = vtk.vtkTransformPolyDataFilter() + transformFilter.SetInputData(polydata) + transformFilter.SetTransform(transform) + transformFilter.Update() - self.picked_mapper.SetInputData(extract_selection.GetOutput()) - self.vtk_widget.GetRenderWindow().Render() + # Get the projected polydata + projected_polydata = transformFilter.GetOutput() + # Create a mapper and actor for the projected data + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(projected_polydata) + + actor = vtk.vtkActor() + actor.SetMapper(mapper) + actor.GetProperty().SetColor(0.0, 1.0, 0.0) # Set color to green + actor.GetProperty().SetLineWidth(4) # Set line width + + # Add the actor to the scene + self.renderer.AddActor(actor) + + # Add a plane to visualize the projection plane + plane_source = vtk.vtkPlaneSource() + plane_source.SetCenter(0, 0, 0) + plane_source.SetNormal(self.selected_normal) + plane_mapper = vtk.vtkPolyDataMapper() + plane_mapper.SetInputConnection(plane_source.GetOutputPort()) + plane_actor = vtk.vtkActor() + plane_actor.SetMapper(plane_mapper) + plane_actor.GetProperty().SetColor(0.8, 0.8, 0.8) # Light gray + plane_actor.GetProperty().SetOpacity(0.5) + self.renderer.AddActor(plane_actor) + + # Reset camera to show all actors + self.renderer.ResetCamera() + + # Render and interact + self.vtk_widget.GetRenderWindow().Render() + + # Reset selected edges + self.selected_edges = [] + + elif len(self.access_selected_points) > 2: + self.access_selected_points = [] + else: + print("Selected cell is not a line") def start(self): self.interactor.Initialize() self.interactor.Start() diff --git a/drawing_modules/vtk_widget_alt_methods.py b/drawing_modules/vtk_widget_alt_methods.py new file mode 100644 index 0000000..c1e2fa8 --- /dev/null +++ b/drawing_modules/vtk_widget_alt_methods.py @@ -0,0 +1,337 @@ +def are_coplanar(self, normal1, normal2, point1, point2, tolerance=1e-6): + # Check if normals are parallel + if np.abs(np.dot(normal1, normal2)) < 1 - tolerance: + return False + + # Check if points lie on the same plane + diff = point2 - point1 + return np.abs(np.dot(diff, normal1)) < tolerance + + +def merge_coplanar_triangles(self, polydata): + # Compute normals + normalGenerator = vtk.vtkPolyDataNormals() + normalGenerator.SetInputData(polydata) + normalGenerator.ComputePointNormalsOff() + normalGenerator.ComputeCellNormalsOn() + normalGenerator.Update() + + mesh = normalGenerator.GetOutput() + n_cells = mesh.GetNumberOfCells() + + # Create a map to store merged triangles + merged = {} + + for i in range(n_cells): + if i in merged: + continue + + cell = mesh.GetCell(i) + normal = np.array(mesh.GetCellData().GetNormals().GetTuple(i)) + point = np.array(cell.GetPoints().GetPoint(0)) + + merged[i] = [i] + + for j in range(i + 1, n_cells): + if j in merged: + continue + + cell_j = mesh.GetCell(j) + normal_j = np.array(mesh.GetCellData().GetNormals().GetTuple(j)) + point_j = np.array(cell_j.GetPoints().GetPoint(0)) + + if self.are_coplanar(normal, normal_j, point, point_j): + merged[i].append(j) + + # Create new polygons + new_polygons = vtk.vtkCellArray() + for group in merged.values(): + if len(group) > 1: + polygon = vtk.vtkPolygon() + points = set() + for idx in group: + cell = mesh.GetCell(idx) + for j in range(3): + point_id = cell.GetPointId(j) + points.add(point_id) + polygon.GetPointIds().SetNumberOfIds(len(points)) + for j, point_id in enumerate(points): + polygon.GetPointIds().SetId(j, point_id) + new_polygons.InsertNextCell(polygon) + else: + new_polygons.InsertNextCell(mesh.GetCell(group[0])) + + # Create new polydata + new_polydata = vtk.vtkPolyData() + new_polydata.SetPoints(mesh.GetPoints()) + new_polydata.SetPolys(new_polygons) + + return new_polydata + + +def create_cube_mesh(self): + # cube_source = vtk.vtkSuperquadricSource() + + reader = vtk.vtkSTLReader() + reader.SetFileName("case.stl") # Replace with your mesh file path + reader.Update() + + featureEdges = vtk.vtkFeatureEdges() + featureEdges.SetInputConnection(reader.GetOutputPort()) + featureEdges.BoundaryEdgesOn() + featureEdges.FeatureEdgesOn() + featureEdges.ManifoldEdgesOff() + featureEdges.NonManifoldEdgesOff() + featureEdges.Update() + + # print(cube_source) + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputConnection(reader.GetOutputPort()) + actor = vtk.vtkActor() + actor.SetMapper(mapper) + self.renderer.AddActor(actor) + + mapper_edge = vtk.vtkPolyDataMapper() + mapper_edge.SetInputConnection(featureEdges.GetOutputPort()) + actor = vtk.vtkActor() + actor.SetMapper(mapper_edge) + 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 create_simplified_outline(self, polydata): + featureEdges = vtk.vtkFeatureEdges() + featureEdges.SetInputData(polydata) + featureEdges.BoundaryEdgesOn() + featureEdges.FeatureEdgesOn() + featureEdges.ManifoldEdgesOff() + featureEdges.NonManifoldEdgesOff() + featureEdges.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 featureEdges + + +def render_from_points_direct_with_faces(self, vertices, faces): + points = vtk.vtkPoints() + for i in range(vertices.shape[0]): + points.InsertNextPoint(vertices[i]) + + # Create a vtkCellArray to store the triangles + triangles = vtk.vtkCellArray() + for i in range(faces.shape[0]): + triangle = vtk.vtkTriangle() + triangle.GetPointIds().SetId(0, faces[i, 0]) + triangle.GetPointIds().SetId(1, faces[i, 1]) + triangle.GetPointIds().SetId(2, faces[i, 2]) + triangles.InsertNextCell(triangle) + + """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(points) + polydata.SetPolys(triangles) + + # Calculate normals + normalGenerator = vtk.vtkPolyDataNormals() + normalGenerator.SetInputData(polydata) + normalGenerator.ComputePointNormalsOn() + normalGenerator.ComputeCellNormalsOn() + normalGenerator.Update() + + self.cell_normals = vtk_to_numpy(normalGenerator.GetOutput().GetCellData().GetNormals()) + + # merged_polydata = self.merge_coplanar_triangles(polydata) + + # Create a mapper and actor + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(polydata) + + 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")""" \ No newline at end of file diff --git a/fluency.py b/fluency.py deleted file mode 100644 index 9c23ef7..0000000 --- a/fluency.py +++ /dev/null @@ -1,57 +0,0 @@ -from math import pi, sin, cos -from direct.showbase.ShowBase import ShowBase -from direct.task import Task -from direct.gui.DirectGui import DirectEntry, DirectButton -from sdf import * - - -class MyApp(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - """# Load the environment model. - self.scene = self.loader.loadModel("station_alpha_ripped.glb") - # Reparent the model to render. - self.scene.reparentTo(self.render) - # Apply scale and position transforms on the model. - self.scene.setScale(1, 1, 1) - self.scene.setPos(0, 0, 0)""" - - # Add the spinCameraTask procedure to the task manager. - self.taskMgr.add(self.spinCameraTask, "SpinCameraTask") - - # Create a DirectEntry widget for text input - self.textEntry = DirectEntry( - text="", scale=0.05, command=self.setText, initialText="Type here" - ) - self.textEntry.reparentTo(self.aspect2d) - self.textEntry.setPos(-1.1, 0, -0.95) - - self.apply = DirectButton(text="Make Cube",scale=0.05, command=self.buttonClicked) - self.apply.reparentTo(self.aspect2d) - self.apply.setPos(-1, -1, 0.8) - - def setText(self, textEntered): - print("Text entered:", textEntered) - - def buttonClicked(self): - print("Clciked") - obj = box(0.9) - model = obj.save("model.stl" , step=0.01) - self.scene = self.loader.loadModel("model.stl") - self.scene.reparentTo(self.render) - self.scene.setScale(1, 1, 1) - self.scene.setPos(0, 0, 0) - - - # Define a procedure to move the camera. - def spinCameraTask(self, task): - angleDegrees = task.time * 6.0 - angleRadians = angleDegrees * (pi / 180.0) - self.camera.setPos(20 * sin(angleRadians), -20 * cos(angleRadians), 3) - self.camera.setHpr(angleDegrees, 0, 0) - return Task.cont - - -app = MyApp() -app.run() diff --git a/fluencyb.py b/fluencyb.py deleted file mode 100644 index 890ca89..0000000 --- a/fluencyb.py +++ /dev/null @@ -1,51 +0,0 @@ -import pygame -from pygame.locals import * - -class Button: - def __init__(self, text, position, size): - self.text = text - self.font = pygame.font.Font(None, 36) - self.position = position - self.size = size - self.rect = pygame.Rect(position, size) - self.clicked = False - - def draw(self, surface): - pygame.draw.rect(surface, (255, 255, 255), self.rect, 2) - text_surface = self.font.render(self.text, True, (255, 255, 255)) - text_rect = text_surface.get_rect(center=self.rect.center) - surface.blit(text_surface, text_rect) - - def is_clicked(self, pos): - if self.rect.collidepoint(pos): - self.clicked = True - return True - return False - -def main(): - pygame.init() - display = (800, 600) - screen = pygame.display.set_mode(display) - clock = pygame.time.Clock() - - button = Button("Click Me!", (300, 250), (200, 50)) - - running = True - while running: - for event in pygame.event.get(): - if event.type == QUIT: - running = False - elif event.type == MOUSEBUTTONDOWN: - if event.button == 1: - if button.is_clicked(event.pos): - print("Button Clicked!") - - screen.fill((0, 0, 0)) - button.draw(screen) - pygame.display.flip() - clock.tick(60) - - pygame.quit() - -if __name__ == "__main__": - main() diff --git a/main.py b/main.py index 9006f51..e0fe266 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,8 @@ 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 +from mesh_modules import simple_mesh, vesta_mesh, interactor_mesh + # main, draw_widget, gl_widget @@ -44,7 +45,8 @@ class MainWindow(QMainWindow): self.ui.sketch_list.itemChanged.connect(self.draw_mesh) ### Sketches - self.ui.pb_origin_wp.pressed.connect(self.add_new_sketch) + self.ui.pb_origin_wp.pressed.connect(self.add_new_sketch_origin) + self.ui.pb_origin_face.pressed.connect(self.add_new_sketch_wp) self.ui.pb_nw_sktch.pressed.connect(self.add_sketch) self.ui.pb_del_sketch.pressed.connect(self.del_sketch) @@ -66,10 +68,21 @@ class MainWindow(QMainWindow): self.sketchWidget.constrain_done.connect(self.draw_op_complete) - def add_new_sketch(self): + def add_new_sketch_origin(self): self.sketchWidget.clear_sketch() self.sketchWidget.create_workplane() + def add_new_sketch_wp(self): + self.sketchWidget.clear_sketch() + edges = [((-158.0, -20.0, -25.0), (286.0, -195.0, -25.0)), ((-158.0, -20.0, 25.0), (-158.0, -20.0, -25.0))] + #edges = self.custom_3D_Widget.access_selected_points + normal = self.custom_3D_Widget.selected_normal + + self.sketchWidget.create_workplane_projected() + self.sketchWidget.create_proj_lines(edges) + + #self.sketchWidget.create_workplane_space(edges, normal) + def act_line_mode(self): if not self.ui.pb_linetool.isChecked(): self.sketchWidget.mouse_mode = 'line' @@ -77,7 +90,6 @@ class MainWindow(QMainWindow): self.sketchWidget.mouse_mode = None self.sketchWidget.line_draw_buffer = [None, None] - def act_constrain_pt_pt_mode(self): if not self.ui.pb_con_ptpt.isChecked(): self.sketchWidget.mouse_mode = 'pt_pt' @@ -126,24 +138,24 @@ class MainWindow(QMainWindow): self.sketchWidget.mouse_mode = None self.sketchWidget.reset_buffers() - 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 calc_sketch_projection_3d(self, lines, z_origin, depth): + print(f"Gemetries {lines}, {z_origin}, {depth}") + + edges = interactor_mesh.generate_mesh(lines, z_origin, depth) + print("final_mesh", edges) + self.custom_3D_Widget.load_interactor_mesh(edges) 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() - #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) + + vesta = vesta_mesh + model_data = vesta.generate_mesh_from_sdf(model, resolution=64, threshold=0) + + vertices, faces = model_data + vesta.save_mesh_as_stl(vertices, faces, 'test.stl') + self.custom_3D_Widget.render_from_points_direct_with_faces(vertices, faces) def on_item_changed(self, current_item, previous_item): if current_item: @@ -158,6 +170,19 @@ class MainWindow(QMainWindow): return points_for_sdf + def convert_lines_for_interactor(self): + points_for_interact = [] + for point_to_poly in self.sketchWidget.slv_lines_main: + start, end = point_to_poly['ui_points'] + start_draw = self.translate_points_tup(start) + end_draw = self.translate_points_tup(end) + line = start_draw, end_draw + points_for_interact.append(line) + + print("packed_lines", points_for_interact) + + return points_for_interact + def add_sketch(self): name = f"sketch-{str(names.get_first_name())}" @@ -226,6 +251,7 @@ class MainWindow(QMainWindow): print(f"Item '{item_name}' not found in either 'sketch' or 'operation' dictionary.") else: print("No item selected.") + def update_body(self): pass @@ -258,13 +284,12 @@ class MainWindow(QMainWindow): selected = self.ui.sketch_list.currentItem() name = selected.text() points = self.model['sketch'][name]['sketch_points'] + lines = self.convert_lines_for_interactor() if points[-1] == points[0]: #detect loop that causes problems in mesh generation del points[-1] - print(points) - length, ok = QInputDialog.getDouble(self, 'Extrude Length', 'Enter a mm value:', decimals=2) #TODO : Implement cancel @@ -278,20 +303,20 @@ class MainWindow(QMainWindow): 'type': 'extrude', 'sdf_object': f, } - print(element) + #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.draw_mesh() - #self.calc_sketch_projection_3d(length) + self.calc_sketch_projection_3d(lines, 0, length) + #self.draw_mesh() def send_cut(self): name = self.ui.body_list.currentItem().text() points = self.model['operation'][name]['sdf_object'] self.list_selected.append(points) - print(self.list_selected) + #print(self.list_selected) if len(self.list_selected) > 1: geo = Geometry() @@ -308,7 +333,7 @@ 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.draw_mesh() + #self.draw_mesh() else: print("mindestens 2!") diff --git a/mesh_modules/interactor_mesh.py b/mesh_modules/interactor_mesh.py new file mode 100644 index 0000000..b5397b2 --- /dev/null +++ b/mesh_modules/interactor_mesh.py @@ -0,0 +1,44 @@ +# Draw simple boundary based on the lines and depth + +def generate_mesh(lines: list, z_origin: float, depth: float, symmetric: bool = True): + if symmetric: + depth1 = depth/2 + depth2 = -depth/2 + origin = create_3D(lines, z_origin, depth1) + extruded = create_3D(lines, z_origin, depth2) + else: + origin = create_3D(lines, z_origin, 0) + extruded = create_3D(lines, z_origin, depth) + + vert_lines = create_vert_lines(origin, extruded) + + print(f"Result = {origin} / {extruded} / {vert_lines}") + + return origin + vert_lines + extruded + + +def create_vert_lines(origin, extruded): + vert_lines = [] + for d3_point_o, d3point_e in zip(origin, extruded): + for sp3d_1, sp3d_2 in zip(d3_point_o, d3point_e): + new_line = sp3d_1, sp3d_2 + vert_lines.append(new_line) + return vert_lines + + +def create_3D(lines, z_origin, depth): + line_loop = [] + for coordinate2d in lines: + start, end = coordinate2d + + xs,ys = start + coordinate3d_start_orig = xs, ys, z_origin + depth + + xe, ye = end + coordinate3d_end_orig = xe, ye, z_origin + depth + + line3d_orig = coordinate3d_start_orig, coordinate3d_end_orig + + line_loop.append(line3d_orig) + + return line_loop diff --git a/mesh_modules/simple_mesh.py b/mesh_modules/simple_mesh.py index e7a181c..48fc829 100644 --- a/mesh_modules/simple_mesh.py +++ b/mesh_modules/simple_mesh.py @@ -1,9 +1,172 @@ import numpy as np -from scipy.spatial import ConvexHull -from stl import mesh +from scipy.spatial import Delaunay, ConvexHull +from shapely.geometry import Polygon, Point -def generate_mesh(points, depth): +def alpha_shape(points, alpha): + """ + Compute the alpha shape (concave hull) of a set of points. + """ + + def add_edge(edges, edge_points, points, i, j): + """Add a line between the i-th and j-th points if not in the list already""" + if (i, j) in edges or (j, i) in edges: + return + edges.add((i, j)) + edge_points.append(points[[i, j]]) + + tri = Delaunay(points) + edges = set() + edge_points = [] + + # Loop over triangles: + for ia, ib, ic in tri.simplices: + pa = points[ia] + pb = points[ib] + pc = points[ic] + # Lengths of sides of triangle + a = np.sqrt((pa[0] - pb[0]) ** 2 + (pa[1] - pb[1]) ** 2) + b = np.sqrt((pb[0] - pc[0]) ** 2 + (pb[1] - pc[1]) ** 2) + c = np.sqrt((pc[0] - pa[0]) ** 2 + (pc[1] - pa[1]) ** 2) + # Semiperimeter of triangle + s = (a + b + c) / 2.0 + # Area of triangle by Heron's formula + area = np.sqrt(s * (s - a) * (s - b) * (s - c)) + circum_r = a * b * c / (4.0 * area) + # Here's the radius filter. + if circum_r < 1.0 / alpha: + add_edge(edges, edge_points, points, ia, ib) + add_edge(edges, edge_points, points, ib, ic) + add_edge(edges, edge_points, points, ic, ia) + + m = np.array(edge_points) + return m + + +def generate_mesh(points, depth, alpha=0.1): + """ + Generate a mesh by extruding a 2D shape along the Z-axis, automatically detecting holes. + + :param points: List of (x, y) tuples representing all points of the 2D shape, including potential holes. + :param depth: Extrusion depth along the Z-axis. + :param alpha: Alpha value for the alpha shape algorithm (controls the "tightness" of the boundary). + :return: Tuple of vertices and faces. + """ + # Convert points to a numpy array + points_2d = np.array(points) + + # Compute the alpha shape (outer boundary) + boundary_edges = alpha_shape(points_2d, alpha) + + # Create a Polygon from the boundary + boundary_polygon = Polygon(boundary_edges) + + # Separate points into boundary and interior + boundary_points = [] + interior_points = [] + for point in points: + if Point(point).touches(boundary_polygon) or Point(point).within(boundary_polygon): + if Point(point).touches(boundary_polygon): + boundary_points.append(point) + else: + interior_points.append(point) + + # Perform Delaunay triangulation on all points + tri = Delaunay(points_2d) + + # Generate the top and bottom faces + bottom_face = np.hstack((tri.points, np.zeros((tri.points.shape[0], 1)))) + top_face = np.hstack((tri.points, np.ones((tri.points.shape[0], 1)) * depth)) + + # Combine top and bottom vertices + vertices_array = np.vstack((bottom_face, top_face)) + + # Create faces + faces = [] + + # Bottom face triangulation + for simplex in tri.simplices: + faces.append(simplex.tolist()) + + # Top face triangulation (with an offset) + top_offset = len(tri.points) + for simplex in tri.simplices: + faces.append([i + top_offset for i in simplex]) + + # Side faces for the outer boundary + for i in range(len(boundary_points)): + next_i = (i + 1) % len(boundary_points) + current = points.index(boundary_points[i]) + next_point = points.index(boundary_points[next_i]) + faces.append([current, top_offset + current, top_offset + next_point]) + faces.append([current, top_offset + next_point, next_point]) + + # Convert vertices to the desired format: list of tuples + vertices = [tuple(vertex) for vertex in vertices_array] + + return vertices, faces + +def generate_mesh_wholes(points, holes, depth): + """ + Generate a mesh by extruding a 2D shape along the Z-axis, including holes. + + :param points: List of (x, y) tuples representing the outer boundary of the 2D shape. + :param holes: List of lists, where each inner list contains (x, y) tuples representing a hole. + :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) + + # Prepare points for triangulation + triangulation_points = points_2d.tolist() + for hole in holes: + triangulation_points.extend(hole) + + # Perform Delaunay triangulation + tri = Delaunay(np.array(triangulation_points)) + + # Generate the top and bottom faces + bottom_face = np.hstack((tri.points, np.zeros((tri.points.shape[0], 1)))) + top_face = np.hstack((tri.points, np.ones((tri.points.shape[0], 1)) * depth)) + + # Combine top and bottom vertices + vertices_array = np.vstack((bottom_face, top_face)) + + # Create faces + faces = [] + + # Bottom face triangulation + for simplex in tri.simplices: + faces.append(simplex.tolist()) + + # Top face triangulation (with an offset) + top_offset = len(tri.points) + for simplex in tri.simplices: + faces.append([i + top_offset for i in simplex]) + + # Side faces + for i in range(len(points)): + next_i = (i + 1) % len(points) + faces.append([i, top_offset + i, top_offset + next_i]) + faces.append([i, top_offset + next_i, next_i]) + + # Side faces for holes + start_index = len(points) + for hole in holes: + for i in range(len(hole)): + current = start_index + i + next_i = start_index + (i + 1) % len(hole) + faces.append([current, top_offset + next_i, top_offset + current]) + faces.append([current, next_i, top_offset + next_i]) + start_index += len(hole) + + # Convert vertices to the desired format: list of tuples + vertices = [tuple(vertex) for vertex in vertices_array] + + return vertices, faces + +def generate_mesh_simple(points, depth): """ Generate a mesh by extruding a 2D shape along the Z-axis. diff --git a/mesh_modules/vesta_mesh.py b/mesh_modules/vesta_mesh.py new file mode 100644 index 0000000..7116742 --- /dev/null +++ b/mesh_modules/vesta_mesh.py @@ -0,0 +1,119 @@ +import numpy as np +from skimage import measure +import multiprocessing +from functools import partial +from multiprocessing.pool import ThreadPool +import itertools +import time + + +def _cartesian_product(*arrays): + la = len(arrays) + dtype = np.result_type(*arrays) + arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype) + for i, a in enumerate(np.ix_(*arrays)): + arr[..., i] = a + return arr.reshape(-1, la) + + +class VESTA: + def __init__(self, sdf, bounds=None, resolution=64, threshold=0.0, workers=None): + self.sdf = sdf + self.bounds = bounds + self.resolution = resolution + self.threshold = threshold + self.workers = workers or multiprocessing.cpu_count() + + def _estimate_bounds(self): + s = 16 + x0 = y0 = z0 = -1e9 + x1 = y1 = z1 = 1e9 + prev = None + for i in range(32): + X = np.linspace(x0, x1, s) + Y = np.linspace(y0, y1, s) + Z = np.linspace(z0, z1, s) + d = np.array([X[1] - X[0], Y[1] - Y[0], Z[1] - Z[0]]) + threshold = np.linalg.norm(d) / 2 + if threshold == prev: + break + prev = threshold + P = _cartesian_product(X, Y, Z) + volume = self.sdf(P).reshape((len(X), len(Y), len(Z))) + where = np.argwhere(np.abs(volume) <= threshold) + if where.size == 0: + continue + x1, y1, z1 = (x0, y0, z0) + where.max(axis=0) * d + d / 2 + x0, y0, z0 = (x0, y0, z0) + where.min(axis=0) * d - d / 2 + if prev is None: + raise ValueError("Failed to estimate bounds. No points found within any threshold.") + return ((x0, y0, z0), (x1, y1, z1)) + + def _vesta_worker(self, chunk): + x0, x1, y0, y1, z0, z1 = chunk + X = np.linspace(x0, x1, self.resolution) + Y = np.linspace(y0, y1, self.resolution) + Z = np.linspace(z0, z1, self.resolution) + P = _cartesian_product(X, Y, Z) + V = self.sdf(P).reshape((self.resolution, self.resolution, self.resolution)) + + try: + verts, faces, _, _ = measure.marching_cubes(V, self.threshold) + except RuntimeError: + # Return empty arrays if marching_cubes fails + return np.array([]), np.array([]) + + # Scale and translate vertices to match the chunk's bounds + verts = verts / (self.resolution - 1) + verts[:, 0] = verts[:, 0] * (x1 - x0) + x0 + verts[:, 1] = verts[:, 1] * (y1 - y0) + y0 + verts[:, 2] = verts[:, 2] * (z1 - z0) + z0 + + return verts, faces + + def _merge_meshes(self, results): + all_verts = [] + all_faces = [] + offset = 0 + for verts, faces in results: + if len(verts) > 0 and len(faces) > 0: + all_verts.append(verts) + all_faces.append(faces + offset) + offset += len(verts) + if not all_verts or not all_faces: + return np.array([]), np.array([]) + return np.vstack(all_verts), np.vstack(all_faces) + + def generate_mesh(self): + if self.bounds is None: + self.bounds = self._estimate_bounds() + + (x0, y0, z0), (x1, y1, z1) = self.bounds + chunks = [ + (x0, x1, y0, y1, z0, z1) + ] + + with ThreadPool(self.workers) as pool: + results = pool.map(self._vesta_worker, chunks) + + verts, faces = self._merge_meshes(results) + return verts, faces + + +def generate_mesh_from_sdf(sdf, bounds=None, resolution=64, threshold=0.0, workers=None): + vesta = VESTA(sdf, bounds, resolution, threshold, workers) + return vesta.generate_mesh() + + +# Helper function to save the mesh as an STL file +def save_mesh_as_stl(vertices, faces, filename): + from stl import mesh + + # Create the mesh + cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype)) + for i, f in enumerate(faces): + for j in range(3): + cube.vectors[i][j] = vertices[f[j], :] + + # Write the mesh to file + cube.save(filename) \ No newline at end of file diff --git a/model.stl b/model.stl deleted file mode 100644 index a024282..0000000 Binary files a/model.stl and /dev/null differ diff --git a/side_fluency.py b/side_fluency.py deleted file mode 100644 index 9017fc6..0000000 --- a/side_fluency.py +++ /dev/null @@ -1,170 +0,0 @@ -import sys -from PySide6.QtOpenGLWidgets import QOpenGLWidget -from PySide6.QtWidgets import (QApplication, QMainWindow, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QGroupBox, - QTextEdit, QSizePolicy) -from PySide6.QtCore import QSize, Qt, QPoint -from OpenGL.GL import * -from OpenGL.GLU import * -from stl import mesh - - -class OpenGLWidget(QOpenGLWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.stl_file = "out.stl" # Replace with your STL file path - self.lastPos = QPoint() - self.xRot = 0 - self.yRot = 0 - self.zoom = -10.0 - - def load_stl(self, filename): - try: - stl_mesh = mesh.Mesh.from_file(filename) - return stl_mesh.vectors - except FileNotFoundError: - print(f"Error: File {filename} not found.") - except Exception as e: - print(f"Error loading {filename}: {e}") - return [] - - def initializeGL(self): - glClearColor(0, 0, 0, 1) - glEnable(GL_DEPTH_TEST) - - def resizeGL(self, w, h): - glViewport(0, 0, w, h) - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - gluPerspective(45, w/h, 0.1, 100.0) - glMatrixMode(GL_MODELVIEW) - - def paintGL(self): - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - glLoadIdentity() - glTranslatef(0.0, 0.0, self.zoom) - glRotatef(self.xRot, 1.0, 0.0, 0.0) - glRotatef(self.yRot, 0.0, 1.0, 0.0) - glColor3f(1.0, 1.0, 1.0) - - mesh_data = self.load_stl(self.stl_file) - if mesh_data.any(): - self.draw_stl(mesh_data) - - 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, 0.6)) - - glBegin(GL_TRIANGLES) - for triangle in vertices: - for vertex in triangle: - glVertex3fv(vertex) - glEnd() - - # Draw outer vertices as points - glDisable(GL_LIGHTING) - glColor3f(1.0, 0.0, 0.0) # Set color to red - glPointSize(5.0) # Set point size - glBegin(GL_POINTS) - for triangle in vertices: - for vertex in triangle: - glVertex3fv(vertex) - glEnd() - - def mousePressEvent(self, event): - self.lastPos = event.pos() - - def mouseMoveEvent(self, event): - dx = event.x() - self.lastPos.x() - dy = event.y() - self.lastPos.y() - - if event.buttons() & Qt.LeftButton: - self.xRot += 0.5 * dy - self.yRot += 0.5 * dx - self.update() - - self.lastPos = event.pos() - - def wheelEvent(self, event): - delta = event.angleDelta().y() - self.zoom += delta / 120 - self.update() - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - - self.setWindowTitle("fluencyCAD") - self.height = 1080 - self.width = 1920 - self.setFixedSize(QSize(self.width, self.height)) - - # Main central widget - central_widget = QWidget() - self.setCentralWidget(central_widget) - - layout = QHBoxLayout(central_widget) - - # Left tools area - left_tools_layout = QVBoxLayout() - - self.left_tools = QGroupBox("Left Tools") - self.left_tools.setFixedSize(100, self.height) - - self.draw_rectangle = QPushButton("Draw Rectangle") - self.draw_rectangle.setFixedSize(80, 30) - left_tools_layout.addWidget(self.draw_rectangle) - - self.draw_rectangle = QPushButton("Draw Rectangle2") - self.draw_rectangle.setFixedSize(80, 30) - left_tools_layout.addWidget(self.draw_rectangle) # Align the button to the top - - self.draw_rectangle = QPushButton("Draw Rectangle3") - self.draw_rectangle.setFixedSize(80, 30) - left_tools_layout.addWidget(self.draw_rectangle) - - self.left_tools.setLayout(left_tools_layout) - self.left_tools.setAlignment(Qt.AlignTop) - layout.addWidget(self.left_tools) - - # Center OpenGL widget and QTextEdit - center_layout = QVBoxLayout() - - self.openGLWidget = OpenGLWidget() - center_layout.addWidget(self.openGLWidget) - self.openGLWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - self.enter_code = QTextEdit() - self.enter_code.setFixedSize(QSize(self.width, self.height // 4)) - center_layout.addWidget(self.enter_code) - - self.code_tools = QGroupBox("Code Tools") - self.code_tools.setFixedSize(QSize(self.width, self.height // 18)) - - code_tools_layout = QHBoxLayout() # Layout for code tools - - self.apply_code = QPushButton("Apply Code") # Creating QPushButton - self.apply_code.setFixedSize(80,30) - - code_tools_layout.addWidget(self.apply_code) # Adding QPushButton to QHBoxLayout - - self.code_tools.setLayout(code_tools_layout) # Setting QHBoxLayout to QGroupBox - self.code_tools.setAlignment(Qt.AlignLeft) - - center_layout.addWidget(self.code_tools) # Adding QGroupBox to the center layout - - layout.addLayout(center_layout) # Adding center layout to the main layout - - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = MainWindow() - window.show() - sys.exit(app.exec()) - diff --git a/vulkan.py b/vulkan.py deleted file mode 100644 index c0b2dfe..0000000 --- a/vulkan.py +++ /dev/null @@ -1,37 +0,0 @@ -import vtk - -def loadStl(fname): - """Load the given STL file, and return a vtkPolyData object for it.""" - reader = vtk.vtkSTLReader() - reader.SetFileName(fname) - reader.Update() - polydata = reader.GetOutput() - return polydata - -def polyDataToActor(polydata): - """Wrap the provided vtkPolyData object in a mapper and an actor, returning the actor.""" - mapper = vtk.vtkPolyDataMapper() - mapper.SetInputData(polydata) - actor = vtk.vtkActor() - actor.SetMapper(mapper) - return actor - -# Create a rendering window and renderer -ren = vtk.vtkRenderer() -renWin = vtk.vtkRenderWindow() -renWin.AddRenderer(ren) - -# Create a RenderWindowInteractor to permit manipulating the camera -iren = vtk.vtkRenderWindowInteractor() -iren.SetRenderWindow(renWin) -style = vtk.vtkInteractorStyleTrackballCamera() -iren.SetInteractorStyle(style) - -stlFilename = "out.stl" -polydata = loadStl(stlFilename) -ren.AddActor(polyDataToActor(polydata)) - -# Enable user interface interactor -iren.Initialize() -renWin.Render() -iren.Start() \ No newline at end of file