feat(tgu-ui): степпер по прохождениям и подсветка конфликтов выбора слотов
Степпер прохождений (виток как проход над районом): - slotPasses.ts: группировка слотов в проходы (КА·cycle·revolution·sign) в хронологии по всем КА + юнит-тесты; - PassStepper.tsx: ◀/▶ листают проходы, окно суток следует за курсором, таблица скроллится к проходу, карта не прыгает; выбранный проход изолирует отображение, «Все» — назад ко всему набору; - дата/время прохождения отдельной строкой во всю ширину (видно целиком). Подсветка конфликтов выбора (мягкая подсказка «станет недоступным»): - slotConflicts.ts: тот же КА + недостаточный зазор на манёвр — 10 с при том же крене, 30 с при смене крена (ROLL_EPSILON); конфликтные слоты гаснут в таблице и на карте, остаются выбираемыми (жёсткая выполнимость — за бэком); - производительность: оконный поиск по времени (~O(N·logM)), кандидаты — только видимые, выбор через useDeferredValue (не молотит во время drag); - чекбокс «Гасить конфликты» в строке выбора: выключает и слой, и расчёт; - юнит-тесты на правило зазора и оконный поиск. Карта: проп dimmedObjectIds — серый «налёт» поверх конфликтных слотов в SVG overlay (без изменения canvas).
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
type PassStepperProps = {
|
||||
/** Индекс текущего витка (0-based) или -1, если виток не выбран. */
|
||||
index: number;
|
||||
/** Всего витков в наборе. */
|
||||
total: number;
|
||||
/** Подпись текущего витка (КА · оборот · ветвь · время) — пустая, если виток не выбран. */
|
||||
label: string;
|
||||
canPrev: boolean;
|
||||
canNext: boolean;
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
/** Сброс курсора витка — вернуться к показу всего набора. */
|
||||
onClear: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Степпер по виткам (проходам): ◀/▶ переводят курсор на предыдущий/следующий проход в хронологии,
|
||||
* таблица скроллится к нему. Пока прохождение выбрано — отображение изолировано до него; «Все»
|
||||
* возвращает к показу всего набора. Стоит под фильтром «Сутки»: сутки — грубый диапазон, проход —
|
||||
* точный курсор внутри.
|
||||
*/
|
||||
export function PassStepper({
|
||||
index,
|
||||
total,
|
||||
label,
|
||||
canPrev,
|
||||
canNext,
|
||||
onPrev,
|
||||
onNext,
|
||||
onClear,
|
||||
}: PassStepperProps) {
|
||||
const active = index >= 0;
|
||||
return (
|
||||
<div className="map-pass-selector tgu-pass-stepper">
|
||||
<div className="map-pass-selector__header">
|
||||
<span className="map-pass-selector__title">Прохождение</span>
|
||||
<span className="map-pass-selector__nums">{active ? `${index + 1} / ${total}` : `— / ${total}`}</span>
|
||||
<span className="map-pass-selector__times">{active ? "" : "выберите прохождение ◀ ▶"}</span>
|
||||
{active && (
|
||||
<button type="button" className="map-pass-selector__reset" title="Показать все прохождения" onClick={onClear}>
|
||||
Все
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{active && <div className="tgu-pass-stepper__label">{label}</div>}
|
||||
|
||||
<div className="tgu-pass-stepper__nav">
|
||||
<button
|
||||
type="button"
|
||||
className="tgu-pass-stepper__btn"
|
||||
onClick={onPrev}
|
||||
disabled={!canPrev}
|
||||
title="Предыдущее прохождение"
|
||||
aria-label="Предыдущее прохождение"
|
||||
>
|
||||
◀
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tgu-pass-stepper__btn"
|
||||
onClick={onNext}
|
||||
disabled={!canNext}
|
||||
title="Следующее прохождение"
|
||||
aria-label="Следующее прохождение"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user