diff --git a/cull/Models/CullSession.swift b/cull/Models/CullSession.swift index 41eeeb3..b419b0f 100644 --- a/cull/Models/CullSession.swift +++ b/cull/Models/CullSession.swift @@ -259,6 +259,27 @@ final class CullSession { } } + /// Returns the first visible photo from the next and previous `groupCount` groups. + /// Interleaved closest-first so nearest groups are cached with highest priority. + func adjacentGroupRepresentatives(groupCount: Int = 15) -> [Photo] { + guard !groups.isEmpty else { return [] } + var result: [Photo] = [] + for offset in 1...groupCount { + // Next group (closest first due to iteration order) + let nextIdx = (selectedGroupIndex + offset) % groups.count + if let first = groups[nextIdx].photos.first(where: { !isPhotoFiltered($0) }) { + result.append(first) + } + // Previous group + let prevIdx = (selectedGroupIndex - offset + groups.count) % groups.count + if prevIdx != nextIdx, + let first = groups[prevIdx].photos.first(where: { !isPhotoFiltered($0) }) { + result.append(first) + } + } + return result + } + // MARK: - Culling Actions private func applyPhotoState(_ photo: Photo, rating: Int, flag: PhotoFlag, actionName: String) { diff --git a/cull/Views/PhotoViewer.swift b/cull/Views/PhotoViewer.swift index 7e3c88f..6498543 100644 --- a/cull/Views/PhotoViewer.swift +++ b/cull/Views/PhotoViewer.swift @@ -290,15 +290,25 @@ struct PhotoViewer: View { } } - // Preload window fanning out from current position (closest first) + // Preload window: current → interleaved group reps + linear neighbors (closest first) guard !Task.isCancelled, displayedPhotoID == photoID else { return } + let groupReps = session.adjacentGroupRepresentatives(groupCount: 15) let ahead = session.photosAhead(lookaheadCount) let behind = session.photosBehind(lookbehindCount) + + // Interleave: 2 group reps per linear neighbor so group scanning gets priority var fanOut: [Photo] = [] - let maxLen = max(ahead.count, behind.count) - for i in 0..