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.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 distance(self, p1, p2): return math.sqrt((p1.x() - p2.x())**2 + (p1.y() - p2.y())**2) 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 mousePressEvent(self, event): local_event_pos = self.viewport_to_local_coord(event.pos()) def check_all_points() -> 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(len(self.slv_points_main) + len(self.slv_lines_main)): #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) +1 : differences.append(f"Length mismatch {len(old_points_ui)} - {len(new_points_ui)}") return differences 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(point_list: list): # Print initial state of slv_points_main """print("Initial slv_points_main:", self.slv_points_main) print("Change list:", point_list)""" 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 """print("Updating index:", index) print("Old point:", old_point) print("New point:", new_point)""" # 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) """# UPDATE UI POINTS with solver points for i in range(len(self.slv_points_main) + len(self.slv_lines_main)): 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) for point_to_be_updated in self.slv_points_main: #print("Checking point:", point_to_be_updated) # if the old ui point is present in the selected constrain line # update the _ui_point with solver_point if point_to_be_updated['ui_point'] == target_line_con['ui_points'][0]: print("that got updated", point_solved) point_to_be_updated['ui_point'] = point_solved elif point_to_be_updated['ui_point'] == target_line_con['ui_points'][1]: print("this got updated", point_solved) point_to_be_updated['ui_point'] = point_solved if point_to_be_updated['ui_point'] == target_line_con['ui_points'][0]: target_line_con['ui_points'][0] = point_solved elif point_to_be_updated['ui_point'] == target_line_con['ui_points'][1]: target_line_con['ui_points'][1] = point_solved #TODO: All points ui and solve into lists and then check before afer for changes # and update displayed points and then lines""" def check_all_lines_and_update(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 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 self.mouse_mode == "line": clicked_pos = local_event_pos # Paintline """self.points.append(clicked_pos) self.update()""" 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.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": point_solve_old = None point_solve_now = None 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: """x, y = self.solv.params(self.slv_points_main[move_id]['solv_handle'].params) moved_point = QPoint(x, y) self.slv_points_main[target_id]['ui_point'] = moved_point for ident, lines in enumerate(self.slv_lines_main): if self.slv_points_main[target_id]['solv_handle'] == lines['solv_entity_points'][0]: self.slv_lines_main[ident]['ui_points'][0] = moved_point""" self.pt_pt_buffer = [] self.constrain_done.emit() #print(dof) self.update() 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 = [] #for points_ui in self.slv_points_main: #self.points.append(points_ui['ui_point']) print(self.points) 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") x, y = self.solv.params(self.pt_line_buffer['solv_handle'].params) #Update UI points to returned points from solver self.slv_points_main[self.selected_main_idx]['ui_point'] = QPoint(x, y) self.slv_lines_main[line_nr]['ui_points'][idx] = QPoint(x, y) self.pt_line_buffer = 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 == "horiz": 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 self.solv.horizontal(line_selected, self.wp) if self.solv.solve() == ResultFlag.OKAY: print("Fuck yeah") self.pt_line_buffer = 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 == "vert": 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 self.solv.vertical(line_selected, self.wp) if self.solv.solve() == ResultFlag.OKAY: print("Fuck yeah") self.pt_line_buffer = 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 == "distance": print("distance") 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]): lines_to_cons = target_line_con['solv_entity_points'] break length, ok = QInputDialog.getDouble(self, 'Distance', 'Enter a mm value:', decimals=2) e1, e2 = lines_to_cons self.solv.distance(e1, e2, length, self.wp) if self.solv.solve() == ResultFlag.OKAY: print("Fuck yeah") self.pt_line_buffer = 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") points_need_update = check_all_points() print("This", points_need_update) update_ui_points(points_need_update) lines_need_update = check_all_lines_and_update(points_need_update) print("This", lines_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) selected_points = [] 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 = 10 tick_spacing = 50 pen = QPen(Qt.gray, 1, Qt.SolidLine) painter.setPen(pen) # Draw tick marks on the X axis for x in range(0, self.width(), tick_spacing): painter.drawLine(x, middle_y - tick_length // 2, x, middle_y + tick_length // 2) # Draw tick marks on the Y axis for y in range(0, self.height(), tick_spacing): painter.drawLine(middle_x - tick_length // 2, y, middle_x + tick_length // 2, 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.drawBackgroundGrid(painter) 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) # Set the background color #painter.fillRect(0, self.width(), 0, self.height(), QColor('black')) # Draw axes 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: painter.drawLine(dic['ui_points'][0] , dic['ui_points'][1]) pen = QPen(Qt.green) pen.setWidth(2) painter.setPen(pen) for i in range(len(self.slv_points_main) + len(self.slv_lines_main)): 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) if self.selected_line: p1, p2 = self.selected_line painter.setPen(QPen(Qt.red, 2)) painter.drawLine(p1, p2) 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())