# 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()