Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Byte-compiled / optimized / DLL files
__pycache__/
__pycache__
*.py[codz]
*$py.class

Expand Down Expand Up @@ -125,9 +125,9 @@ cython_debug/
.abstra/

# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/

Expand Down
36 changes: 23 additions & 13 deletions bot/control/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,16 @@ def __init__(self, base, scene, settings):
self.base.accept("cmd_center", self.recenter)
self.base.accept("cmd_zoom", self.handle_zoom)
self.base.accept("cmd_align_plane", self.align_to_plane)
self.base.accept("cmd_toggle_marker", self.toggle_marker)

# Vitesse de déplacement au clavier
self.key_pan_speed = 2.0

show_marker = bool(settings.get("show_marker_at_start", False))
self._marker_visible = show_marker
if not show_marker:
self.marker.hide()

# Task pour maintenir le marqueur et le gizmo
self.base.taskMgr.add(self.update_task, "CameraUpdateTask")
# Configure la camera par rapport au contenu de la scene
Expand Down Expand Up @@ -110,6 +116,23 @@ def create_marker(self):
ls.drawTo(v)
return NodePath(ls.create())

def toggle_marker(self):
"""Show or hide the camera pivot marker."""
self._marker_visible = not getattr(self, "_marker_visible", True)
if self._marker_visible:
self.marker.show()
else:
self.marker.hide()

def apply_settings(self, settings: dict):
"""Apply camera settings from config (speeds, marker visibility)."""
if "show_marker_at_start" in settings:
self._marker_visible = bool(settings["show_marker_at_start"])
if self._marker_visible:
self.marker.show()
else:
self.marker.hide()

def handle_rotate(self, dx, dy):
"""
Rotate the camera around the pivot node.
Expand Down Expand Up @@ -267,19 +290,6 @@ def align_to_plane(self, axis):
# On lance et on déverrouille à la fin
Sequence(self.transition, Func(self._unlock)).start()

# Parallel(
# self.focal_node.hprInterval(duration, target_hpr, blendType='easeInOut'),
# self.focal_node.posInterval(duration, center, blendType='easeInOut'),
# LerpFunc(lambda s: self.lens.setFilmSize(s),
# fromData=self.lens.getFilmSize().getX(),
# toData=max_dim * 1.5,
# duration=duration,
# blendType='easeInOut'),
# name="AlignAnimation"
# ).start()

# taskMgr.doMethodLater(duration, self._unlock, "UnlockTask")

def update_task(self, task):
"""
Per-frame task: keep the pivot marker and gizmo in sync with the camera.
Expand Down
97 changes: 0 additions & 97 deletions bot/control/keyboard.py

This file was deleted.

67 changes: 37 additions & 30 deletions bot/control/mouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,28 @@

class MouseHandler:
"""
Handles mouse input and dispatches camera commands via the Panda3D messenger.
Handles mouse input for domain picking and camera gestures.

- Left-button drag → ``cmd_rotate``
- Shift + left-button drag → ``cmd_pan``
- Scroll wheel → ``cmd_zoom``
Domain (priority):
- Hover → ``ViewEventType.HOVER``
- Left click on curve → ``ViewEventType.CURVE_SELECTED``
- Control-point drag → ``CP_PICK_START`` / ``CP_DRAG`` / ``CP_PICK_END``

Local (via shortcut ``GestureTracker``):
- Left-button drag → ``cmd_pan``
- Scroll wheel → ``cmd_zoom``
"""

def __init__(self, base):
def __init__(self, base, gesture_tracker=None):
"""
Register mouse-wheel bindings and start the per-frame update task.
Register the per-frame update task.

Args:
base: Panda3D ShowBase instance.
gesture_tracker: Optional ``GestureTracker`` from the shortcut registry.
"""
self.base = base
self.gesture_tracker = gesture_tracker
self.prev_mouse_pos = None
self._left_was_down = False

Expand All @@ -39,16 +46,11 @@ def __init__(self, base):
self.drag_last_valid_world_pos = None
self.drag_offset = [0.0, 0.0, 0.0]

# Bindings molette souris
self.base.accept(
"wheel_up", lambda: self.base.messenger.send("cmd_zoom", [0.9])
)
self.base.accept(
"wheel_down", lambda: self.base.messenger.send("cmd_zoom", [1.1])
)

self.base.taskMgr.add(self.update, "MouseTask")

