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,90 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { RevolutionSign } from "../../model/slotTypes";
|
||||
import { groupPasses } from "./slotPasses";
|
||||
|
||||
/** Минимальный слот для группировки в витки. */
|
||||
type S = {
|
||||
satelliteId: number;
|
||||
slotNumber: number;
|
||||
cycle: number;
|
||||
revolution: number;
|
||||
revolutionSign: RevolutionSign;
|
||||
tn: string;
|
||||
};
|
||||
|
||||
const keyOf = (s: S) => `${s.satelliteId}:${s.slotNumber}:${s.cycle}`;
|
||||
|
||||
/** Слот на минуту mm от базовой даты; виток задаётся (revolution, sign, cycle). */
|
||||
function slot(
|
||||
satelliteId: number,
|
||||
slotNumber: number,
|
||||
mm: number,
|
||||
revolution: number,
|
||||
revolutionSign: RevolutionSign = "ASC",
|
||||
cycle = 1,
|
||||
): S {
|
||||
return {
|
||||
satelliteId,
|
||||
slotNumber,
|
||||
cycle,
|
||||
revolution,
|
||||
revolutionSign,
|
||||
tn: `2026-06-17T10:${String(mm).padStart(2, "0")}:00`,
|
||||
};
|
||||
}
|
||||
|
||||
describe("groupPasses", () => {
|
||||
it("пустой вход → пустой список", () => {
|
||||
expect(groupPasses([], keyOf)).toEqual([]);
|
||||
});
|
||||
|
||||
it("веер кренов одного оборота → один виток (крен не дробит)", () => {
|
||||
// Три нацеливания одного прохода КА 1, оборот 100, восх — все в один виток.
|
||||
const passes = groupPasses(
|
||||
[slot(1, 1, 0, 100), slot(1, 2, 0, 100), slot(1, 3, 1, 100)],
|
||||
keyOf,
|
||||
);
|
||||
expect(passes).toHaveLength(1);
|
||||
expect(passes[0].id).toBe("1:1:100:ASC");
|
||||
expect(passes[0].keys).toEqual(["1:1:1", "1:2:1", "1:3:1"]);
|
||||
expect(passes[0].satelliteId).toBe(1);
|
||||
expect(passes[0].revolution).toBe(100);
|
||||
});
|
||||
|
||||
it("разные обороты / ветви / КА → разные витки", () => {
|
||||
const passes = groupPasses(
|
||||
[
|
||||
slot(1, 1, 0, 100, "ASC"),
|
||||
slot(1, 2, 5, 100, "DESC"), // та же revolution, другая ветвь
|
||||
slot(1, 3, 10, 101, "ASC"), // другой оборот
|
||||
slot(2, 4, 2, 100, "ASC"), // другой КА
|
||||
],
|
||||
keyOf,
|
||||
);
|
||||
expect(passes.map((p) => p.id)).toEqual([
|
||||
"1:1:100:ASC", // 10:00
|
||||
"2:1:100:ASC", // 10:02
|
||||
"1:1:100:DESC", // 10:05
|
||||
"1:1:101:ASC", // 10:10
|
||||
]);
|
||||
});
|
||||
|
||||
it("витки упорядочены по началу прохода (хронология по всем КА)", () => {
|
||||
const passes = groupPasses(
|
||||
[slot(1, 1, 30, 100), slot(2, 2, 5, 200), slot(1, 3, 15, 101)],
|
||||
keyOf,
|
||||
);
|
||||
expect(passes.map((p) => p.startMs)).toEqual(
|
||||
[...passes.map((p) => p.startMs)].sort((a, b) => a - b),
|
||||
);
|
||||
expect(passes[0].satelliteId).toBe(2); // самый ранний (10:05)
|
||||
});
|
||||
|
||||
it("ключи внутри витка отсортированы по времени", () => {
|
||||
const passes = groupPasses(
|
||||
[slot(1, 3, 2, 100), slot(1, 1, 0, 100), slot(1, 2, 1, 100)],
|
||||
keyOf,
|
||||
);
|
||||
expect(passes[0].keys).toEqual(["1:1:1", "1:2:1", "1:3:1"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user