# nuitka-project: --plugin-enable=pyside6 # nuitka-project: --plugin-enable=numpy # nuitka-project: --standalone # nuitka-project: --macos-create-app-bundle import uuid import names from PySide6.QtCore import Qt, QPoint, Signal, QSize from PySide6.QtWidgets import QApplication, QMainWindow, QSizePolicy, QInputDialog, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QDoubleSpinBox, QCheckBox, QPushButton from Gui import Ui_fluencyCAD # Import the generated GUI module 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, vesta_mesh, interactor_mesh from dataclasses import dataclass, field # main, draw_widget, gl_widget class ExtrudeDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle('Extrude Options') def create_hline(): line = QLabel() line.setStyleSheet("border-top: 1px solid #cccccc;") # Light grey line line.setFixedHeight(1) return line layout = QVBoxLayout() # Length input length_layout = QHBoxLayout() length_label = QLabel('Extrude Length (mm):') self.length_input = QDoubleSpinBox() self.length_input.setDecimals(2) self.length_input.setRange(0, 1000) # Adjust range as needed length_layout.addWidget(length_label) length_layout.addWidget(self.length_input) # Symmetric checkbox self.symmetric_checkbox = QCheckBox('Symmetric Extrude') self.invert_checkbox = QCheckBox('Invert Extrusion') self.cut_checkbox = QCheckBox('Perform Cut') self.union_checkbox = QCheckBox('Combine') self.rounded_checkbox = QCheckBox('Round Edges') self.seperator = create_hline() # OK and Cancel buttons button_layout = QHBoxLayout() ok_button = QPushButton('OK') cancel_button = QPushButton('Cancel') ok_button.clicked.connect(self.accept) cancel_button.clicked.connect(self.reject) button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) # Add all widgets to main layout layout.addLayout(length_layout) layout.addWidget(self.seperator) layout.addWidget(self.cut_checkbox) layout.addWidget(self.union_checkbox) layout.addWidget(self.seperator) layout.addWidget(self.symmetric_checkbox) layout.addWidget(self.invert_checkbox) layout.addWidget(self.seperator) layout.addWidget(self.rounded_checkbox) layout.addLayout(button_layout) self.setLayout(layout) def get_values(self): return self.length_input.value(), self.symmetric_checkbox.isChecked() ,self.invert_checkbox.isChecked(), self.cut_checkbox.isChecked(), self.union_checkbox.isChecked(), self.rounded_checkbox.isChecked() class MainWindow(QMainWindow): send_command = Signal(str) def __init__(self): super().__init__() # Set up the UI from the generated GUI module self.ui = Ui_fluencyCAD() self.ui.setupUi(self) self.custom_3D_Widget = VTKWidget() layout = self.ui.gl_box.layout() layout.addWidget(self.custom_3D_Widget) size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) #self.custom_3D_Widget.setSizePolicy(size_policy) self.sketchWidget = SketchWidget() layout2 = self.ui.sketch_tab.layout() # Get the layout of self.ui.gl_canvas layout2.addWidget(self.sketchWidget) size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.sketchWidget.setSizePolicy(size_policy) ### Main Model self.model = { 'sketches': {}, 'operation': {}, } self.list_selected = [] #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.draw_mesh) ### Sketches 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) self.ui.pb_edt_sktch.pressed.connect(self.edit_sketch) self.ui.pb_flip_face.pressed.connect(self.on_flip_face) ###Modes self.ui.pb_linetool.pressed.connect(self.act_line_mode) self.ui.pb_con_ptpt.pressed.connect(self.act_constrain_pt_pt_mode) self.ui.pb_con_line.pressed.connect(self.act_constrain_pt_line_mode) self.ui.pb_con_horiz.pressed.connect(self.act_constrain_horiz_line_mode) self.ui.pb_con_vert.pressed.connect(self.act_constrain_vert_line_mode) self.ui.pb_con_dist.pressed.connect(self.act_constrain_distance_mode) self.ui.pb_con_mid.pressed.connect(self.act_constrain_mid_point_mode) ### Operations self.ui.pb_extrdop.pressed.connect(self.send_extrude) self.ui.pb_cutop.pressed.connect(self.send_cut) self.ui.pb_del_body.pressed.connect(self.del_body) self.sketchWidget.constrain_done.connect(self.draw_op_complete) self.setFocusPolicy(Qt.StrongFocus) self.send_command.connect(self.custom_3D_Widget.on_receive_command) self.ui.actionNew_Project.triggered.connect(self.new_project) self.project = Project() self.new_project() """Project -> Timeline -> Component -> Sketch -> Body / Interactor -> Connector -> Assembly -> PB Render""" def on_flip_face(self): self.send_command.emit("flip") def act_line_mode(self): if not self.ui.pb_linetool.isChecked(): self.sketchWidget.mouse_mode = 'line' else: 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' else: self.sketchWidget.mouse_mode = None def act_constrain_pt_line_mode(self): if not self.ui.pb_con_line.isChecked(): self.sketchWidget.mouse_mode = 'pt_line' else: self.sketchWidget.mouse_mode = None def act_constrain_horiz_line_mode(self): if not self.ui.pb_con_horiz.isChecked(): self.sketchWidget.mouse_mode = 'horiz' else: self.sketchWidget.mouse_mode = None def act_constrain_vert_line_mode(self): if not self.ui.pb_con_vert.isChecked(): self.sketchWidget.mouse_mode = 'vert' else: self.sketchWidget.mouse_mode = None def act_constrain_distance_mode(self): if not self.ui.pb_con_dist.isChecked(): self.sketchWidget.mouse_mode = 'distance' else: self.sketchWidget.mouse_mode = None def act_constrain_mid_point_mode(self): if not self.ui.pb_con_mid.isChecked(): self.sketchWidget.mouse_mode = 'pb_con_mid' else: self.sketchWidget.mouse_mode = None def draw_op_complete(self): # safely disable the line modes self.ui.pb_linetool.setChecked(False) self.ui.pb_con_ptpt.setChecked(False) self.ui.pb_con_line.setChecked(False) self.ui.pb_con_dist.setChecked(False) self.ui.pb_con_mid.setChecked(False) self.ui.pb_con_perp.setChecked(False) self.sketchWidget.mouse_mode = None self.sketchWidget.reset_buffers() def draw_mesh(self): name = self.ui.body_list.currentItem().text() print("selected_for disp", name) model = self.project.timeline[-1].body[name].sdf_body 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: name = current_item.text() #self.view_update() print(f"Selected item: {name}") def new_project(self): print("New project") timeline = [] self.project.timeline = timeline self.new_component() def new_component(self): print("Compo") compo = Component() compo.id = "New Compo" compo.descript = "Initial Component" compo.sketches = {} compo.body = {} self.project.timeline.append(compo) # Create a horizontal layout horizontal_layout = QHBoxLayout() # Set the layout for the timeline_box QFrame self.ui.timeline_box.setLayout(horizontal_layout) # Create the button button = QPushButton() button.setToolTip(compo.id) button.setText(str(len(self.project.timeline))) # Set the button's fixed size button.setFixedSize(QSize(40, 40)) # Add the button to the horizontal layout horizontal_layout.addWidget(button) # Set the alignment of the layout to left horizontal_layout.setAlignment(Qt.AlignLeft) def add_new_sketch_origin(self): name = f"sketches-{str(names.get_first_name())}" sketch = Sketch() sketch.id = name sketch.origin = [0,0,0] sketch.slv_points = [] sketch.slv_lines = [] sketch.proj_points = [] sketch.proj_lines = [] self.sketchWidget.reset_buffers() self.sketchWidget.set_sketch(sketch) def add_new_sketch_wp(self): name = f"sketches-{str(names.get_first_name())}" sketch = Sketch() sketch.id = name sketch.origin = self.custom_3D_Widget.centroid sketch.normal = self.custom_3D_Widget.selected_normal sketch.slv_points = [] sketch.slv_lines = [] sketch.proj_points = self.custom_3D_Widget.project_tosketch_points sketch.proj_lines = self.custom_3D_Widget.project_tosketch_lines self.sketchWidget.reset_buffers() self.sketchWidget.set_sketch(sketch) self.sketchWidget.create_workplane_projected() self.sketchWidget.convert_proj_points() self.sketchWidget.convert_proj_lines() self.sketchWidget.update() # CLear all selections after it has been projected self.custom_3D_Widget.project_tosketch_points.clear() self.custom_3D_Widget.project_tosketch_lines.clear() self.custom_3D_Widget.clear_actors_projection() self.custom_3D_Widget.clear_actors_normals() def add_sketch(self): sketch = self.sketchWidget.get_sketch() sketch.convert_points_for_sdf() self.project.timeline[-1].sketches[sketch.id] = sketch self.ui.sketch_list.addItem(sketch.id) self.ui.pb_linetool.setChecked(False) self.sketchWidget.line_mode = False items = self.ui.sketch_list.findItems(sketch.id, Qt.MatchExactly)[0] self.ui.sketch_list.setCurrentItem(items) def edit_sketch(self): name = self.ui.sketch_list.currentItem().text() selected = self.ui.sketch_list.currentItem() name = selected.text() # TODO: add selected element from timeline sel_compo = self.project.timeline[-1] sketch = sel_compo.sketches[name] self.sketchWidget.set_sketch(sketch) self.sketchWidget.update() def del_sketch(self): # Old print("Deleting") name = self.ui.sketch_list.currentItem() # Get the current item print(self.model) if name is not None: item_name = name.text() print("obj_name", item_name) # Check if the 'sketches' key exists in the model dictionary if 'sketches' in self.model and item_name in self.model['sketches']: if self.model['sketches'][item_name]['id'] == item_name: row = self.ui.sketch_list.row(name) # Get the row of the current item self.ui.sketch_list.takeItem(row) # Remove the item from the list widget self.sketchWidget.clear_sketch() self.model['sketches'].pop(item_name) # Remove the item from the sketches dictionary print(f"Removed sketches: {item_name}") # Check if the 'operation' key exists in the model dictionary elif 'operation' in self.model and item_name in self.model['operation']: if self.model['operation'][item_name]['id'] == item_name: row = self.ui.sketch_list.row(name) # Get the row of the current item self.ui.sketch_list.takeItem(row) # Remove the item from the list widget self.sketchWidget.clear_sketch() self.model['operation'].pop(item_name) # Remove the item from the operation dictionary print(f"Removed operation: {item_name}") else: print(f"Item '{item_name}' not found in either 'sketches' or 'operation' dictionary.") else: print("No item selected.") def update_body(self): pass def del_body(self): print("Deleting") name = self.ui.body_list.currentItem() # Get the current item if name is not None: item_name = name.text() print("obj_name", item_name) # Check if the 'operation' key exists in the model dictionary if 'operation' in self.model and item_name in self.model['operation']: if self.model['operation'][item_name]['id'] == item_name: row = self.ui.body_list.row(name) # Get the row of the current item 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.custom_3D_Widget.clear_mesh() def send_extrude(self): # Dialog input is_symmetric = None length = None invert = None selected = self.ui.sketch_list.currentItem() name = selected.text() # TODO: add selected element from timeline sel_compo = self.project.timeline[-1] sketch = sel_compo.sketches[name] points = sketch.sdf_points if points[-1] == points[0]: #detect loop that causes problems in mesh generation del points[-1] dialog = ExtrudeDialog(self) if dialog.exec(): length, is_symmetric, invert, cut, union_with, rounded = dialog.get_values() print(f"Extrude length: {length}, Symmetric: {is_symmetric} Invert: {invert}") else: length = 0 print("Extrude cancelled") normal = self.custom_3D_Widget.selected_normal #print("Normie enter", normal) if normal is None: normal = [0, 0, 1] centroid = self.custom_3D_Widget.centroid if centroid is None: centroid = [0, 0, 0] else: centroid = list(centroid) #print("This centroid ", centroid) sketch.origin = centroid sketch.normal = normal f = sketch.extrude(length, is_symmetric, invert, 0) name_op = f"extrd-{name}" sel_compo.body body = Body() body.sketch = sketch #we add the sketches for reference here body.id = name_op body.sdf_body = f ### Interactor interactor = Interactor() interactor.add_lines_for_interactor(sketch.slv_lines) if not invert: edges = interactor_mesh.generate_mesh(interactor.lines, 0, length) else: edges = interactor_mesh.generate_mesh(interactor.lines, 0, -length) body.interactor = interactor sel_compo.body[name_op] = body offset_vector = interactor.vector_to_centroid(None, centroid, normal) #print("off_ved", offset_vector) if len(offset_vector) == 0 : offset_vector = [0, 0, 0] self.custom_3D_Widget.load_interactor_mesh(edges, offset_vector) 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() def send_cut(self): name = self.ui.body_list.currentItem().text() points = self.model['operation'][name]['sdf_object'] self.list_selected.append(points) if len(self.list_selected) == 2: geo = Geometry() f = geo.cut_shapes(self.list_selected[0], self.list_selected[1] ) element = { 'id': name, 'type': 'cut', 'sdf_object': f, } name_op = f"cut-{name}" self.model['operation'][name_op] = element self.ui.body_list.addItem(name_op) items = self.ui.body_list.findItems(name_op, Qt.MatchExactly) self.ui.body_list.setCurrentItem(items[-1]) self.custom_3D_Widget.clear_body_actors() self.draw_mesh() elif len(self.list_selected) > 2: self.list_selected.clear() else: print("mindestens 2!") def load_and_render(self, file): self.custom_3D_Widget.load_stl(file) self.custom_3D_Widget.update() @dataclass class Timeline: """Timeline """ timeline: list = None """add to time, remove from time, """ class Assembly: """Connecting Components in 3D space based on slvs solver""" @dataclass class Component: """The base container combining all related elements id : The unique ID sketches : the base sketches, bodys can contain additonal sketches for features interactor : A smiplified model used as interactor body : The body class that contains the actual 3d information connector : Vector and Nomral information for assembly descript : a basic description materil : Speicfy a material for pbr rendering """ id = None sketches: dict = None body: dict = None connector = None # Description descript = None # PBR material = None class Connector: """An Element that contains vectors and or normals as connection points. These connection points can exist independently of bodies and other elements""" id = None vector = None normal = None class Code: """A class that holds all information from the code based approach""" command_list = None def generate_mesh_from_code(self, code_text: str): local_vars = {} try: print(code_text) exec(code_text, globals(), local_vars) # Retrieve the result from the captured local variables result = local_vars.get('result') print("Result:", result) except Exception as e: print("Error executing code:", e) @dataclass class Sketch: """All of the 2D Information of a sketches""" id = None # Space Information origin = None slv_plane = None normal = None # Points in UI form the sketches widget ui_points: list = None ui_lines: list = None # Points cartesian coming as result of the solver slv_points: list = None slv_lines: list = None sdf_points: list = None # Points coming back from the 3D-Widget as projection to draw on proj_points: list = None proj_lines: list = None # Workingplane working_plane = None def translate_points_tup(self, point: QPoint): """QPoints from Display to mesh data input: Qpoints output: Tuple X,Y """ if isinstance(point, QPoint): return point.x(), point.y() def vector_to_centroid(self, shape_center, centroid, normal): if not shape_center: # Calculate the current center of the shape shape_center = [0, 0, 0] # Calculate the vector from the shape's center to the centroid center_to_centroid = np.array(centroid) - np.array(shape_center) # Project this vector onto the normal to get the required translation along the normal translation_along_normal = np.dot(center_to_centroid, normal) * normal return translation_along_normal def angle_between_normals(self, normal1, normal2): # Ensure the vectors are normalized n1 = normal1 / np.linalg.norm(normal1) n2 = normal2 / np.linalg.norm(normal2) # Compute the dot product dot_product = np.dot(n1, n2) # Clip the dot product to the valid range [-1, 1] dot_product = np.clip(dot_product, -1.0, 1.0) # Compute the angle in radians angle_rad = np.arccos(dot_product) # Convert to degrees if needed angle_deg = np.degrees(angle_rad) print("Angle deg", angle_deg) return angle_rad def offset_syn(self, f, length): f = f.translate((0,0, length / 2)) return f def distance(self, p1, p2): """Calculate the distance between two points.""" print("p1", p1) print("p2", p2) return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) def convert_points_for_sdf(self): points_for_sdf = [] for point_to_poly in self.slv_points: points_for_sdf.append(self.translate_points_tup(point_to_poly['ui_point'])) self.sdf_points = points_for_sdf def extrude(self, height: float, symet: bool = True, invert: bool = False, offset_length: float = None): """ Extrude a 2D shape into 3D, orient it along the normal, and position it relative to the centroid. """ # Normalize the normal vector normal = np.array(self.normal) normal = normal / np.linalg.norm(self.normal) # Create the 2D shape f = polygon(self.sdf_points) # Extrude the shape along the Z-axis f = f.extrude(height) # Center the shape along its extrusion axis f = f.translate((0, 0, height / 2)) # Orient the shape along the normal vector f = f.orient(normal) offset_vector = self.vector_to_centroid(None, self.origin, normal) # Adjust the offset vector by subtracting the inset distance along the normal direction adjusted_offset = offset_vector - (normal * height) if invert: # Translate the shape along the adjusted offset vector f = f.translate(adjusted_offset) else: f = f.translate(offset_vector) # If offset_length is provided, adjust the offset_vector if offset_length is not None: # Check if offset_vector is not a zero vector offset_vector_magnitude = np.linalg.norm(offset_vector) if offset_vector_magnitude > 1e-10: # Use a small threshold to avoid floating-point issues # Normalize the offset vector offset_vector_norm = offset_vector / offset_vector_magnitude # Scale the normalized vector by the desired length offset_vector = offset_vector_norm * offset_length f = f.translate(offset_vector) else: print("Warning: Offset vector has zero magnitude. Using original vector.") # Translate the shape along the adjusted offset vector return f @dataclass class Interactor: """Helper mesh consisting of edges for selection""" lines = None faces = None body = None def translate_points_tup(self, point: QPoint): """QPoints from Display to mesh data input: Qpoints output: Tuple X,Y """ if isinstance(point, QPoint): return point.x(), point.y() def vector_to_centroid(self, shape_center, centroid, normal): if not shape_center: # Calculate the current center of the shape shape_center = [0, 0, 0] # Calculate the vector from the shape's center to the centroid center_to_centroid = np.array(centroid) - np.array(shape_center) # Project this vector onto the normal to get the required translation along the normal translation_along_normal = np.dot(center_to_centroid, normal) * normal return translation_along_normal def add_lines_for_interactor(self, input_lines: list): """Expects slvs_lines main list""" points_for_interact = [] for point_to_poly in input_lines: start, end = point_to_poly['ui_points'] from_coord_start = window.sketchWidget.from_quadrant_coords_no_center(start) from_coord_end = window.sketchWidget.from_quadrant_coords_no_center(end) start_draw = self.translate_points_tup(from_coord_start) end_draw = self.translate_points_tup(from_coord_end) line = start_draw, end_draw points_for_interact.append(line) print("packed_lines", points_for_interact) self.lines = points_for_interact @dataclass class Body: """The actual body as sdf3 object""" id = None sketch = None height = None interactor = None sdf_body = None def mirror_body(self, sdf_object3d): f = sdf_object3d.rotate(pi) return f def cut_shapes(self, sdf_object1, sdf_object2): f = difference(sdf_object1, sdf_object2) # equivalent return f class Output: def export_mesh(self, sdf_object): """FINAL EXPORT""" result_points = sdf_object.generate() write_binary_stl('out.stl', result_points) def generate_mesh_from_code(self, code_text: str): local_vars = {} try: print(code_text) exec(code_text, globals(), local_vars) # Retrieve the result from the captured local variables result = local_vars.get('result') print("Result:", result) except Exception as e: print("Error executing code:", e) class Project: """Project -> Timeline -> Component -> Sketch -> Body / Interactor -> Connector -> Assembly -> PB Render""" timeline: Timeline = None assembly: Assembly = None if __name__ == "__main__": app = QApplication([]) window = MainWindow() window.show() app.exec()