Skip to content
Snippets Groups Projects
Commit 7f9fa646 authored by s224389's avatar s224389
Browse files

Added ability to drag points around. Also implemented point class for better tracking.

parent b69dbdf7
No related branches found
No related tags found
No related merge requests found
...@@ -8,6 +8,42 @@ from PyQt5.QtWidgets import ( ...@@ -8,6 +8,42 @@ from PyQt5.QtWidgets import (
) )
from PyQt5.QtGui import QPixmap, QPen, QBrush from PyQt5.QtGui import QPixmap, QPen, QBrush
from PyQt5.QtCore import Qt, QRectF 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): class ImageGraphicsView(QGraphicsView):
...@@ -30,7 +66,6 @@ class ImageGraphicsView(QGraphicsView): ...@@ -30,7 +66,6 @@ class ImageGraphicsView(QGraphicsView):
# Points and dot items # Points and dot items
self.points = [] self.points = []
self.point_items = []
self.editor_mode = False self.editor_mode = False
self.dot_radius = 4 self.dot_radius = 4
...@@ -43,6 +78,8 @@ class ImageGraphicsView(QGraphicsView): ...@@ -43,6 +78,8 @@ class ImageGraphicsView(QGraphicsView):
self._press_view_pos = None self._press_view_pos = None
self._drag_threshold = 5 self._drag_threshold = 5
self._was_dragging = False self._was_dragging = False
self._dragging_idx = None
self._drag_offset = (0, 0)
def load_image(self, image_path): def load_image(self, image_path):
"""Load an image and fit it in the view.""" """Load an image and fit it in the view."""
...@@ -56,7 +93,6 @@ class ImageGraphicsView(QGraphicsView): ...@@ -56,7 +93,6 @@ class ImageGraphicsView(QGraphicsView):
# Clear existing dots from previous image # Clear existing dots from previous image
self.points.clear() self.points.clear()
self._clear_point_items()
# Reset transform then fit image in view # Reset transform then fit image in view
self.resetTransform() self.resetTransform()
...@@ -72,7 +108,28 @@ class ImageGraphicsView(QGraphicsView): ...@@ -72,7 +108,28 @@ class ImageGraphicsView(QGraphicsView):
self._was_dragging = False self._was_dragging = False
self._press_view_pos = event.pos() self._press_view_pos = event.pos()
# Switch to closed-hand cursor while left mouse is down 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) self.viewport().setCursor(Qt.ClosedHandCursor)
elif event.button() == Qt.RightButton: elif event.button() == Qt.RightButton:
...@@ -82,41 +139,46 @@ class ImageGraphicsView(QGraphicsView): ...@@ -82,41 +139,46 @@ class ImageGraphicsView(QGraphicsView):
super().mousePressEvent(event) super().mousePressEvent(event)
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
""" if self._dragging_idx is not None:
If movement > _drag_threshold: consider it a drag. # Move that point to new coords
The actual panning is handled by QGraphicsView in ScrollHandDrag mode. scene_pos = self.mapToScene(event.pos())
""" x_new = scene_pos.x() - self._drag_offset[0]
# Check if the mouse is being dragged 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): 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() dist = (event.pos() - self._press_view_pos).manhattanLength()
if dist > self._drag_threshold: if dist > self._drag_threshold:
self._was_dragging = True self._was_dragging = True
super().mouseMoveEvent(event) super().mouseMoveEvent(event)
def mouseReleaseEvent(self, 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: if event.button() == Qt.LeftButton and self._mouse_pressed:
self._mouse_pressed = False self._mouse_pressed = False
# Always go back to arrow cursor AFTER letting QGraphicsView handle release
self.viewport().setCursor(Qt.ArrowCursor) self.viewport().setCursor(Qt.ArrowCursor)
if not self._was_dragging: # If we were dragging a point, stop.
# It's a click: if editor mode is ON add a dot if self._dragging_idx is not None:
if self.editor_mode: 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._add_point(event.pos())
self._was_dragging = False self._was_dragging = False
def wheelEvent(self, event): def wheelEvent(self, event):
"""Mouse wheel = zoom.""" """Mouse wheel = zoom."""
zoom_in_factor = 1.25 zoom_in_factor = 1.25
...@@ -136,9 +198,8 @@ class ImageGraphicsView(QGraphicsView): ...@@ -136,9 +198,8 @@ class ImageGraphicsView(QGraphicsView):
scene_pos = self.mapToScene(view_pos) scene_pos = self.mapToScene(view_pos)
x, y = scene_pos.x(), scene_pos.y() x, y = scene_pos.x(), scene_pos.y()
self.points.append((x, y)) dot = PointItem(x, y, radius=self.dot_radius)
dot = self._create_dot_item(x, y) self.points.append(dot)
self.point_items.append(dot)
self.scene.addItem(dot) self.scene.addItem(dot)
def _remove_point(self, view_pos): def _remove_point(self, view_pos):
...@@ -146,22 +207,19 @@ class ImageGraphicsView(QGraphicsView): ...@@ -146,22 +207,19 @@ class ImageGraphicsView(QGraphicsView):
scene_pos = self.mapToScene(view_pos) scene_pos = self.mapToScene(view_pos)
x_click, y_click = scene_pos.x(), scene_pos.y() x_click, y_click = scene_pos.x(), scene_pos.y()
# Define threshold for removing a point
threshold = 10 threshold = 10
closest_idx = None closest_idx = None
min_dist = float('inf') min_dist = float('inf')
# Find the closest point to the click for i, point_item in enumerate(self.points):
for i, (x, y) in enumerate(self.points): dist = point_item.distance_to(x_click, y_click)
dist_sq = (x - x_click)**2 + (y - y_click)**2 if dist < min_dist:
if dist_sq < min_dist: min_dist = dist
min_dist = dist_sq
closest_idx = i closest_idx = i
# Remove the closest point if it's within the threshold # Remove if within threshold
if closest_idx is not None and min_dist <= threshold**2: if closest_idx is not None and min_dist <= threshold:
self.scene.removeItem(self.point_items[closest_idx]) self.scene.removeItem(self.points[closest_idx])
del self.point_items[closest_idx]
del self.points[closest_idx] del self.points[closest_idx]
def _create_dot_item(self, x, y): def _create_dot_item(self, x, y):
...@@ -174,9 +232,29 @@ class ImageGraphicsView(QGraphicsView): ...@@ -174,9 +232,29 @@ class ImageGraphicsView(QGraphicsView):
def _clear_point_items(self): def _clear_point_items(self):
"""Remove all dot items from the scene.""" """Remove all dot items from the scene."""
for item in self.point_items: for p in self.points:
self.scene.removeItem(item) self.scene.removeItem(p)
self.point_items = [] 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): class MainWindow(QMainWindow):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment