|
1 | 1 | import time |
2 | 2 | import os |
3 | 3 | import pathlib |
| 4 | +import threading |
4 | 5 | from base64 import b64decode |
5 | 6 |
|
6 | 7 | from . import platform |
@@ -71,6 +72,9 @@ def __init__( |
71 | 72 | self._js_engine = None |
72 | 73 | self._on_event_proxy = None |
73 | 74 | self._on_camera_changed_proxy = None |
| 75 | + self._select_lock = threading.Lock() |
| 76 | + self._pending_select = None |
| 77 | + self._select_running = False |
74 | 78 | self._use_js_engine = ( |
75 | 79 | _default_use_js_engine if use_js_engine is None else bool(use_js_engine) |
76 | 80 | ) |
@@ -101,6 +105,9 @@ def __setstate__(self, state): |
101 | 105 | self._js_engine = None |
102 | 106 | self._on_event_proxy = None |
103 | 107 | self._on_camera_changed_proxy = None |
| 108 | + self._select_lock = threading.Lock() |
| 109 | + self._pending_select = None |
| 110 | + self._select_running = False |
104 | 111 | self._use_js_engine = state.get("use_js_engine", _default_use_js_engine) |
105 | 112 | self._show_gui_controls = state.get("show_gui_controls", True) |
106 | 113 |
|
@@ -434,8 +441,43 @@ def get_position(self, x: int, y: int): |
434 | 441 | return p |
435 | 442 | return None |
436 | 443 |
|
437 | | - @debounce |
438 | 444 | def select(self, x: int, y: int): |
| 445 | + """Queue an object selection at (x, y). |
| 446 | +
|
| 447 | + A single worker processes only the most recent request, so a backlog |
| 448 | + (e.g. hover moves piled up while Python was busy) collapses to the last |
| 449 | + position instead of replaying every queued move. Never runs two selects |
| 450 | + concurrently. |
| 451 | + """ |
| 452 | + if self._render_mutex is None: |
| 453 | + return |
| 454 | + if self.canvas is None or self.canvas.height == 0: |
| 455 | + return |
| 456 | + # Pyodide: JS is only reachable from this thread — run synchronously. |
| 457 | + if is_pyodide: |
| 458 | + self._do_select(int(x), int(y)) |
| 459 | + return |
| 460 | + with self._select_lock: |
| 461 | + self._pending_select = (int(x), int(y)) |
| 462 | + if self._select_running: |
| 463 | + return |
| 464 | + self._select_running = True |
| 465 | + threading.Thread(target=self._select_worker, daemon=True).start() |
| 466 | + |
| 467 | + def _select_worker(self): |
| 468 | + while True: |
| 469 | + with self._select_lock: |
| 470 | + pending = self._pending_select |
| 471 | + self._pending_select = None |
| 472 | + if pending is None: |
| 473 | + self._select_running = False |
| 474 | + return |
| 475 | + try: |
| 476 | + self._do_select(*pending) |
| 477 | + except Exception as e: |
| 478 | + print(f"warning: select failed: {e}") |
| 479 | + |
| 480 | + def _do_select(self, x: int, y: int): |
439 | 481 | """Perform an object selection at (x, y) and dispatch callbacks on matching renderers.""" |
440 | 482 | if self._render_mutex is None: |
441 | 483 | return |
|
0 commit comments