ICRA/app/gui/exclusions.py

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"]