From 190c914d45edd24b1334a91f3c7dc4c45be2bcbe Mon Sep 17 00:00:00 2001 From: Jules Date: Sat, 21 Mar 2026 23:42:39 +0100 Subject: [PATCH] =?UTF-8?q?Crop=20:=20ratio=20verrouill=C3=A9,=20bord=20?= =?UTF-8?q?=C3=A0=20bord,=20pas=20de=20marge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Redimensionnement verrouille le ratio (coins uniquement) - Cadre le plus grand possible (100% d'un bord, pas de marge) - Déplacement contraint dans les limites de la photo - Même comportement dans l'éditeur et l'écran impression Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ui/screens/editor_screen.py | 11 ++- src/ui/screens/print_screen.py | 10 +-- src/ui/widgets/crop_overlay.py | 136 +++++++++++++++++++++++++------- 3 files changed, 118 insertions(+), 39 deletions(-) diff --git a/src/ui/screens/editor_screen.py b/src/ui/screens/editor_screen.py index 425b224..01e750d 100644 --- a/src/ui/screens/editor_screen.py +++ b/src/ui/screens/editor_screen.py @@ -158,7 +158,7 @@ class EditorScreen(QWidget): # --- Cadre ratio suggéré --- def _show_ratio_crop(self): - """Affiche un cadre de recadrage au ratio 3:2 en suggestion.""" + """Affiche un cadre de recadrage au ratio 3:2, le plus grand possible.""" if not self.pixmap_item: return self.is_cropping = True @@ -171,14 +171,17 @@ class EditorScreen(QWidget): if img_w < img_h: ratio = 1.0 / ratio - # Plus grande zone possible + # Le plus grand possible : bord à bord, pas de marge if img_w / img_h > ratio: - crop_h = img_h * 0.95 + # Image plus large → hauteur = 100%, largeur ajustée + crop_h = img_h crop_w = crop_h * ratio else: - crop_w = img_w * 0.95 + # Image plus haute → largeur = 100%, hauteur ajustée + crop_w = img_w crop_h = crop_w / ratio + # Centrer x = (img_w - crop_w) / 2 y = (img_h - crop_h) / 2 diff --git a/src/ui/screens/print_screen.py b/src/ui/screens/print_screen.py index 47611a9..6703b64 100644 --- a/src/ui/screens/print_screen.py +++ b/src/ui/screens/print_screen.py @@ -196,17 +196,15 @@ class PrintScreen(QWidget): # Image portrait → cadre portrait (inverser le ratio) ratio = 1.0 / ratio - # Calculer la plus grande zone de crop possible dans l'image + # Le plus grand possible : bord à bord, pas de marge if img_w / img_h > ratio: - # Image plus large que le ratio → hauteur max, largeur ajustée - crop_h = img_h * 0.9 + crop_h = img_h crop_w = crop_h * ratio else: - # Image plus haute que le ratio → largeur max, hauteur ajustée - crop_w = img_w * 0.9 + crop_w = img_w crop_h = crop_w / ratio - # Centrer le cadre + # Centrer x = (img_w - crop_w) / 2 y = (img_h - crop_h) / 2 diff --git a/src/ui/widgets/crop_overlay.py b/src/ui/widgets/crop_overlay.py index bd3930d..4b15d0b 100644 --- a/src/ui/widgets/crop_overlay.py +++ b/src/ui/widgets/crop_overlay.py @@ -8,7 +8,7 @@ HANDLE_SIZE = 20 class CropOverlay(QGraphicsRectItem): - """Rectangle de recadrage avec poignées tactiles redimensionnables.""" + """Rectangle de recadrage avec ratio verrouillé et poignées tactiles.""" def __init__(self, rect, parent=None): super().__init__(rect, parent) @@ -19,15 +19,41 @@ class CropOverlay(QGraphicsRectItem): self.setAcceptHoverEvents(True) self._resizing = False - self._resize_edge = None + self._resize_corner = None self._start_rect = None self._start_pos = None self._bounds = None + # Ratio verrouillé (largeur / hauteur) + self._locked_ratio = rect.width() / rect.height() if rect.height() > 0 else 1.5 def set_bounds(self, rect): """Limites dans lesquelles le crop peut bouger (la photo).""" self._bounds = rect + def set_ratio(self, ratio): + """Change le ratio verrouillé.""" + self._locked_ratio = ratio + + def itemChange(self, change, value): + """Empêche le cadre de sortir des limites de la photo.""" + if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._bounds: + r = self.rect() + new_pos = value + b = self._bounds + + # Contraindre le déplacement + if new_pos.x() + r.left() < b.left(): + new_pos.setX(b.left() - r.left()) + if new_pos.y() + r.top() < b.top(): + new_pos.setY(b.top() - r.top()) + if new_pos.x() + r.right() > b.right(): + new_pos.setX(b.right() - r.right()) + if new_pos.y() + r.bottom() > b.bottom(): + new_pos.setY(b.bottom() - r.bottom()) + + return new_pos + return super().itemChange(change, value) + def paint(self, painter, option, widget=None): super().paint(painter, option, widget) r = self.rect() @@ -36,7 +62,6 @@ class CropOverlay(QGraphicsRectItem): if self._bounds: painter.setBrush(QBrush(QColor(0, 0, 0, 120))) painter.setPen(Qt.PenStyle.NoPen) - # Les 4 zones sombres autour du crop b = self.mapRectFromScene(self._bounds) # Haut painter.drawRect(QRectF(b.left(), b.top(), b.width(), r.top() - b.top())) @@ -64,10 +89,10 @@ class CropOverlay(QGraphicsRectItem): def mousePressEvent(self, event): pos = event.pos() - edge = self._detect_edge(pos) - if edge: + corner = self._detect_corner(pos) + if corner: self._resizing = True - self._resize_edge = edge + self._resize_corner = corner self._start_rect = QRectF(self.rect()) self._start_pos = pos event.accept() @@ -78,20 +103,69 @@ class CropOverlay(QGraphicsRectItem): if self._resizing: delta = event.pos() - self._start_pos r = QRectF(self._start_rect) - e = self._resize_edge + corner = self._resize_corner - if "left" in e: - r.setLeft(r.left() + delta.x()) - if "right" in e: - r.setRight(r.right() + delta.x()) - if "top" in e: - r.setTop(r.top() + delta.y()) - if "bottom" in e: - r.setBottom(r.bottom() + delta.y()) + # Redimensionner en gardant le ratio + # On utilise le déplacement diagonal le plus grand + if "right" in corner: + dx = delta.x() + else: + dx = -delta.x() + + if "bottom" in corner: + dy = delta.y() + else: + dy = -delta.y() + + # Prendre le delta dominant + if abs(dx) > abs(dy): + new_w = r.width() + dx + new_h = new_w / self._locked_ratio + else: + new_h = r.height() + dy + new_w = new_h * self._locked_ratio # Taille minimum - if r.width() >= 50 and r.height() >= 50: - self.setRect(r) + if new_w < 50 or new_h < 50: + event.accept() + return + + # Appliquer selon le coin tiré + if corner == "bottomright": + r.setWidth(new_w) + r.setHeight(new_h) + elif corner == "bottomleft": + r.setLeft(r.right() - new_w) + r.setHeight(new_h) + elif corner == "topright": + r.setWidth(new_w) + r.setTop(r.bottom() - new_h) + elif corner == "topleft": + r.setLeft(r.right() - new_w) + r.setTop(r.bottom() - new_h) + + # Contraindre aux limites de la photo + if self._bounds: + b = self.mapRectFromScene(self._bounds) + # Ne pas dépasser les bords + if r.left() < b.left(): + r.moveLeft(b.left()) + if r.top() < b.top(): + r.moveTop(b.top()) + if r.right() > b.right(): + over = r.right() - b.right() + new_w = r.width() - over + new_h = new_w / self._locked_ratio + r.setWidth(new_w) + r.setHeight(new_h) + if r.bottom() > b.bottom(): + over = r.bottom() - b.bottom() + new_h = r.height() - over + new_w = new_h * self._locked_ratio + r.setWidth(new_w) + r.setHeight(new_h) + + self.setRect(r) event.accept() else: super().mouseMoveEvent(event) @@ -99,24 +173,28 @@ class CropOverlay(QGraphicsRectItem): def mouseReleaseEvent(self, event): if self._resizing: self._resizing = False - self._resize_edge = None + self._resize_corner = None event.accept() else: super().mouseReleaseEvent(event) - def _detect_edge(self, pos): + def _detect_corner(self, pos): r = self.rect() margin = HANDLE_SIZE - edges = "" - if abs(pos.y() - r.top()) < margin: - edges += "top" - elif abs(pos.y() - r.bottom()) < margin: - edges += "bottom" - if abs(pos.x() - r.left()) < margin: - edges += "left" - elif abs(pos.x() - r.right()) < margin: - edges += "right" - return edges or None + near_top = abs(pos.y() - r.top()) < margin + near_bottom = abs(pos.y() - r.bottom()) < margin + near_left = abs(pos.x() - r.left()) < margin + near_right = abs(pos.x() - r.right()) < margin + + if near_top and near_left: + return "topleft" + if near_top and near_right: + return "topright" + if near_bottom and near_left: + return "bottomleft" + if near_bottom and near_right: + return "bottomright" + return None def get_crop_rect(self): """Retourne le rectangle de crop en coordonnées scène."""