import math import re from copy import copy from PySide6.QtWidgets import QApplication, QWidget, QMessageBox, QInputDialog from PySide6.QtGui import QPainter, QPen, QColor from PySide6.QtCore import Qt, QPoint, QPointF, Signal from python_solvespace import SolverSystem, ResultFlag class SketchWidget(QWidget): constrain_done = Signal() def __init__(self): super().__init__() self.main_buffer = [None, None] self.distance_line = None self.distance_point = None self.point_for_mid = None self.line_selected_mid = None self.zoom = 1 self.selected_main_idx = None self.pt_line_buffer = None self.hovered_point = None self.line_buffer = None self.pt_pt_buffer = None self.points = [] self.selected_line = None self.snapping_range = 20 # Range in pixels for snapping self.setMouseTracking(True) self.mouse_mode = False self.wp = None self.solv = SolverSystem() self.slv_points_main = [] self.slv_lines_main = [] def set_points(self, points: list): self.points = points #self.update() def create_worplane(self): self.wp = self.solv.create_2d_base() def get_handle_nr(self, input_str: str) -> int: # Define the regex pattern to extract the handle number pattern = r"handle=(\d+)" # Use re.search to find the handle number in the string match = re.search(pattern, input_str) if match: handle_number = int(match.group(1)) print(f"Handle number: {handle_number}") return int(handle_number) else: print("Handle number not found.") return 0 def get_keys(self, d: dict, target: QPoint) -> list: result = [] path = [] print(d) print(target) for k, v in d.items(): path.append(k) if isinstance(v, dict): self.get_keys(v, target) if v == target: result.append(copy(path)) path.pop() return result def get_handle_from_ui_point(self, ui_point: QPoint): """Input QPoint and you shall reveive a slvs entity handle!""" for point in self.slv_points_main: if ui_point == point['ui_point']: slv_handle = point['solv_handle'] return slv_handle def get_line_handle_from_ui_point(self, ui_point: QPoint): """Input Qpoint that is on a line and you shall receive the handle of the line!""" for target_line_con in self.slv_lines_main: if self.is_point_on_line(ui_point, target_line_con['ui_points'][0], target_line_con['ui_points'][1]): slv_handle = target_line_con['solv_handle'] return slv_handle def get_point_line_handles_from_ui_point(self, ui_point: QPoint) -> tuple: """Input Qpoint that is on a line and you shall receive the handles of the points of the line!""" for target_line_con in self.slv_lines_main: if self.is_point_on_line(ui_point, target_line_con['ui_points'][0], target_line_con['ui_points'][1]): lines_to_cons = target_line_con['solv_entity_points'] return lines_to_cons def distance(self, p1, p2): return math.sqrt((p1.x() - p2.x())**2 + (p1.y() - p2.y())**2) def calculate_midpoint(self, point1, point2): mx = (point1.x() + point2.x()) // 2 my = (point1.y() + point2.y()) // 2 return QPoint(mx, my) def is_point_on_line(self, p, p1, p2, tolerance=5): # Calculate the lengths of the sides of the triangle a = self.distance(p, p1) b = self.distance(p, p2) c = self.distance(p1, p2) # Calculate the semi-perimeter s = (a + b + c) / 2 # Calculate the area using Heron's formula area = math.sqrt(s * (s - a) * (s - b) * (s - c)) # Calculate the height (perpendicular distance from the point to the line) if c > 0: height = (2 * area) / c # Check if the height is within the tolerance distance to the line if height > tolerance: return False # Check if the projection of the point onto the line is within the line segment dot_product = ((p.x() - p1.x()) * (p2.x() - p1.x()) + (p.y() - p1.y()) * (p2.y() - p1.y())) / (c ** 2) return 0 <= dot_product <= 1 else: return None def viewport_to_local_coord(self, qt_pos : QPoint) -> QPoint: self.to_quadrant_coords(qt_pos) return QPoint(self.to_quadrant_coords(qt_pos)) def check_all_points(self,) -> list: old_points_ui = [] new_points_ui = [] for old_point_ui in self.slv_points_main: old_points_ui.append(old_point_ui['ui_point']) for i in range(self.solv.entity_len()): # Iterate though full length because mixed list from SS entity = self.solv.entity(i) if entity.is_point_2d() and self.solv.params(entity.params): x_tbu, y_tbu = self.solv.params(entity.params) point_solved = QPoint(x_tbu, y_tbu) new_points_ui.append(point_solved) # Now we have old_points_ui and new_points_ui, let's compare them differences = [] if len(old_points_ui) != len(new_points_ui): print(f"Length mismatch {len(old_points_ui)} - {len(new_points_ui)}") for index, (old_point, new_point) in enumerate(zip(old_points_ui, new_points_ui)): if old_point != new_point: differences.append((index, old_point, new_point)) return differences def update_ui_points(self, point_list: list): # Print initial state of slv_points_main # print("Initial slv_points_main:", self.slv_points_main) print("Change list:", point_list) if len(point_list) > 0: for tbu_points_idx in point_list: # Each tbu_points_idx is a tuple: (index, old_point, new_point) index, old_point, new_point = tbu_points_idx # Update the point in slv_points_main self.slv_points_main[index]['ui_point'] = new_point # Print updated state # print("Updated slv_points_main:", self.slv_points_main) def check_all_lines_and_update(self,changed_points: list): for tbu_points_idx in changed_points: index, old_point, new_point = tbu_points_idx for line_needs_update in self.slv_lines_main: if old_point == line_needs_update['ui_points'][0]: line_needs_update['ui_points'][0] = new_point elif old_point == line_needs_update['ui_points'][1]: line_needs_update['ui_points'][1] = new_point def mouseReleaseEvent(self, event): local_event_pos = self.viewport_to_local_coord(event.pos()) if event.button() == Qt.LeftButton and not self.mouse_mode: self.main_buffer[1] = local_event_pos print("Le main buffer", self.main_buffer) if len(self.main_buffer) == 2: entry = self.main_buffer[0] new_params = [self.main_buffer[1].x(), self.main_buffer[1].y()] self.solv.set_params(entry.params, new_params ) self.solv.solve() points_need_update = self.check_all_points() self.update_ui_points(points_need_update) self.check_all_lines_and_update(points_need_update) self.update() def mousePressEvent(self, event): local_event_pos = self.viewport_to_local_coord(event.pos()) relation_point = { 'handle_nr': None, 'solv_handle': None, 'ui_point': None, 'part_of_entity': None } relation_line = { 'handle_nr': None, 'solv_handle': None, 'solv_entity_points': None, 'ui_points': None } if event.button() == Qt.LeftButton and not self.mouse_mode: self.main_buffer[0] = self.get_handle_from_ui_point(self.hovered_point) if event.button() == Qt.LeftButton and self.mouse_mode == "line": clicked_pos = local_event_pos u = clicked_pos.x() v = clicked_pos.y() point = self.solv.add_point_2d(u, v, self.wp) # Track Relationship # Points handle_nr = self.get_handle_nr(str(point)) relation_point['handle_nr'] = handle_nr relation_point['solv_handle'] = point relation_point['ui_point'] = clicked_pos # List of points related to the current "figure" self.points.append(clicked_pos) # Solverline if self.line_buffer: line = self.solv.add_line_2d(self.line_buffer, point, self.wp) handle_nr_line = self.get_handle_nr(str(line)) relation_line['handle_nr'] = handle_nr_line relation_line['solv_handle'] = line relation_line['solv_entity_points'] = (self.line_buffer, point) relation_line['ui_points'] = [self.points[-2], self.points[-1]] #track relationship of point in line relation_point['part_of_entity'] = handle_nr_line self.slv_lines_main.append(relation_line) self.line_buffer = None self.slv_points_main.append(relation_point) self.line_buffer = point print("points", self.slv_points_main) print("lines", self.slv_lines_main) if event.button() == Qt.LeftButton and self.mouse_mode == "pt_pt": if self.pt_pt_buffer: for new_id, target_line in enumerate(self.slv_points_main): if self.hovered_point == target_line['ui_point']: point_solve_now = target_line['solv_handle'] target_id = new_id break else: point_solve_now = None for old_id, target_line in enumerate(self.slv_points_main): if self.pt_pt_buffer == target_line['ui_point']: point_solve_old = target_line['solv_handle'] move_id = old_id break else: point_solve_old = None if point_solve_old and point_solve_now: self.solv.coincident(point_solve_now, point_solve_old, self.wp) if self.solv.solve() == ResultFlag.OKAY: self.constrain_done.emit() print("Fuck yeah") elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE: print("Solve_failed - Converge" ) elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS: print("Solve_failed - Unknowns" ) elif self.solv.solve() == ResultFlag.INCONSISTENT: print("Solve_failed - Incons" ) self.points = [] self.pt_pt_buffer = [] print("Points_all", self.slv_points_main) self.pt_pt_buffer = self.hovered_point self.update() if event.button() == Qt.LeftButton and self.mouse_mode == "pt_line": print("ptline") line_selected = None if self.hovered_point: for nr, entry_point in enumerate(self.slv_points_main): if self.hovered_point == entry_point['ui_point']: self.pt_line_buffer = entry_point self.selected_main_idx = nr print("Point set", self.pt_line_buffer) break if self.pt_line_buffer: #Line selection target_line to constrain to for target_line_con in self.slv_lines_main: if self.is_point_on_line(local_event_pos, target_line_con['ui_points'][0], target_line_con['ui_points'][1]): line_selected = target_line_con['solv_handle'] print(line_selected.params) break #Update UI Line position ot the line of the selected point for line_nr, move_line in enumerate(self.slv_lines_main): #Test what point is going to be moved from the line if move_line['ui_points'][0] == self.pt_line_buffer['ui_point']: print("On line", line_nr) idx = 0 break elif move_line['ui_points'][1] == self.pt_line_buffer['ui_point']: print("On line", line_nr) idx = 1 break # Contrain point to line if line_selected: self.solv.coincident(self.pt_line_buffer['solv_handle'], line_selected, self.wp) print(f"1 : {self.pt_line_buffer['solv_handle']}, 2: {line_selected}") if self.solv.solve() == ResultFlag.OKAY: print("Fuck yeah") self.constrain_done.emit() elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE: print("Solve_failed - Converge") elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS: print("Solve_failed - Unknowns") elif self.solv.solve() == ResultFlag.INCONSISTENT: print("Solve_failed - Incons") # Clear saved_points after solve attempt self.pt_line_buffer = None if event.button() == Qt.LeftButton and self.mouse_mode == "horiz": line_selected = self.get_line_handle_from_ui_point(local_event_pos) self.solv.horizontal(line_selected, self.wp) if self.solv.solve() == ResultFlag.OKAY: print("Fuck yeah") self.constrain_done.emit() elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE: print("Solve_failed - Converge") elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS: print("Solve_failed - Unknowns") elif self.solv.solve() == ResultFlag.INCONSISTENT: print("Solve_failed - Incons") if event.button() == Qt.LeftButton and self.mouse_mode == "vert": line_selected = self.get_line_handle_from_ui_point(local_event_pos) self.solv.vertical(line_selected, self.wp) if self.solv.solve() == ResultFlag.OKAY: print("Fuck yeah") self.constrain_done.emit() elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE: print("Solve_failed - Converge") elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS: print("Solve_failed - Unknowns") elif self.solv.solve() == ResultFlag.INCONSISTENT: print("Solve_failed - Incons") self.pt_line_buffer = None if event.button() == Qt.LeftButton and self.mouse_mode == "distance": print("distance") e1 = None e2 = None if self.hovered_point: self.distance_point = self.hovered_point if self.selected_line: self.distance_line = self.selected_line if self.distance_point and self.distance_line: e1 = self.get_handle_from_ui_point(self.distance_point) e2 = self.get_line_handle_from_ui_point(local_event_pos) elif not self.distance_point: e1, e2 = self.get_point_line_handles_from_ui_point(local_event_pos) if e1 and e2: length, ok = QInputDialog.getDouble(self, 'Distance', 'Enter a mm value:', value=100, decimals=2) self.solv.distance(e1, e2, length, self.wp) if self.solv.solve() == ResultFlag.OKAY: print("Fuck yeah") self.distance_point = None self.distance_line = None self.constrain_done.emit() elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE: print("Solve_failed - Converge") elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS: print("Solve_failed - Unknowns") elif self.solv.solve() == ResultFlag.INCONSISTENT: print("Solve_failed - Incons") if event.button() == Qt.LeftButton and self.mouse_mode == "pb_con_mid": for entry_point_mid in self.slv_points_main: if self.hovered_point == entry_point_mid['ui_point']: self.point_for_mid = entry_point_mid['solv_handle'] break if self.point_for_mid: for target_line_con in self.slv_lines_main: if self.is_point_on_line(local_event_pos, target_line_con['ui_points'][0], target_line_con['ui_points'][1]): self.line_selected_mid = target_line_con['solv_handle'] break print(f"{self.point_for_mid}, {self.line_selected_mid}") if self.line_selected_mid and self.point_for_mid: self.solv.midpoint(self.point_for_mid, self.line_selected_mid, self.wp) if self.solv.solve() == ResultFlag.OKAY: print("Fuck yeah") self.constrain_done.emit() elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE: print("Solve_failed - Converge") elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS: print("Solve_failed - Unknowns") elif self.solv.solve() == ResultFlag.INCONSISTENT: print("Solve_failed - Incons") self.point_for_mid = None self.line_selected_mid = None points_need_update = self.check_all_points() self.update_ui_points(points_need_update) self.check_all_lines_and_update(points_need_update) dof = self.solv.dof() print(dof) self.update() def mouseMoveEvent(self, event): local_event_pos = self.viewport_to_local_coord(event.pos()) closest_point = None min_distance = float('inf') threshold = 10 # Distance threshold for highlighting for point in self.slv_points_main: distance = (local_event_pos - point['ui_point']).manhattanLength() if distance < threshold and distance < min_distance: closest_point = point['ui_point'] min_distance = distance if closest_point != self.hovered_point: self.hovered_point = closest_point print(self.hovered_point) for dic in self.slv_lines_main: p1 = dic['ui_points'][0] p2 = dic['ui_points'][1] if self.is_point_on_line(local_event_pos, p1, p2): self.selected_line = p1, p2 break else: self.selected_line = None self.update() def mouseDoubleClickEvent(self, event): pass def drawBackgroundGrid(self, painter): """Draw a background grid.""" grid_spacing = 50 pen = QPen(QColor(200, 200, 200), 1, Qt.SolidLine) painter.setPen(pen) # Draw vertical grid lines for x in range(-self.width() // 2, self.width() // 2, grid_spacing): painter.drawLine(x, -self.height() // 2, x, self.height() // 2) # Draw horizontal grid lines for y in range(-self.height() // 2, self.height() // 2, grid_spacing): painter.drawLine(-self.width() // 2, y, self.width() // 2, y) def drawAxes(self, painter): painter.setRenderHint(QPainter.Antialiasing) # Set up pen for dashed lines pen = QPen(Qt.gray, 1, Qt.DashLine) painter.setPen(pen) middle_x = self.width() // 2 middle_y = self.height() // 2 # Draw X axis as dashed line painter.drawLine(0, middle_y, self.width(), middle_y) # Draw Y axis as dashed line painter.drawLine(middle_x, 0, middle_x, self.height()) # Draw tick marks tick_length = int(10 * self.zoom) tick_spacing = int(50 * self.zoom) pen = QPen(Qt.gray, 1, Qt.SolidLine) painter.setPen(pen) # Draw tick marks on the X axis to the right and left from the middle point for x in range(0, self.width() // 2, tick_spacing): painter.drawLine(middle_x + x, middle_y - tick_length // 2, middle_x + x, middle_y + tick_length // 2) painter.drawLine(middle_x - x, middle_y - tick_length // 2, middle_x - x, middle_y + tick_length // 2) # Draw tick marks on the Y axis upwards and downwards from the middle point for y in range(0, self.height() // 2, tick_spacing): painter.drawLine(middle_x - tick_length // 2, middle_y + y, middle_x + tick_length // 2, middle_y + y) painter.drawLine(middle_x - tick_length // 2, middle_y - y, middle_x + tick_length // 2, middle_y - y) # Draw the origin point in red painter.setPen(QPen(Qt.red, 4)) painter.drawPoint(middle_x, middle_y) def to_quadrant_coords(self, point): """Translate linear coordinates to quadrant coordinates.""" center_x = self.width() // 2 center_y = self.height() // 2 quadrant_x = point.x() - center_x quadrant_y = point.y() - center_y return QPoint(quadrant_x, quadrant_y) / self.zoom def paintEvent(self, event): painter = QPainter(self) self.drawAxes(painter) # Translate the origin to the center of the widget center = QPoint(self.width() // 2, self.height() // 2) painter.translate(center) # Apply the zoom factor painter.scale(self.zoom, self.zoom) pen = QPen(Qt.gray) pen.setWidth(2 / self.zoom) painter.setPen(pen) # Draw points for point in self.slv_points_main: painter.drawEllipse(point['ui_point'], 3 / self.zoom, 3 / self.zoom) for dic in self.slv_lines_main: p1 = dic['ui_points'][0] p2 = dic['ui_points'][1] painter.drawLine(p1, p2) dis = self.distance(p1, p2) mid = self.calculate_midpoint(p1, p2) painter.drawText(mid, str(round(dis, 2))) pen = QPen(Qt.green) pen.setWidth(2) painter.setPen(pen) if self.solv.entity_len(): for i in range(self.solv.entity_len()): # 3 Entitys in the beginning of the workplane normal and point entity = self.solv.entity(i) if entity.is_point_2d() and self.solv.params(entity.params): x, y = self.solv.params(entity.params) point = QPoint(x, y) painter.drawEllipse(point, 6 / self.zoom, 6 / self.zoom) #Highlight point hovered if self.hovered_point: highlight_pen = QPen(QColor(255, 0, 0)) highlight_pen.setWidth(2) painter.setPen(highlight_pen) painter.drawEllipse(self.hovered_point, 5 / self.zoom, 5 / self.zoom) # Highlight line hovered if self.selected_line and not self.hovered_point: p1, p2 = self.selected_line painter.setPen(QPen(Qt.red, 2)) painter.drawLine(p1, p2) # self.drawBackgroundGrid(painter) painter.end() def wheelEvent(self, event): delta = event.angleDelta().y() self.zoom += (delta / 200) * 0.1 self.update() def aspect_ratio(self): return self.width() / self.height() * (1.0 / abs(self.zoom)) def clear_sketch(self): self.points = [] self.update() # Example usage if __name__ == "__main__": import sys app = QApplication(sys.argv) window = SketchWidget() window.setWindowTitle("Snap Line Widget") window.resize(800, 600) window.show() sys.exit(app.exec())