feat(tgu-ui): выбор полосы слотов мышью на карте (Shift+drag)

This commit is contained in:
Дмитрий Соловьев
2026-06-17 11:31:21 +03:00
parent 61a2c92d28
commit 653c930dd6
5 changed files with 239 additions and 4 deletions
@@ -39,7 +39,7 @@ import {
} from "./coverage";
import { applySlotFilters, slotStatusKey, type SlotStatusKey } from "./slotFilters";
import { selectionAfterCheck, selectStripRange } from "./slotSelection";
import { groupStrips } from "./slotStrips";
import { groupStrips, pickStripAnchor, pickStripEnd, type PolosaId } from "./slotStrips";
import { runChunked } from "./runChunked";
import { ChunkProgress } from "./ChunkProgress";
@@ -298,10 +298,61 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
// времени (виток не граница). idByKey — id полосы по ключу слота; membersOf — члены полосы по времени.
const polosa = useMemo(() => groupStrips(displaySlots, slotKey), [displaySlots]);
// Состояние жеста «выбор полосы мышью на карте» (Shift+drag). Держим в ref: правки идут на каждом
// движении указателя, перерисовка не нужна. members — члены полосы якоря без BUSY (их не бронируем).
const mapStripRef = useRef<{ anchorKey: string; anchorId: PolosaId; members: string[] } | null>(null);
// Shift+нажатие по слотам на карте: якорь полосы = самый ранний по времени выбираемый слот под
// курсором. Выбор этой полосы сбрасываем к одному якорю (далее drag растянет), как и в таблице.
const handleStripAnchor = useCallback(
(keys: string[]) => {
const tnMsOf = (key: string) => {
const slot = slotByKey.get(key);
return slot ? parseLocal(slot.tn) : Number.NaN;
};
const selectable = keys.filter((key) => {
const slot = slotByKey.get(key);
return slot && !slotIsBusy(slot);
});
const anchor = pickStripAnchor(selectable, tnMsOf);
const anchorId = anchor ? polosa.idByKey.get(anchor) : undefined;
if (!anchor || !anchorId) {
mapStripRef.current = null;
return;
}
const members = (polosa.membersOf.get(anchorId) ?? []).filter((key) => {
const slot = slotByKey.get(key);
return !(slot && slotIsBusy(slot));
});
mapStripRef.current = { anchorKey: anchor, anchorId, members };
setAnchorKey(anchor);
setSelectedKeys((current) => selectStripRange(current, members, anchor, anchor));
},
[polosa, slotByKey]
);
// Движение указателя в жесте: конец полосы = самый поздний по времени член ТОЙ ЖЕ полосы под
// курсором (граница полосы соблюдается). Никого из полосы под курсором — оставляем прежний конец.
const handleStripDrag = useCallback(
(keys: string[]) => {
const gesture = mapStripRef.current;
if (!gesture) return;
const end = pickStripEnd(polosa, gesture.anchorId, keys);
if (!end) return;
setSelectedKeys((current) => selectStripRange(current, gesture.members, gesture.anchorKey, end));
},
[polosa]
);
const handleStripCommit = useCallback(() => {
mapStripRef.current = null;
}, []);
// Якорь жеста привязан к ВИДИМОМУ набору: смена фильтров/окна суток/брони делает прежний якорь
// неактуальным → сбрасываем (как при смене сортировки), иначе Shift выделил бы не ту полосу.
useEffect(() => {
setAnchorKey(null);
mapStripRef.current = null;
}, [filteredSlots]);
// Загрузка заявок интервала (на карту); вызывается на входе, при смене диапазона и из фильтра.
@@ -1205,8 +1256,8 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
</span>
)}
</span>
<span className="tgu-slots-selbar__sub" title="Кликните первый слот, затем Shift+клик по последнему — выделится полоса между ними. Выйти за полосу (другой КА/крен/разрыв по времени) нельзя.">
Полоса: клик + Shift+клик
<span className="tgu-slots-selbar__sub" title="Таблица: кликните первый слот, затем Shift+клик по последнему — выделится полоса между ними. На карте: Shift+зажать на слоте и протянуть мышью вдоль полосы. Выйти за полосу (другой КА/крен/разрыв по времени) нельзя.">
Полоса: клик+Shift в таблице · Shift+тянуть на карте
</span>
<div className="tgu-slots-selbar__actions">
<button
@@ -1533,6 +1584,9 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
enableObjectChooser
onObjectsAtPoint={handleObjectsAtPoint}
onObjectSelect={handleObjectSelect}
onStripAnchor={hasRequest ? handleStripAnchor : undefined}
onStripDrag={hasRequest ? handleStripDrag : undefined}
onStripCommit={hasRequest ? handleStripCommit : undefined}
selectedObjectId={hasRequest ? activeSlotId : activeHitId}
markedObjectIds={hasRequest ? markedSlotIds : undefined}
focusPoint={focusPoint}