208 lines
7.5 KiB
Python
208 lines
7.5 KiB
Python
"""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
|
|
accent = self._exclusion_preview_colour()
|
|
self._stroke_preview_id = self.canvas_orig.create_line(
|
|
x,
|
|
y,
|
|
x,
|
|
y,
|
|
fill=accent,
|
|
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
|
|
accent = self._exclusion_preview_colour()
|
|
self._rubber_id = self.canvas_orig.create_rectangle(x, y, x, y, outline=accent, 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
|
|
self._rubber_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
|
|
|
|
def _exclusion_preview_colour(self) -> str:
|
|
is_dark = getattr(self, "theme", "light") == "dark"
|
|
return "#ffd700" if is_dark else "#c56217"
|
|
|
|
@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"]
|