diff --git a/GUI_draft.py b/GUI_draft.py index 7cd2f3acaff15b9b81023716cf496b653108fc6a..299b7ab794d24d040507410fbfdc2186814f544a 100644 --- a/GUI_draft.py +++ b/GUI_draft.py @@ -8,6 +8,42 @@ from PyQt5.QtWidgets import ( ) from PyQt5.QtGui import QPixmap, QPen, QBrush from PyQt5.QtCore import Qt, QRectF +import math + + + +class PointItem(QGraphicsEllipseItem): + """ + Represents a single draggable point on the scene. + """ + def __init__(self, x, y, radius=4, parent=None): + super().__init__(x - radius, y - radius, 2*radius, 2*radius, parent) + self._x = x + self._y = y + self._r = radius + + self.setBrush(QBrush(Qt.red)) + self.setPen(QPen(Qt.red)) + + def get_pos(self): + """Return the (x, y) of this point in scene coords.""" + return (self._x, self._y) + + def set_pos(self, x, y): + """ + Move point to (x,y). + This also updates the ellipse rectangle so the visual dot moves. + """ + self._x = x + self._y = y + self.setRect(x - self._r, y - self._r, 2*self._r, 2*self._r) + + def distance_to(self, x_other, y_other): + """Euclidean distance from this point to arbitrary (x_other, y_other).""" + dx = self._x - x_other + dy = self._y - y_other + return math.sqrt(dx*dx + dy*dy) + class ImageGraphicsView(QGraphicsView): @@ -30,7 +66,6 @@ class ImageGraphicsView(QGraphicsView): # Points and dot items self.points = [] - self.point_items = [] self.editor_mode = False self.dot_radius = 4 @@ -43,6 +78,8 @@ class ImageGraphicsView(QGraphicsView): self._press_view_pos = None self._drag_threshold = 5 self._was_dragging = False + self._dragging_idx = None + self._drag_offset = (0, 0) def load_image(self, image_path): """Load an image and fit it in the view.""" @@ -56,7 +93,6 @@ class ImageGraphicsView(QGraphicsView): # Clear existing dots from previous image self.points.clear() - self._clear_point_items() # Reset transform then fit image in view self.resetTransform() @@ -72,8 +108,29 @@ class ImageGraphicsView(QGraphicsView): self._was_dragging = False self._press_view_pos = event.pos() - # Switch to closed-hand cursor while left mouse is down - self.viewport().setCursor(Qt.ClosedHandCursor) + if self.editor_mode: + # Check if we're near a point + idx = self._find_point_near(event.pos(), threshold=10) + if idx is not None: + # Start dragging that point + self._dragging_idx = idx + # Compute offset so point doesn't jump if clicked off-center + scene_pos = self.mapToScene(event.pos()) + px, py = self.points[idx].get_pos() + self._drag_offset = (scene_pos.x() - px, scene_pos.y() - py) + + # Temporarily disable QGraphicsView's panning + self.setDragMode(QGraphicsView.NoDrag) + self.viewport().setCursor(Qt.ClosedHandCursor) + return + else: + # Not near a point, so we do normal panning + self.setDragMode(QGraphicsView.ScrollHandDrag) + self.viewport().setCursor(Qt.ClosedHandCursor) + else: + # Editor mode is off => always do normal panning + self.setDragMode(QGraphicsView.ScrollHandDrag) + self.viewport().setCursor(Qt.ClosedHandCursor) elif event.button() == Qt.RightButton: # If Editor Mode is on remove the nearest dot @@ -82,41 +139,46 @@ class ImageGraphicsView(QGraphicsView): super().mousePressEvent(event) + def mouseMoveEvent(self, event): - """ - If movement > _drag_threshold: consider it a drag. - The actual panning is handled by QGraphicsView in ScrollHandDrag mode. - """ - # Check if the mouse is being dragged - if self._mouse_pressed and (event.buttons() & Qt.LeftButton): - # If the mouse moved more than the threshold, consider it a drag - dist = (event.pos() - self._press_view_pos).manhattanLength() - if dist > self._drag_threshold: - self._was_dragging = True + if self._dragging_idx is not None: + # Move that point to new coords + scene_pos = self.mapToScene(event.pos()) + x_new = scene_pos.x() - self._drag_offset[0] + y_new = scene_pos.y() - self._drag_offset[1] + + self.points[self._dragging_idx].set_pos(x_new, y_new) + return # Skip QGraphicsView's panning logic + else: + # Old logic: if movement > threshold -> set _was_dragging = True + if self._mouse_pressed and (event.buttons() & Qt.LeftButton): + dist = (event.pos() - self._press_view_pos).manhattanLength() + if dist > self._drag_threshold: + self._was_dragging = True + + super().mouseMoveEvent(event) - super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): - """ - After releasing the left button, go back to arrow cursor. - If it wasn't a drag, treat as a click (Editor Mode: add dot). - """ - # Let QGraphicsView handle release first - super().mouseReleaseEvent(event) + super().mouseReleaseEvent(event) if event.button() == Qt.LeftButton and self._mouse_pressed: self._mouse_pressed = False - - # Always go back to arrow cursor AFTER letting QGraphicsView handle release self.viewport().setCursor(Qt.ArrowCursor) - if not self._was_dragging: - # It's a click: if editor mode is ON add a dot - if self.editor_mode: + # If we were dragging a point, stop. + if self._dragging_idx is not None: + self._dragging_idx = None + self._drag_offset = (0, 0) + self.setDragMode(QGraphicsView.ScrollHandDrag) + else: + # We were NOT dragging a point => check if it was a click to add a new point + if not self._was_dragging and self.editor_mode: self._add_point(event.pos()) self._was_dragging = False + def wheelEvent(self, event): """Mouse wheel = zoom.""" zoom_in_factor = 1.25 @@ -136,9 +198,8 @@ class ImageGraphicsView(QGraphicsView): scene_pos = self.mapToScene(view_pos) x, y = scene_pos.x(), scene_pos.y() - self.points.append((x, y)) - dot = self._create_dot_item(x, y) - self.point_items.append(dot) + dot = PointItem(x, y, radius=self.dot_radius) + self.points.append(dot) self.scene.addItem(dot) def _remove_point(self, view_pos): @@ -146,22 +207,19 @@ class ImageGraphicsView(QGraphicsView): scene_pos = self.mapToScene(view_pos) x_click, y_click = scene_pos.x(), scene_pos.y() - # Define threshold for removing a point threshold = 10 closest_idx = None min_dist = float('inf') - # Find the closest point to the click - for i, (x, y) in enumerate(self.points): - dist_sq = (x - x_click)**2 + (y - y_click)**2 - if dist_sq < min_dist: - min_dist = dist_sq + for i, point_item in enumerate(self.points): + dist = point_item.distance_to(x_click, y_click) + if dist < min_dist: + min_dist = dist closest_idx = i - # Remove the closest point if it's within the threshold - if closest_idx is not None and min_dist <= threshold**2: - self.scene.removeItem(self.point_items[closest_idx]) - del self.point_items[closest_idx] + # Remove if within threshold + if closest_idx is not None and min_dist <= threshold: + self.scene.removeItem(self.points[closest_idx]) del self.points[closest_idx] def _create_dot_item(self, x, y): @@ -174,9 +232,29 @@ class ImageGraphicsView(QGraphicsView): def _clear_point_items(self): """Remove all dot items from the scene.""" - for item in self.point_items: - self.scene.removeItem(item) - self.point_items = [] + for p in self.points: + self.scene.removeItem(p) + self.points.clear() + + def _find_point_near(self, view_pos, threshold=10): + scene_pos = self.mapToScene(view_pos) + x_click, y_click = scene_pos.x(), scene_pos.y() + + closest_idx = None + min_dist = float('inf') + + for i, p in enumerate(self.points): + dist = p.distance_to(x_click, y_click) + if dist < min_dist: + min_dist = dist + closest_idx = i + + if closest_idx is not None and min_dist <= threshold: + return closest_idx + return None + + + class MainWindow(QMainWindow):