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

571 lines
20 KiB
Python

# 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
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')
self.invert_checkbox = QCheckBox('Invert Extrusion')
# 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(length_layout)
layout.addWidget(self.invert_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()
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 = {
'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)
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)
def on_flip_face(self):
self.send_command.emit("flip")
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)
# CLear all selections after it has been projected
#self.custom_3D_Widget.clear_edge_select()
self.custom_3D_Widget.clear_actors_projection()
#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 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']
from_coord_start = self.sketchWidget.from_quadrant_coords_no_center(start)
from_coord_end = self.sketchWidget.from_quadrant_coords_no_center(end)
start_draw = self.translate_points_tup(from_coord_start)
end_draw = self.translate_points_tup(from_coord_end)
line = start_draw, end_draw
points_for_interact.append(line)
print("packed_lines", points_for_interact)
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 rotate_point_to_normal(self, centroid, normal):
# Ensure the normal is a unit vector
normal = normal / np.linalg.norm(normal)
# Initial direction (assuming positive Z-axis)
initial_direction = np.array([0, 0, 1])
# Compute rotation axis
rotation_axis = np.cross(initial_direction, normal)
# If rotation axis is zero (vectors are parallel), no rotation is needed
if np.allclose(rotation_axis, 0):
return centroid
# Compute rotation angle
rotation_angle = np.arccos(np.dot(initial_direction, normal))
# Create rotation matrix using Rodrigues' formula
K = np.array([
[0, -rotation_axis[2], rotation_axis[1]],
[rotation_axis[2], 0, -rotation_axis[0]],
[-rotation_axis[1], rotation_axis[0], 0]
])
rotation_matrix = (
np.eye(3) +
np.sin(rotation_angle) * K +
(1 - np.cos(rotation_angle)) * np.dot(K, K)
)
# Apply rotation to centroid
rotated_centroid = np.dot(rotation_matrix, centroid)
return rotated_centroid
def send_extrude(self):
is_symmetric = None
length = None
invert = 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]
dialog = ExtrudeDialog(self)
if dialog.exec():
length, is_symmetric, invert = dialog.get_values()
print(f"Extrude length: {length}, Symmetric: {is_symmetric} Invert: {invert}")
else:
length = 0
print("Extrude cancelled")
#Create and draw Interactor
geo = Geometry()
# Rotation is done in vtk matrix trans
angle = 0
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)
f = geo.extrude_shape(points, length, angle, normal, centroid, is_symmetric, invert)
z_origin = centroid[2]
if is_symmetric:
z_origin = z_origin - length / 2
if invert:
edges = interactor_mesh.generate_mesh(lines, z_origin, length, True)
else:
edges = interactor_mesh.generate_mesh(lines, z_origin, length, False)
self.custom_3D_Widget.load_interactor_mesh(edges)
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) == 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()
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, centroid, symet: bool = True, invert: bool = False):
"""
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(normal)
normal = normal / np.linalg.norm(normal)
# Create the 2D shape
f = polygon(points)
# Extrude the shape along the Z-axis
f = f.extrude(length)
# Center the shape along its extrusion axis
f = f.translate((0, 0, length / 2))
# Orient the shape along the normal vector
f = f.orient(normal)
# 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
# Translate the shape along the normal
f = f.translate(translation_along_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()