def set_gesture_tracker(self, gesture_tracker) -> None:
self.gesture_tracker = gesture_tracker

def set_edit_mode(self, enabled: bool, curve_tag=None):
self.edit_mode_enabled = bool(enabled)
self.active_curve_tag = str(curve_tag) if curve_tag is not None else None
Expand Down Expand Up @@ -228,26 +230,34 @@ def _handle_curve_click(self, m_pos: Point2, left_down: bool):
):
self.base._on_event_cb(ViewEventType.CURVE_SELECTED, metadata["curve_tag"])

def _handle_drag(self, curr_pos: Point2):
if self.dragging_cp:
self.prev_mouse_pos = Point2(curr_pos)
def _current_modifiers(self) -> frozenset[str]:
mods = set()
if inputState.isSet("shift"):
mods.add("shift")
if inputState.isSet("control"):
mods.add("control")
if inputState.isSet("alt"):
mods.add("alt")
return frozenset(mods)

def _handle_gestures(self, curr_pos: Point2, left_down: bool):
if self.gesture_tracker is None:
return

if self.base.mouseWatcherNode.isButtonDown(MouseButton.one()):
if self.prev_mouse_pos is not None:
delta = curr_pos - self.prev_mouse_pos
if delta.lengthSquared() > 0:
self.base.messenger.send("cmd_pan", [delta.getX(), delta.getY()])
self.prev_mouse_pos = Point2(curr_pos)
else:
self.prev_mouse_pos = None
self.gesture_tracker.on_frame(
left_down=left_down,
pos=(curr_pos.getX(), curr_pos.getY()),
modifiers=self._current_modifiers(),
blocked=self.dragging_cp,
)

def update(self, task):
if not self.base.mouseWatcherNode.hasMouse():
if self.dragging_cp:
self._finalize_drag(self.drag_last_valid_world_pos)
self._left_was_down = False
self.prev_mouse_pos = None
if self.gesture_tracker is not None:
self.gesture_tracker.reset()
return task.cont

m_pos = self.base.mouseWatcherNode.getMouse()
Expand All @@ -257,10 +267,7 @@ def update(self, task):
self._handle_hover(m_pos)
self._handle_curve_click(m_pos, left_down)
self._handle_cp_interaction(m_pos, left_down)
self._handle_drag(curr_pos)
self._handle_gestures(curr_pos, left_down)

self._left_was_down = left_down
return task.cont

def is_shift_down(self) -> bool:
return inputState.isSet("shift")
22 changes: 22 additions & 0 deletions bot/control/shortcuts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from bot.control.shortcuts.binding import Click, Drag, Hold, Key, Seq, Wheel
from bot.control.shortcuts.engine import (
Delta,
InputContext,
ShortcutRegistry,
bind,
registry,
)

__all__ = [
"Click",
"Delta",
"Drag",
"Hold",
"InputContext",
"Key",
"Seq",
"ShortcutRegistry",
"Wheel",
"bind",
"registry",
]
61 changes: 61 additions & 0 deletions bot/control/shortcuts/binding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Immutable binding types for the shortcut system."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal


@dataclass(frozen=True)
class Key:
"""Single key press (Panda3D event name, e.g. ``\"c\"``, ``\"shift-x\"``)."""

name: str


@dataclass(frozen=True)
class Seq:
"""Ordered key sequence; fires only when the full sequence matches."""

keys: tuple[str, ...]
timeout: float = 0.4

def __init__(self, *keys: str, timeout: float = 0.4):
object.__setattr__(self, "keys", tuple(keys))
object.__setattr__(self, "timeout", timeout)


@dataclass(frozen=True)
class Wheel:
"""Mouse wheel tick."""

direction: Literal["up", "down"]


@dataclass(frozen=True)
class Click:
"""Mouse click without significant movement."""

button: Literal["left", "right", "middle"]
modifiers: frozenset[str] = field(default_factory=frozenset)


@dataclass(frozen=True)
class Drag:
"""Continuous mouse drag; handler receives ``(ctx, delta)`` each frame."""

button: Literal["left", "right", "middle"]
modifiers: frozenset[str] = field(default_factory=frozenset)


@dataclass(frozen=True)
class Hold:
"""Keys held down; handler is called every frame with pressed-state dict."""

keys: tuple[str, ...]

def __init__(self, *keys: str):
object.__setattr__(self, "keys", tuple(keys))


Binding = Key | Seq | Wheel | Click | Drag | Hold
Loading
Loading