# 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, QButtonGroup 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_widget_solve 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 -OLD ? """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_to_compo) 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.clicked.connect(self.sketchWidget.act_line_mode) self.ui.pb_con_ptpt.clicked.connect(self.sketchWidget.act_constrain_pt_pt_mode) self.ui.pb_con_line.clicked.connect(self.sketchWidget.act_constrain_pt_line_mode) self.ui.pb_con_horiz.clicked.connect(self.sketchWidget.act_constrain_horiz_line_mode) self.ui.pb_con_vert.clicked.connect(self.sketchWidget.act_constrain_vert_line_mode) self.ui.pb_con_dist.clicked.connect(self.sketchWidget.act_constrain_distance_mode) self.ui.pb_con_mid.clicked.connect(self.sketchWidget.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.ui.pb_enable_construct.clicked.connect(self.sketchWidget.on_construct_change) self.project = Project() self.new_project() ### COMPOS ### COMPOS self.ui.new_compo.pressed.connect(self.new_component) """Project -> (Timeline) -> Component -> Sketch -> Body / Interactor -> Connector -> Assembly -> PB Render""" def new_project(self): print("New project") timeline = [] self.project.timeline = timeline self.new_component() def new_component(self): print("Creating a new component...") # Lazily initialize self.compo_layout if it doesn't exist if not hasattr(self, 'compo_layout'): print("Initializing compo_layout...") self.compo_layout = QHBoxLayout() # Create a button group self.compo_group = QButtonGroup(self) self.compo_group.setExclusive(True) # Ensure exclusivity # Ensure the QGroupBox has a layout if not self.ui.compo_box.layout(): self.ui.compo_box.setLayout(QVBoxLayout()) # Set a default layout for QGroupBox # Add the horizontal layout to the QGroupBox's layout self.ui.compo_box.layout().addLayout(self.compo_layout) # Align the layout to the left self.compo_layout.setAlignment(Qt.AlignLeft) # Create and initialize a new Component compo = Component() compo.id = f"Component {len(self.project.timeline)}" compo.descript = "Initial Component" compo.sketches = {} compo.bodies = {} self.project.timeline.append(compo) # Create a button for the new component button = QPushButton() button.setToolTip(compo.id) button.setText(str(len(self.project.timeline))) button.setFixedSize(QSize(40, 40)) # Set button size button.setCheckable(True) #button.setAutoExclusive(True) button.released.connect(self.on_compo_change) button.setChecked(True) # Add button to the group self.compo_group.addButton(button) # Add the button to the layout self.compo_layout.addWidget(button) # We automatically switch to the new compo hence, refresh self.on_compo_change() print(f"Added component {compo.id} to the layout.") def get_activated_compo(self): # Iterate through all items in the layout total_elements = self.compo_layout.count() #print(total_elements) for i in range(total_elements): widget = self.compo_layout.itemAt(i).widget() # Get the widget at the index if widget: # Check if the widget is not None if isinstance(widget, QPushButton) and widget.isCheckable(): state = widget.isChecked() # Get the checked state print(f"{widget.text()} is {'checked' if state else 'unchecked'}.") if state: return i def add_new_sketch_origin(self): name = f"sketches-{str(names.get_first_name())}" sketch = Sketch() sketch.id = name sketch.origin = [0,0,0] self.sketchWidget.reset_buffers() self.sketchWidget.create_sketch(sketch) def add_new_sketch_wp(self): ## Sketch projected from 3d view into 2d 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.create_sketch(sketch) self.sketchWidget.create_workplane_projected() if not sketch.proj_lines: self.sketchWidget.convert_proj_points(sketch.proj_points) self.sketchWidget.convert_proj_lines(sketch.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_to_compo(self): """ Add sketch to component :return: """ sketch = Sketch() sketch_from_widget = self.sketchWidget.get_sketch() #Save original for editing later sketch.original_sketch = sketch_from_widget #Get parameters points = [point for point in sketch_from_widget.points if hasattr(point, 'is_helper') and not point.is_helper] sketch.convert_points_for_sdf(points) sketch.id = sketch_from_widget.id sketch.filter_lines_for_interactor(sketch_from_widget.lines) # Register sketch to timeline ### Add selection compo here compo_id = self.get_activated_compo() print("newsketch_name", sketch.id) self.project.timeline[compo_id].sketches[sketch.id] = sketch # Add Item to slection menu self.ui.sketch_list.addItem(sketch.id) # Deactivate drawing 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 on_compo_change(self): self.custom_3D_Widget.clear_body_actors() self.custom_3D_Widget.clear_actors_interactor() self.custom_3D_Widget.clear_actors_projection() compo_id = self.get_activated_compo() if compo_id is not None: self.ui.sketch_list.clear() self.ui.body_list.clear() print("id", compo_id) print("sketch_registry", self.project.timeline[compo_id].sketches) for sketch in self.project.timeline[compo_id].sketches: print(sketch) self.ui.sketch_list.addItem(sketch) for body in self.project.timeline[compo_id].bodies: self.ui.body_list.addItem(body) if self.project.timeline[compo_id].bodies: item = self.ui.body_list.findItems(body , Qt.MatchExactly)[0] self.ui.body_list.setCurrentItem(item) self.draw_mesh() selected = self.ui.body_list.currentItem() name = selected.text() edges = self.project.timeline[compo_id].bodies[name].interactor.edges offset_vec = self.project.timeline[compo_id].bodies[name].interactor.offset_vector self.custom_3D_Widget.load_interactor_mesh(edges, offset_vec) def edit_sketch(self): selected = self.ui.sketch_list.currentItem() name = selected.text() sel_compo = self.project.timeline[self.get_activated_compo()] sketch = sel_compo.sketches[name].original_sketch self.sketchWidget.set_sketch(sketch) self.sketchWidget.update() def del_sketch(self): selected = self.ui.sketch_list.currentItem() name = selected.text() sel_compo = self.project.timeline[self.get_activated_compo()] sketch = sel_compo.sketches[name] if sketch is not None: sel_compo.sketches.pop(name) row = self.ui.sketch_list.row(selected) # Get the row of the current item self.ui.sketch_list.takeItem(row) # Remove the item from the list widget self.sketchWidget.sketch = None print(sketch) else: print("No item selected.") def on_flip_face(self): self.send_command.emit("flip") 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) compo_id = self.get_activated_compo() model = self.project.timeline[compo_id].bodies[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 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() sel_compo = self.project.timeline[self.get_activated_compo()] #print(sel_compo) sketch = sel_compo.sketches[name] #print(sketch) 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) # Create body element and assign known stuff name_op = f"extrd-{name}" 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.interactor_lines) interactor.invert = invert if not invert: edges = interactor_mesh.generate_mesh(interactor.lines, 0, length) else: edges = interactor_mesh.generate_mesh(interactor.lines, 0, -length) sel_compo.bodies[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] interactor.edges = edges interactor.offset_vector = offset_vector body.interactor = interactor 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'] sel_compo = self.project.timeline[self.get_activated_compo()] points = sel_compo.bodies[]. self.list_selected.append(points)""" selected = self.ui.body_list.currentItem() name = selected.text() sel_compo = self.project.timeline[self.get_activated_compo()] # print(sel_compo) body = sel_compo.bodies[name] # print(sketch) self.list_selected.append(body.sdf_body) if len(self.list_selected) == 2: f = difference(self.list_selected[0], self.list_selected[1]) # equivalent element = { 'id': name, 'type': 'cut', 'sdf_object': f, } # Create body element and assign known stuff name_op = f"cut-{name}" body = Body() body.id = name_op body.sdf_body = f ## Add to component sel_compo.bodies[name_op] = body 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 """ ### Collection of the Components 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 bodies: 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""" # Save the incomng sketch from the 2D widget for late redit original_sketch = None 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 interactor_lines: 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): points_for_sdf = [] for point in points: if point.is_helper is False: print("point", point) points_for_sdf.append(self.translate_points_tup(point.ui_point)) self.sdf_points = points_for_sdf def filter_lines_for_interactor(self, lines): ### Filter lines that are not meant to be drawn for the interactor like contruction lines filtered_lines = [] for line in lines: if not line.is_helper: filtered_lines.append(line) self.interactor_lines = filtered_lines 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 offset_vector = None edges = 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): """Takes Line2D objects from the sketch widget and preparesit for interactor mesh. Translates coordinates.""" points_for_interact = [] for point_to_poly in input_lines: from_coord_start = window.sketchWidget.from_quadrant_coords_no_center(point_to_poly.crd1.ui_point) from_coord_end = window.sketchWidget.from_quadrant_coords_no_center(point_to_poly.crd2.ui_point) 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 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()