// Чистая логика выбора слотов и разбиения на чанки — выделена из 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 } /** * Новое множество выбора после жеста «полоса от первого к последнему»: выбор полосы становится * РОВНО диапазоном [fromKey..toKey] (в порядке времени). Члены полосы вне диапазона снимаются — * так Shift+клик по слоту ближе к якорю «сжимает» выделение. Выбор за пределами полосы (другие * полосы/КА) сохраняется: снимаем/добавляем только ключи из members. Жест ограничен одной полосой: * вызывающий передаёт members именно её (один КА, один крен, подряд по времени); граница полосы — * гарантия «нельзя выделить не полосу». Если fromKey или toKey не из members — выбор не меняется * (страховка). Вход не мутируется. * * @param current Текущее множество выбранных ключей. * @param members Ключи членов полосы в порядке времени. * @param fromKey Ключ якоря (первый клик). * @param toKey Ключ второго клика. */ export function selectStripRange( current: ReadonlySet, members: readonly string[], fromKey: string, toKey: string, ): Set { const next = new Set(current) const from = members.indexOf(fromKey) const to = members.indexOf(toKey) if (from < 0 || to < 0) return next const lo = Math.min(from, to) const hi = Math.max(from, to) members.forEach((key, i) => { if (i >= lo && i <= hi) next.add(key) else next.delete(key) }) return next } /** * Снимает выбор с диапазона полосы [fromKey..toKey] (в порядке времени). Зеркало selectStripRange: * убирает ровно члены диапазона, не трогая остальной выбор. Нужно для повторного Shift+клика по тому * же концу — он снимает выделенный диапазон. Если fromKey/toKey не из members — выбор не меняется. * * @param current Текущее множество выбранных ключей. * @param members Ключи членов полосы в порядке времени. * @param fromKey Ключ якоря (первый клик). * @param toKey Ключ второго клика. */ export function deselectStripRange( current: ReadonlySet, members: readonly string[], fromKey: string, toKey: string, ): Set { const next = new Set(current) const from = members.indexOf(fromKey) const to = members.indexOf(toKey) if (from < 0 || to < 0) return next const lo = Math.min(from, to) const hi = Math.max(from, to) for (let i = lo; i <= hi; i++) next.delete(members[i]) 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 }