fluencyCAD/main.py
2024-07-11 20:16:20 +02:00

479 lines
17 KiB
Python

import uuid
import names
from PySide6.QtCore import Qt, QPoint
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
# main, draw_widget, gl_widget
class ExtrudeDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle('Extrude Options')
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')
# 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.symmetric_checkbox)
layout.addLayout(button_layout)
self.setLayout(layout)
def get_values(self):
return self.length_input.value(), self.symmetric_checkbox.isChecked()
class MainWindow(QMainWindow):
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 = {
'sketch': {},
'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)
###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)
def add_new_sketch_origin(self):
self.sketchWidget.clear_sketch()
self.sketchWidget.create_workplane()
def add_new_sketch_wp(self):
self.sketchWidget.clear_sketch()
#edges = [((-158.0, -20.0, -25.0), (286.0, -195.0, -25.0)), ((-158.0, -20.0, 25.0), (-158.0, -20.0, -25.0))]
edges = self.custom_3D_Widget.project_tosketch_edge
normal = self.custom_3D_Widget.selected_normal
self.sketchWidget.create_workplane_projected()
self.sketchWidget.create_proj_lines(edges)
#self.sketchWidget.create_workplane_space(edges, normal)
def act_line_mode(self):
if not self.ui.pb_linetool.isChecked():
self.sketchWidget.mouse_mode = 'line'
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 calc_sketch_projection_3d(self, lines, z_origin, depth):
print(f"Gemetries {lines}, {z_origin}, {depth}")
edges = interactor_mesh.generate_mesh(lines, z_origin, depth)
print("final_mesh", edges)
self.custom_3D_Widget.load_interactor_mesh(edges)
def draw_mesh(self):
name = self.ui.body_list.currentItem().text()
print("selected_for disp", name)
model = self.model['operation'][name]['sdf_object']
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 convert_points_for_sdf(self):
points_for_sdf = []
for point_to_poly in self.sketchWidget.slv_points_main:
points_for_sdf.append(self.translate_points_tup(point_to_poly['ui_point']))
return points_for_sdf
def convert_lines_for_interactor(self):
points_for_interact = []
for point_to_poly in self.sketchWidget.slv_lines_main:
start, end = point_to_poly['ui_points']
start_draw = self.translate_points_tup(start)
end_draw = self.translate_points_tup(end)
line = start_draw, end_draw
points_for_interact.append(line)
print("packed_lines", points_for_interact)
return points_for_interact
def add_sketch(self):
name = f"sketch-{str(names.get_first_name())}"
points_for_sdf = self.convert_points_for_sdf()
element = {
'id': name,
'type': 'sketch',
'point_list': self.sketchWidget.slv_points_main,
'line_list': self.sketchWidget.slv_lines_main,
'sketch_points': points_for_sdf,
'solver': self.sketchWidget.solv
}
self.model['sketch'][element['id']] = element
print(self.model)
self.ui.sketch_list.addItem(name)
self.ui.pb_linetool.setChecked(False)
self.sketchWidget.line_mode = False
items = self.ui.sketch_list.findItems(name, Qt.MatchExactly)[0]
self.ui.sketch_list.setCurrentItem(items)
def edit_sketch(self):
name = self.ui.sketch_list.currentItem().text()
#self.sketchWidget.clear_sketch()
self.sketchWidget.slv_points_main = self.model['sketch'][name]['point_list']
self.sketchWidget.slv_lines_main = self.model['sketch'][name]['line_list']
self.sketchWidget.solv = self.model['sketch'][name]['solver']
self.sketchWidget.update()
print("model",self.model)
print("widget", self.sketchWidget.slv_points_main)
def del_sketch(self):
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 'sketch' key exists in the model dictionary
if 'sketch' in self.model and item_name in self.model['sketch']:
if self.model['sketch'][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['sketch'].pop(item_name) # Remove the item from the sketch dictionary
print(f"Removed sketch: {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 'sketch' 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 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 send_extrude(self):
is_symmetric = None
length = None
selected = self.ui.sketch_list.currentItem()
name = selected.text()
points = self.model['sketch'][name]['sketch_points']
lines = self.convert_lines_for_interactor()
if points[-1] == points[0]:
#detect loop that causes problems in mesh generation
del points[-1]
"""length, ok = QInputDialog.getDouble(self, 'Extrude Length', 'Enter a mm value:', decimals=2)
#TODO : Implement cancel"""
dialog = ExtrudeDialog(self)
if dialog.exec():
length, is_symmetric = dialog.get_values()
print(f"Extrude length: {length}, Symmetric: {is_symmetric}")
else:
length = 0
print("Extrude cancelled")
#Create and draw Interactor
geo = Geometry()
# Rotation is done in vtk matrix trans
angle = 0
normal = [0, 0, 1]
f = geo.extrude_shape(points, length, angle, normal, is_symmetric)
if not is_symmetric:
origin_z_lvl = length / 2
else:
origin_z_lvl = 0
self.calc_sketch_projection_3d(lines, origin_z_lvl, length)
name_op = f"extrd-{name}"
element = {
'id': name_op,
'type': 'extrude',
'sdf_object': f,
}
#print(element)
self.model['operation'][name_op] = element
self.ui.body_list.addItem(name_op)
items = self.ui.body_list.findItems(name_op, Qt.MatchExactly)[0]
self.ui.body_list.setCurrentItem(items)
self.draw_mesh()
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) > 1:
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.sketch_list.findItems(name_op, Qt.MatchExactly)
#self.ui.body_list.setCurrentItem(items[-1])
self.draw_mesh()
else:
print("mindestens 2!")
def load_and_render(self, file):
self.custom_3D_Widget.load_stl(file)
self.custom_3D_Widget.update()
class Geometry:
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 extrude_shape(self, points, length: float, angle, normal, symet: bool = True):
"""2D to 3D sdf always first"""
f = polygon(points).rotate(angle)
if not symet:
print("Offsetting", symet)
f = f.extrude(length).orient(normal).translate((0, 0, length/2)) # orient(normal)
else:
f = f.extrude(length).orient(normal)
return f
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
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)
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
#pyside6-uic gui.ui > Gui.py -g python