feat(tgu-ui): выбор полосы слотов мышью «от первого к последнему»

Замена авто-чипа «полоса ×N» на ручной жест. Полоса переопределена:
один КА + один крен (ε) + подряд по времени; виток в границу не входит.

- slotStrips.ts: groupStrips → { idByKey, membersOf }; группировка по КА →
  кластеры по крену (ROLL_EPSILON=0.5) → деление по каденции (CADENCE_GAP_FACTOR=1.5)
- slotSelection.ts: selectStripRange вместо toggleStripSelection
- TguSlotsTab.tsx: клик по строке = якорь + выбор; Shift+клик тянет полосу
  между якорем и строкой; выход за полосу запрещён + подсказка; рельс полосы якоря
- tgu-planning.css: убраны стили чипа/hover; user-select:none на строках
- тесты обновлены (125 зелёных), tsc/build чисто
This commit is contained in:
Дмитрий Соловьев
2026-06-17 10:13:11 +03:00
parent 8faaa03af3
commit b092e50dd8
7 changed files with 322 additions and 296 deletions
+29 -10
View File
@@ -90,16 +90,35 @@
| disabled chip | N<2 или 0 выбираемых членов | чипа нет |
| booked/occupied | как есть (бейдж) | не меняется; в полосу входит визуально, в выбор — нет |
## Реализовано (it.1, 2026-06-17)
- `slotStrips.ts` (`groupStrips`, `ROLL_EPSILON=0.01`, якорный крен, тотальный порядок) +
`slotStrips.test.ts` (11 кейсов: мультиполоса, два КА, граница ε, дрейф крена, порядок,
tie-break, NaN `tn`, пустой вход).
- `toggleStripSelection` в `slotSelection.ts` (чистый, без мутации) + тесты.
- `TguSlotsTab.tsx`: `stripIdByKey` (memo над `displaySlots`), `stripInfo` (видимые члены /
выбираемые не-BUSY по полосе), чип `полоса ×N` в ячейке «КА» (активен при всех выбранных),
рельс по `StripId` (sortMode `time`), подсветка членов по `StripId` при наведении.
- Смежный баг закрыт: `anchorIndexRef` сбрасывается при смене видимого набора (`filteredSlots`).
- Тесты 123 зелёных, `tsc -b` чисто, `vite build` ок. Бэкенд/DTO не тронуты.
## Реализовано (it.1, 2026-06-17 — пересмотрено)
ВАЖНО: первая версия it.1 (авто-чип «полоса ×N» по равному крену) оказалась не той фичей и
переделана. На реальных данных полоса по «подряд по времени + равный крен» не формировалась
(в один момент идёт «веер» кренов с шагом ~3°, равный крен повторяется не подряд, а вдоль трассы
через каденцию). Оператор хотел не авто-магию, а ручной выбор полосы мышью «от первого к последнему»
с запретом выйти за полосу.
Переопределение полосы (согласовано с оператором):
- **полоса = один КА + один крен (допуск `ROLL_EPSILON`) + подряд по времени (без разрыва каденции);**
- **виток/цикл в границу НЕ входят** — у экватора номер витка меняется, а слоты остаются в одной полосе.
Сделано:
- `slotStrips.ts`: `groupStrips(slots, keyOf) → { idByKey, membersOf }`. Группировка: по КА →
кластеризация по крену (`ROLL_EPSILON=0.5`, разрыв > ε начинает кластер) → внутри кластера деление
по непрерывности времени (`CADENCE_GAP_FACTOR=1.5` от медианной каденции). `PolosaId =
polosa:${ключ первого по времени члена}`; `membersOf` — члены в порядке времени.
- `slotSelection.ts`: вместо `toggleStripSelection` — `selectStripRange(current, members, fromKey,
toKey)` (чистая: добавляет членов полосы между двумя кликами включительно, без мутации).
- `TguSlotsTab.tsx`: убран чип/hover/`stripInfo`. Жест: обычный клик ставит якорь; Shift+клик по
слоту той же полосы выделяет членов между якорем и кликом (BUSY исключаются); Shift+клик по слоту
другой полосы/КА — выбор не меняется + подсказка «только в пределах одной полосы». Якорь — состояние
(`anchorKey`), строки полосы якоря подсвечены рельсом (оттенок по `PolosaId`); сбрасывается при
смене заявки/сортировки/фильтров/расчёта. Подсказка жеста в selbar.
- `tgu-planning.css`: удалены стили чипа/hover; рельс `tgu-slots-row--strip` переиспользован под
полосу якоря, добавлен `is-strip-anchor`.
- Калибровка `ROLL_EPSILON=0.5` / `CADENCE_GAP_FACTOR=1.5` — по скрину данных; при ручном прогоне
возможна подстройка по фактическим `roll`/`tn`.
- Бэкенд/DTO не тронуты. Правило выполнимости (манёвренность КА) по-прежнему it.2.
## Решение — Iteration 2 (проверка выполнимости, отдельно)
- Бэкенд: read-only `POST /api/slots/booking/validate` (набор слотов → violations),