# utils/click.py — Click detection: nearest leg, nearest city, coordinate extraction import math from typing import Optional, Tuple import gradio as gr from config import get_style, DEFAULT_STYLE from core.geometry import control_point, sample_curve from models.city import DB def _extract_xy(evt: gr.SelectData) -> Tuple[Optional[float], Optional[float], Optional[any]]: """Extract (x, y, size) in base image pixels from Gradio click event.""" if evt is None: return None, None, None x = getattr(evt, "x", None) y = getattr(evt, "y", None) size = getattr(evt, "size", None) if x is not None and y is not None: return float(x), float(y), size idx = getattr(evt, "index", None) if isinstance(idx, (list, tuple)) and len(idx) >= 2: return float(idx[0]), float(idx[1]), size val = getattr(evt, "value", None) if isinstance(val, dict) and ("x" in val) and ("y" in val): w = val.get("width") h = val.get("height") if (w is None or h is None) and isinstance(val.get("image"), dict): w = val["image"].get("width") h = val["image"].get("height") return float(val["x"]), float(val["y"]), (w, h) return None, None, None def _nearest_leg_on_click(x, y, legs, to_xy, bulge_overrides=None, n=220): best = None _default_bulge = get_style(DEFAULT_STYLE).curve_bulge for idx, e in enumerate(legs or []): a, b = e.get("a"), e.get("b") if a not in DB.by_name or b not in DB.by_name: continue bulge = float((bulge_overrides or {}).get((a, b), _default_bulge)) p0 = to_xy(a) p2 = to_xy(b) pc = control_point(p0, p2, bulge) pts = sample_curve(p0, pc, p2, n=n) best_d = 1e18 best_i = 0 for i, (xx, yy) in enumerate(pts): d = (xx - x) ** 2 + (yy - y) ** 2 if d < best_d: best_d = d best_i = i d_px = math.sqrt(best_d) t = best_i / (n - 1) if (best is None) or (d_px < best[0]): best = (d_px, idx, t) return best def _nearest_city_on_click(x, y, to_xy, city_names): best_nm, best_d = None, 1e18 for nm in city_names: cx, cy = to_xy(nm) d = math.hypot(x - cx, y - cy) if d < best_d: best_d, best_nm = d, nm return best_nm, best_d