// Чистая логика выбора слотов и разбиения на чанки — выделена из TguSlotsTab, // чтобы покрыть юнит-тестами без рендера компонента. /** * Новое множество выбора после клика по строке. * Обычный клик — переключает один ключ. Shift+клик при заданном якоре — выбирает (или * снимает) непрерывный диапазон от якоря до текущего индекса; направление задаётся новым * состоянием текущей строки (если она была не выбрана — диапазон выбираем, иначе снимаем). * * @param current Текущее множество выбранных ключей. * @param keyAt Доступ к ключу слота по индексу в видимом (отфильтрованном) списке. * @param index Индекс строки, по которой кликнули. * @param shiftKey Был ли зажат Shift. * @param anchorIndex Индекс предыдущего клика (якорь) либо null. */ export function selectionAfterCheck( current: ReadonlySet, keyAt: (index: number) => string, index: number, shiftKey: boolean, anchorIndex: number | null, ): Set { const next = new Set(current) const targetKey = keyAt(index) if (shiftKey && anchorIndex != null) { const lo = Math.min(anchorIndex, index) const hi = Math.max(anchorIndex, index) const select = !current.has(targetKey) for (let i = lo; i <= hi; i++) { const k = keyAt(i) if (select) next.add(k) else next.delete(k) } } else if (next.has(targetKey)) { next.delete(targetKey) } else { next.add(targetKey) } return next } /** * Разбивает массив на чанки заданного размера (для чанкованной брони крупных наборов). * @param items Исходный массив. * @param size Размер чанка; <= 0 — один чанк со всем массивом. */ export function chunk(items: readonly T[], size: number): T[][] { if (size <= 0) return items.length ? [items.slice()] : [] const out: T[][] = [] for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size)) return out }