From f1fadd123fff8c7392722bf87ace891f08b809ba Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 24 Oct 2025 19:27:58 +0200 Subject: [PATCH] feat: add rollback button --- src/car.py | 22 +++++++++++++++++----- src/command.py | 33 +++++++++++++++++++++++++++++---- src/recorder.py | 16 ++++++++++++++-- src/remote_controller.py | 6 +++++- src/snapshot.py | 5 +++++ 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/car.py b/src/car.py index bd31d6c..4ed203c 100644 --- a/src/car.py +++ b/src/car.py @@ -8,7 +8,8 @@ from src.remote_controller import RemoteController from src.utils import get_segments_intersection, segments_intersect from src.vec import Vec -sign = lambda x: 0 if x == 0 else (-1 if x < 0 else 1) + +def sign(x): return 0 if x == 0 else (-1 if x < 0 else 1) class Car: @@ -27,6 +28,8 @@ class Car: RAYS_MAX_DIST = 100 def __init__(self, pos: Vec, direction: Vec) -> None: + self.initial_pos: Vec = pos.copy() + self.initial_dir: Vec = direction.copy() self.pos: Vec = pos self.direction: Vec = direction self.speed: float = 0 @@ -77,7 +80,8 @@ class Car: if show_raycasts: pos: Vec = camera.world2screen(self.pos) for p in self.rays_end: - pygame.draw.line(surf, (255, 0, 0), pos, camera.world2screen(p), 2) + pygame.draw.line(surf, (255, 0, 0), pos, + camera.world2screen(p), 2) pts: list[Vec] = self.get_corners() pts = [camera.world2screen(p) for p in pts] @@ -127,14 +131,17 @@ class Car: n *= -1 dist = -dist self.speed = 0 - self.pos = self.pos + n * (self.COLLISION_MARGIN - dist) + self.pos = self.pos + n * \ + (self.COLLISION_MARGIN - dist) return def cast_rays(self, polygons: list[list[Vec]]): for i in range(self.N_RAYS): - angle: float = radians((i / (self.N_RAYS - 1) - 0.5) * self.RAYS_FOV) + angle: float = radians( + (i / (self.N_RAYS - 1) - 0.5) * self.RAYS_FOV) p: Optional[Vec] = self.cast_ray(angle, polygons) - self.rays[i] = self.RAYS_MAX_DIST if p is None else (p - self.pos).mag() + self.rays[i] = self.RAYS_MAX_DIST if p is None else ( + p - self.pos).mag() self.rays_end[i] = self.pos if p is None else p def cast_ray(self, angle: float, polygons: list[list[Vec]]) -> Optional[Vec]: @@ -161,3 +168,8 @@ class Car: dist = d closest = p return closest + + def reset(self): + self.pos = self.initial_pos.copy() + self.direction = self.initial_dir.copy() + self.speed = 0 diff --git a/src/command.py b/src/command.py index 82e32ab..4ceb890 100644 --- a/src/command.py +++ b/src/command.py @@ -5,10 +5,14 @@ from enum import IntEnum import struct from typing import Type +from src.snapshot import Snapshot + class CommandType(IntEnum): CAR_CONTROL = 0 RECORDING = 1 + APPLY_SNAPSHOT = 2 + RESET = 3 class CarControl(IntEnum): @@ -30,8 +34,8 @@ class Command(abc.ABC): ) Command.REGISTRY[cls.TYPE] = cls - @abc.abstractmethod - def get_payload(self) -> bytes: ... + def get_payload(self) -> bytes: + return b"" def pack(self) -> bytes: payload: bytes = self.get_payload() @@ -43,8 +47,8 @@ class Command(abc.ABC): return Command.REGISTRY[type].from_payload(data[1:]) @classmethod - @abc.abstractmethod - def from_payload(cls, payload: bytes) -> Command: ... + def from_payload(cls, payload: bytes) -> Command: + return cls() class ControlCommand(Command): @@ -82,3 +86,24 @@ class RecordingCommand(Command): def from_payload(cls, payload: bytes) -> Command: state: bool = struct.unpack(">B", payload)[0] return RecordingCommand(state) + + +class ApplySnapshotCommand(Command): + TYPE = CommandType.APPLY_SNAPSHOT + __match_args__ = ("snapshot",) + + def __init__(self, snapshot: Snapshot) -> None: + super().__init__() + self.snapshot: Snapshot = snapshot + + def get_payload(self) -> bytes: + return self.snapshot.pack() + + @classmethod + def from_payload(cls, payload: bytes) -> Command: + snapshot: Snapshot = Snapshot.unpack(payload) + return ApplySnapshotCommand(snapshot) + + +class ResetCommand(Command): + TYPE = CommandType.RESET diff --git a/src/recorder.py b/src/recorder.py index d7c6e37..6392615 100644 --- a/src/recorder.py +++ b/src/recorder.py @@ -9,7 +9,7 @@ from PyQt6.QtCore import QObject, QThread, QTimer, pyqtSignal, pyqtSlot from PyQt6.QtGui import QKeyEvent from PyQt6.QtWidgets import QMainWindow -from src.command import CarControl, Command, ControlCommand, RecordingCommand +from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand from src.record_file import RecordFile from src.recorder_ui import Ui_Recorder from src.snapshot import Snapshot @@ -203,7 +203,19 @@ class RecorderWindow(Ui_Recorder, QMainWindow): self.send_command(RecordingCommand(self.recording)) def rollback(self): - pass + rollback_by: int = self.forgetSnapshotNumber.value() + rollback_by = max(0, min(rollback_by, len(self.snapshots) - 1)) + + self.snapshots = self.snapshots[:-rollback_by] + self.nbrSnapshotSaved.setText(str(len(self.snapshots))) + + if len(self.snapshots) == 0: + self.send_command(ResetCommand()) + else: + self.send_command(ApplySnapshotCommand(self.snapshots[-1])) + + if self.recording: + self.toggle_record() def toggle_autopilot(self): self.autopiloting = not self.autopiloting diff --git a/src/remote_controller.py b/src/remote_controller.py index e18aca3..ad57698 100644 --- a/src/remote_controller.py +++ b/src/remote_controller.py @@ -6,7 +6,7 @@ import struct import threading from typing import TYPE_CHECKING, Optional -from src.command import CarControl, Command, ControlCommand, RecordingCommand +from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand from src.snapshot import Snapshot from src.utils import RepeatTimer @@ -119,6 +119,10 @@ class RemoteController: self.set_control(control, active) case RecordingCommand(state): self.recording = state + case ApplySnapshotCommand(snapshot): + snapshot.apply(self.car) + case ResetCommand(): + self.car.reset() def set_control(self, control: CarControl, active: bool): setattr(self.car, self.CONTROL_ATTRIBUTES[control], active) diff --git a/src/snapshot.py b/src/snapshot.py index c423887..e519290 100644 --- a/src/snapshot.py +++ b/src/snapshot.py @@ -94,3 +94,8 @@ class Snapshot: raycast_distances=car.rays.copy(), image=None ) + + def apply(self, car: Car): + car.pos = self.position.copy() + car.direction = self.direction.copy() + car.speed = 0