Major changes: - Fixed right-click handler to directly set mode to NONE instead of relying on main app signal handling - Added safety checks in left-click handler to prevent drawing when no draggable point is found in NONE mode - Enhanced mode compatibility by treating Python None as SketchMode.NONE in set_mode() method - Added comprehensive debug logging for mode changes and interaction state tracking - Resolved integration issue where persistent constraint modes were prematurely reset by main app - Ensured point dragging is only enabled in NONE mode, preventing accidental polyline creation This fixes the reported issue where deactivating the line tool would still create lines when dragging, and ensures proper mode transitions between drawing tools and selection/drag mode.
852 lines
30 KiB
Python
852 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
|
|
import numpy as np
|
|
|
|
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()
|
|
|
|
|