"""Mouse handlers for exclusion shapes.""" from __future__ import annotations class ExclusionMixin: """Manage exclusion shapes (rectangles and freehand strokes) on the preview canvas.""" def _exclude_start(self, event): if self.preview_img is None: return mode = getattr(self, "exclude_mode", "rect") x = max(0, min(self.preview_img.width - 1, int(event.x))) y = max(0, min(self.preview_img.height - 1, int(event.y))) if mode == "free": self._current_stroke = [(x, y)] preview_id = getattr(self, "_stroke_preview_id", None) if preview_id: try: self.canvas_orig.delete(preview_id) except Exception: pass self._stroke_preview_id = self.canvas_orig.create_line( x, y, x, y, fill="yellow", width=2, smooth=True, capstyle="round", joinstyle="round", ) self._rubber_start = None return self._rubber_start = (x, y) if self._rubber_id: try: self.canvas_orig.delete(self._rubber_id) except Exception: pass self._rubber_id = self.canvas_orig.create_rectangle(x, y, x, y, outline="yellow", width=2) def _exclude_drag(self, event): mode = getattr(self, "exclude_mode", "rect") if mode == "free": stroke = getattr(self, "_current_stroke", None) if not stroke: return x = max(0, min(self.preview_img.width - 1, int(event.x))) y = max(0, min(self.preview_img.height - 1, int(event.y))) if stroke[-1] != (x, y): stroke.append((x, y)) preview_id = getattr(self, "_stroke_preview_id", None) if preview_id: coords = [coord for point in stroke for coord in point] self.canvas_orig.coords(preview_id, *coords) return if not self._rubber_start: return x0, y0 = self._rubber_start x1 = max(0, min(self.preview_img.width - 1, int(event.x))) y1 = max(0, min(self.preview_img.height - 1, int(event.y))) self.canvas_orig.coords(self._rubber_id, x0, y0, x1, y1) def _exclude_end(self, event): mode = getattr(self, "exclude_mode", "rect") if mode == "free": stroke = getattr(self, "_current_stroke", None) if stroke and len(stroke) > 2: polygon = self._close_polygon(self._compress_stroke(stroke)) if len(polygon) >= 3: shape = { "kind": "polygon", "points": polygon, } self.exclude_shapes.append(shape) stamper = getattr(self, "_stamp_shape_on_mask", None) if callable(stamper): stamper(shape) else: self._exclude_mask_dirty = True self._current_stroke = None preview_id = getattr(self, "_stroke_preview_id", None) if preview_id: try: self.canvas_orig.delete(preview_id) except Exception: pass self._stroke_preview_id = None self.update_preview() return if not self._rubber_start: return x0, y0 = self._rubber_start x1 = max(0, min(self.preview_img.width - 1, int(event.x))) y1 = max(0, min(self.preview_img.height - 1, int(event.y))) rx0, rx1 = sorted((x0, x1)) ry0, ry1 = sorted((y0, y1)) if (rx1 - rx0) > 0 and (ry1 - ry0) > 0: shape = {"kind": "rect", "coords": (rx0, ry0, rx1, ry1)} self.exclude_shapes.append(shape) stamper = getattr(self, "_stamp_shape_on_mask", None) if callable(stamper): stamper(shape) else: self._exclude_mask_dirty = True if self._rubber_id: try: self.canvas_orig.delete(self._rubber_id) except Exception: pass self._rubber_start = None self._rubber_id = None self.update_preview() def clear_excludes(self): self.exclude_shapes = [] self._rubber_start = None self._current_stroke = None if self._rubber_id: try: self.canvas_orig.delete(self._rubber_id) except Exception: pass self._rubber_id = None if self._stroke_preview_id: try: self.canvas_orig.delete(self._stroke_preview_id) except Exception: pass self._stroke_preview_id = None for item in getattr(self, "_exclude_canvas_ids", []): try: self.canvas_orig.delete(item) except Exception: pass self._exclude_canvas_ids = [] self._exclude_mask = None self._exclude_mask_px = None self._exclude_mask_dirty = True self.update_preview() def undo_exclude(self): if not getattr(self, "exclude_shapes", None): return self.exclude_shapes.pop() self._exclude_mask_dirty = True self.update_preview() def toggle_exclusion_mode(self): current = getattr(self, "exclude_mode", "rect") next_mode = "free" if current == "rect" else "rect" self.exclude_mode = next_mode self._current_stroke = None if next_mode == "free": if self._rubber_id: try: self.canvas_orig.delete(self._rubber_id) except Exception: pass self._rubber_id = None self._rubber_start = None else: if self._stroke_preview_id: try: self.canvas_orig.delete(self._stroke_preview_id) except Exception: pass self._stroke_preview_id = None message_key = "status.free_draw_enabled" if next_mode == "free" else "status.free_draw_disabled" if hasattr(self, "status"): try: self.status.config(text=self._t(message_key)) except Exception: pass @staticmethod def _compress_stroke(points: list[tuple[int, int]]) -> list[tuple[int, int]]: """Reduce duplicate points without altering the drawn path too much.""" if not points: return [] compressed: list[tuple[int, int]] = [points[0]] for point in points[1:]: if point != compressed[-1]: compressed.append(point) return compressed @staticmethod def _close_polygon(points: list[tuple[int, int]]) -> list[tuple[int, int]]: """Ensure the polygon is closed by repeating the start if necessary.""" if len(points) < 3: return points closed = list(points) if closed[0] != closed[-1]: closed.append(closed[0]) return closed __all__ = ["ExclusionMixin"]