Skip to content

Use Case #4: Отмена и Повтор действий (Undo / Redo / Ctrl+Z)

Исходное состояние: Пользователь строит маршрут и совершает действие: ставит случайную точку в стороне, неудачно тянет трек, удаляет важный чекпоинт или меняет профиль с «Шоссе» на «MTB» (из-за чего трек перестраивается по корягам). Действие: Пользователь нажимает кнопку ↩️ (Undo) в UI или использует шорткат Ctrl+Z / Cmd+Z.


1. Интент (Зачем это юзеру на самом деле?)

Два принципиально разных мотива:

  1. Режим "Упс, ошибка" (Паника/Спасение): Случайный мисклик, дрогнула рука. Нужно мгновенно вернуть всё как было.
  2. Режим "Исследование / A/B тест": Юзер думает: "А что если пустить маршрут вооон через ту гору?" Перетягивает туда трек, видит, что набор высоты подскочил на +1000м. Жмет Undo, затем Redo, чтобы снова оценить цифры, и выбирает лучший вариант. Здесь Undo — это полноценный инструмент аналитики.

2. Идеальная логика системы (State Management)

  • Изоляция мутаций от вьюпорта: В стек истории (History Stack) должны записываться только смысловые изменения маршрута. Смещение карты мышкой (Pan) или масштабирование (Zoom) НИКОГДА не должны попадать в стек.
  • Zero-Latency (Мгновенность из кэша): При нажатии Undo система не должна заново отправлять запрос к серверам роутинга. Состояние трека (геометрия + метрики) должно сохраняться локально (Snapshots) и доставаться из памяти мгновенно для комфортного A/B тестирования.

3. Визуальный отклик и интерфейс (UX/UI)

  • Умная камера (Smart Viewport) — Критический момент! Если измененный участок находится вне текущего экрана (юзер сдвинул карту после ошибки), камера обязана сделать плавный перелет (Pan & Zoom), чтобы место изменения оказалось в центре. Пользователь должен увидеть результат отмены!
  • Синхронизация метрик: Цифры дистанции и график высот должны мгновенно прыгнуть к старым значениям. Идеально — добавить легкую CSS-анимацию подсветки изменения.
  • Индикация кнопок: Кнопки ↩️ и ↪️ всегда на видном месте. Неактивные состояния визуально серые (Disabled).

4. Антипаттерны (Как делать НЕ надо)

  • Сброс стека (Амнезия): Очистка Undo при "автосохранении" или сворачивании панелей. Стек должен жить локально до закрытия вкладки.
  • Запись микро-движений (Drag & Drop): Движение мыши не должно создавать 10 разных шагов в Undo. Весь процесс MouseDown -> Drag -> MouseUp — это строго один атомарный шаг истории.
  • Конфликт фокуса ввода: Нажатие Ctrl+Z стирает букву в строке поиска, а не отменяет маршрут (или наоборот). Хоткеи карты должны корректно отключаться, если фокус активно стоит в текстовом инпуте.

5. Edge Cases (Граничные случаи)

  • Отмена в процессе ожидания (Лоадер): Юзер бросил точку далеко, появился лоадер. Он тут же жмет Ctrl+Z.
    • Решение: Система мгновенно прерывает (AbortController) текущий API-запрос к движку роутинга и откатывает стейт.
  • Глобальное удаление: Случайное нажатие "Очистить маршрут" (Clear All).
    • Решение: Полное удаление обязано попадать в стек Undo. Кнопка ↩️ становится активной для полного спасения работы.
  • Временной парадокс (Перезапись будущего): Юзер сделал 5 шагов. Нажал Undo до шага "2" и сделал новое действие.
    • Решение: Будущая ветка Redo (шаги 3, 4, 5) безвозвратно удаляется.

Эпики и Задачи (Epics & Tasks)

Декомпозиция Use Case #4 для разработки.

Epic 1: Управление состоянием (Zustand History Stack)

  • Task 1.1: Архитектура Undo/Redo. Интегрировать middleware (например, zundo) или написать кастомную логику поверх plannerStore. Стек должен хранить полные snapshots массива Waypoints, метрик и геометрии трека, чтобы возврат происходил за 0 мс без API вызовов.
  • Task 1.2: Изоляция UI состояния. Убедиться, что стейт карты (center, zoom, pitch) вынесен в отдельный стор или исключен из отслеживания Undo. Стек истории реагирует только на мутации роутинга.
  • Task 1.3: Очистка Redo при новой мутации (Time Paradox). При совершении любого нового смыслового действия (добавление точки) стек future (Redo) должен полностью очищаться.
  • Task 1.4: Поддержка глобальной очистки. Действие "Clear Route" должно просто пушить пустой snapshot в стек истории, а не сбрасывать сам стек.

Epic 2: Обработка событий и Хоткеи

  • Task 2.1: Атомарность Drag & Drop. При работе с "резинкой" или перемещении точек запись в стек Undo должна происходить только один раз — на событие onDragEnd (или после получения ответа от сервера).
  • Task 2.2: Глобальный перехватчик (Keyboard Listener). Добавить обработчик для Ctrl+Z / Cmd+Z и Ctrl+Shift+Z / Cmd+Shift+Z. Написать проверку: если document.activeElement является input или textarea, событие отмены маршрута игнорируется (браузер отрабатывает свой нативный текстовый Undo).
  • Task 2.3: Отмена API запросов (AbortController). Доработать API адаптер роутинга: при вызове Undo/Redo все pending запросы к BRouter/OSRM немедленно прерываются через AbortSignal, чтобы не затереть возвращенный из кэша стейт опоздавшим ответом.

Epic 3: Визуальный отклик (Smart Viewport & Animations)

  • Task 3.1: Умная камера при Undo (Smart Viewport).
    • При срабатывании Undo система вычисляет Bounding Box разницы между старым и новым состоянием трека/точек.
    • Если эта область не помещается в текущий Map viewport, вызывается метод fitBounds или flyTo, чтобы принудительно показать пользователю место изменения.
  • Task 3.2: Анимация изменения метрик. Добавить легкую CSS-вспышку или перелистывание цифр в компонентах DistanceWidget и ElevationWidget при срабатывании Undo/Redo.
  • Task 3.3: Состояния UI кнопок. Связать disabled состояние кнопок ↩️ и ↪️ с длинами массивов past и future в сторе.