c402f376c5
Степпер прохождений (виток как проход над районом): - slotPasses.ts: группировка слотов в проходы (КА·cycle·revolution·sign) в хронологии по всем КА + юнит-тесты; - PassStepper.tsx: ◀/▶ листают проходы, окно суток следует за курсором, таблица скроллится к проходу, карта не прыгает; выбранный проход изолирует отображение, «Все» — назад ко всему набору; - дата/время прохождения отдельной строкой во всю ширину (видно целиком). Подсветка конфликтов выбора (мягкая подсказка «станет недоступным»): - slotConflicts.ts: тот же КА + недостаточный зазор на манёвр — 10 с при том же крене, 30 с при смене крена (ROLL_EPSILON); конфликтные слоты гаснут в таблице и на карте, остаются выбираемыми (жёсткая выполнимость — за бэком); - производительность: оконный поиск по времени (~O(N·logM)), кандидаты — только видимые, выбор через useDeferredValue (не молотит во время drag); - чекбокс «Гасить конфликты» в строке выбора: выключает и слой, и расчёт; - юнит-тесты на правило зазора и оконный поиск. Карта: проп dimmedObjectIds — серый «налёт» поверх конфликтных слотов в SVG overlay (без изменения canvas).
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
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>
|
||
);
|
||
}
|