# 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_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
        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]

        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(self):
        """
        :return:
        """
        sketch = Sketch()
        sketch_from_widget = self.sketchWidget.get_sketch()
        points = sketch_from_widget.points

        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
        self.project.timeline[-1].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 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]
        #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}"
        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.interactor_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

    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

    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

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