fluencyCAD/drawing_modules/draw_widget2d.py
bklronin 24e4d1028b - Added püoint to line incl detection
- Added draggable points
- added fhunctions for solv retreival
2024-06-28 14:22:27 +02:00

666 lines
24 KiB
Python

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())