851 lines
30 KiB
Python
851 lines
30 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, 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()
|
|
|
|
### SNAPS
|
|
|
|
self.ui.pb_snap_midp.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("mpoint", checked))
|
|
self.ui.pb_snap_horiz.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("horiz", checked))
|
|
self.ui.pb_snap_vert.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("vert", checked))
|
|
self.ui.pb_snap_angle.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("angle", checked))
|
|
self.ui.pb_enable_snap.toggled.connect(lambda checked: self.sketchWidget.on_snap_mode_change("point", checked))
|
|
### 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):
|
|
'''This function redraws the sdf and helper mesh from available bodies and adds the names back to the list entries'''
|
|
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
|
|
|
|
# detect loop that causes problems in mesh generation
|
|
if points[-1] == points[0]:
|
|
print("overlap")
|
|
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()
|
|
|
|
|