not working, claude is ass
This commit is contained in:
@@ -328,9 +328,16 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
/// The item currently being dragged (nil when no drag active)
|
||||
private var draggingItem: ItineraryRowItem?
|
||||
|
||||
/// Row indices that are invalid drop targets for the current drag
|
||||
/// The day number the user is targeting during custom item drag (for stable positioning)
|
||||
private var dragTargetDay: Int?
|
||||
|
||||
/// Row indices that are invalid drop targets for the current drag (for visual dimming)
|
||||
private var invalidRowIndices: Set<Int> = []
|
||||
|
||||
/// Row indices that ARE valid drop targets - pre-calculated at drag start for stability
|
||||
/// Using a sorted array enables O(log n) nearest-neighbor lookup
|
||||
private var validDropRows: [Int] = []
|
||||
|
||||
/// IDs of games that act as barriers for the current travel drag (for gold highlighting)
|
||||
private var barrierGameIds: Set<String> = []
|
||||
|
||||
@@ -605,7 +612,9 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
/// 4. Removes visual dimming and highlighting
|
||||
private func endDrag() {
|
||||
draggingItem = nil
|
||||
dragTargetDay = nil
|
||||
invalidRowIndices = []
|
||||
validDropRows = []
|
||||
barrierGameIds = []
|
||||
isInValidZone = true
|
||||
|
||||
@@ -629,41 +638,40 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
// Get valid day range from pre-calculated ranges
|
||||
guard let validRange = travelValidRanges[travelId] else {
|
||||
invalidRowIndices = []
|
||||
validDropRows = []
|
||||
barrierGameIds = []
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate invalid row indices (rows outside valid day range)
|
||||
// Calculate invalid and valid row indices based on day range
|
||||
// Pre-calculate ALL valid positions for stable drag behavior
|
||||
var invalidRows = Set<Int>()
|
||||
var validRows: [Int] = []
|
||||
|
||||
for (index, rowItem) in flatItems.enumerated() {
|
||||
let dayNum: Int
|
||||
switch rowItem {
|
||||
case .dayHeader(let dayNum, _):
|
||||
// Day header rows for days outside valid range are invalid drop targets
|
||||
// (travel would appear BEFORE this header, making it belong to this day)
|
||||
if !validRange.contains(dayNum) {
|
||||
invalidRows.insert(index)
|
||||
}
|
||||
case .games(_, let dayNum):
|
||||
// Games on days outside valid range are invalid
|
||||
if !validRange.contains(dayNum) {
|
||||
invalidRows.insert(index)
|
||||
}
|
||||
case .travel(_, let dayNum):
|
||||
// Other travel rows on days outside valid range
|
||||
if !validRange.contains(dayNum) {
|
||||
invalidRows.insert(index)
|
||||
}
|
||||
case .dayHeader(let d, _):
|
||||
dayNum = d
|
||||
case .games(_, let d):
|
||||
dayNum = d
|
||||
case .travel(_, let d):
|
||||
dayNum = d
|
||||
case .customItem(let item):
|
||||
// Custom items on days outside valid range
|
||||
if !validRange.contains(item.day) {
|
||||
invalidRows.insert(index)
|
||||
}
|
||||
dayNum = item.day
|
||||
}
|
||||
|
||||
if validRange.contains(dayNum) {
|
||||
validRows.append(index)
|
||||
} else {
|
||||
invalidRows.insert(index)
|
||||
}
|
||||
}
|
||||
|
||||
invalidRowIndices = invalidRows
|
||||
validDropRows = validRows // Already sorted since we iterate in order
|
||||
|
||||
// Find barrier games using ItineraryConstraints
|
||||
// First, find or create the ItineraryItem for this travel
|
||||
if let travelItem = findItineraryItem(for: segment),
|
||||
let constraints = constraints {
|
||||
let barriers = constraints.barrierGames(for: travelItem)
|
||||
@@ -678,15 +686,22 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
/// Custom items can go on any day, but we mark certain positions as
|
||||
/// less ideal (e.g., directly on day headers or before travel).
|
||||
private func calculateCustomItemDragZones(item: ItineraryItem) {
|
||||
// Custom items are flexible - no day restrictions
|
||||
// But we can mark day headers as invalid since items shouldn't drop ON them
|
||||
// Custom items are flexible - can go anywhere except ON day headers
|
||||
// Pre-calculate ALL valid row indices for stable drag behavior
|
||||
var invalidRows = Set<Int>()
|
||||
var validRows: [Int] = []
|
||||
|
||||
for (index, rowItem) in flatItems.enumerated() {
|
||||
if case .dayHeader = rowItem {
|
||||
invalidRows.insert(index)
|
||||
} else {
|
||||
// All non-header rows are valid drop targets
|
||||
validRows.append(index)
|
||||
}
|
||||
}
|
||||
|
||||
invalidRowIndices = invalidRows
|
||||
validDropRows = validRows // Already sorted since we iterate in order
|
||||
barrierGameIds = [] // No barrier highlighting for custom items
|
||||
}
|
||||
|
||||
@@ -859,6 +874,16 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
// Calculate the new day and sortOrder for the dropped position
|
||||
let destinationDay = dayNumber(forRow: destinationIndexPath.row)
|
||||
let sortOrder = calculateSortOrder(at: destinationIndexPath.row)
|
||||
|
||||
// DEBUG: Log the final state after insertion
|
||||
print("🎯 [Drop] source=\(sourceIndexPath.row) → dest=\(destinationIndexPath.row)")
|
||||
print("🎯 [Drop] flatItems around dest:")
|
||||
for i in max(0, destinationIndexPath.row - 2)...min(flatItems.count - 1, destinationIndexPath.row + 2) {
|
||||
let marker = i == destinationIndexPath.row ? "→" : " "
|
||||
print("🎯 [Drop] \(marker) [\(i)] \(flatItems[i])")
|
||||
}
|
||||
print("🎯 [Drop] Calculated day=\(destinationDay), sortOrder=\(sortOrder)")
|
||||
|
||||
onCustomItemMoved?(customItem.id, destinationDay, sortOrder)
|
||||
|
||||
default:
|
||||
@@ -924,7 +949,7 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
|
||||
// DRAG START DETECTION
|
||||
// The first call to this method indicates drag has started.
|
||||
// Initialize drag state, calculate invalid zones, and trigger pickup haptic.
|
||||
// Initialize drag state, calculate valid/invalid zones, and trigger pickup haptic.
|
||||
if draggingItem == nil {
|
||||
beginDrag(at: sourceIndexPath)
|
||||
}
|
||||
@@ -941,75 +966,10 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
checkZoneTransition(at: proposedRow)
|
||||
|
||||
switch item {
|
||||
case .travel(let segment, _):
|
||||
// TRAVEL CONSTRAINT LOGIC
|
||||
// Travel can only be on certain days (must finish games in departure city,
|
||||
// must arrive by game time in destination city)
|
||||
|
||||
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||
|
||||
guard let validRange = travelValidRanges[travelId] else {
|
||||
print("No valid range for travel: \(travelId)")
|
||||
return proposedDestinationIndexPath
|
||||
}
|
||||
|
||||
// Figure out which day the user is trying to drop onto
|
||||
var proposedDay = dayForTravelAtProposed(row: proposedRow, excluding: sourceIndexPath.row)
|
||||
|
||||
// Clamp to valid range - this is what creates the "snap" effect
|
||||
if proposedDay < validRange.lowerBound {
|
||||
proposedDay = validRange.lowerBound
|
||||
} else if proposedDay > validRange.upperBound {
|
||||
proposedDay = validRange.upperBound
|
||||
}
|
||||
|
||||
// Convert day number back to row position (right before that day's header)
|
||||
if let headerRow = dayHeaderRow(forDay: proposedDay) {
|
||||
var targetRow = headerRow
|
||||
// Account for the fact that the source row will be removed first
|
||||
// This is a UITableView quirk - the destination index is calculated
|
||||
// as if the source has already been removed
|
||||
if sourceIndexPath.row < headerRow {
|
||||
targetRow -= 1
|
||||
}
|
||||
return IndexPath(row: max(0, targetRow), section: 0)
|
||||
}
|
||||
|
||||
return proposedDestinationIndexPath
|
||||
|
||||
case .customItem(let customItem):
|
||||
// CUSTOM ITEM CONSTRAINT LOGIC
|
||||
// Custom items are flexible - they can go anywhere within the itinerary,
|
||||
// but we prevent dropping in places that would be confusing
|
||||
|
||||
// Use ItineraryConstraints to validate position
|
||||
let proposedDay = dayNumber(forRow: proposedRow)
|
||||
let proposedSortOrder = calculateSortOrder(at: proposedRow)
|
||||
|
||||
if let constraints = constraints {
|
||||
if !constraints.isValidPosition(for: customItem, day: proposedDay, sortOrder: proposedSortOrder) {
|
||||
// If position is invalid, try to find a valid nearby position
|
||||
// For now, just prevent dropping on day headers
|
||||
}
|
||||
}
|
||||
|
||||
// Don't drop ON a day header - go after it instead
|
||||
if proposedRow < flatItems.count, case .dayHeader = flatItems[proposedRow] {
|
||||
return IndexPath(row: proposedRow + 1, section: 0)
|
||||
}
|
||||
|
||||
// Don't drop before travel at the start of a day
|
||||
// (would visually appear before the travel card, which is confusing)
|
||||
if proposedRow < flatItems.count, case .travel = flatItems[proposedRow] {
|
||||
// Find the day header after this travel and drop after the header
|
||||
for i in proposedRow..<flatItems.count {
|
||||
if case .dayHeader = flatItems[i] {
|
||||
return IndexPath(row: i + 1, section: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return IndexPath(row: proposedRow, section: 0)
|
||||
case .travel, .customItem:
|
||||
// UNIFIED CONSTRAINT LOGIC using pre-calculated validDropRows
|
||||
// This eliminates bouncing by using a simple lookup instead of recalculating
|
||||
return snapToValidRow(proposedRow)
|
||||
|
||||
default:
|
||||
// Fixed items (shouldn't reach here since canMoveRowAt returns false)
|
||||
@@ -1017,6 +977,55 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Snaps a proposed row to the nearest valid drop position.
|
||||
///
|
||||
/// Uses pre-calculated `validDropRows` for O(log n) lookup.
|
||||
/// If the proposed row is already valid, returns it immediately (prevents bouncing).
|
||||
/// Otherwise, finds the nearest valid row using binary search.
|
||||
private func snapToValidRow(_ proposedRow: Int) -> IndexPath {
|
||||
// Fast path: if proposed row is valid, return it immediately
|
||||
// This is the key to preventing bouncing - no recalculation needed
|
||||
if validDropRows.contains(proposedRow) {
|
||||
return IndexPath(row: proposedRow, section: 0)
|
||||
}
|
||||
|
||||
// Proposed row is invalid - find the nearest valid row
|
||||
guard !validDropRows.isEmpty else {
|
||||
return IndexPath(row: proposedRow, section: 0)
|
||||
}
|
||||
|
||||
// Binary search for insertion point
|
||||
var low = 0
|
||||
var high = validDropRows.count
|
||||
|
||||
while low < high {
|
||||
let mid = (low + high) / 2
|
||||
if validDropRows[mid] < proposedRow {
|
||||
low = mid + 1
|
||||
} else {
|
||||
high = mid
|
||||
}
|
||||
}
|
||||
|
||||
// low is now the insertion point - check neighbors to find nearest
|
||||
let before = low > 0 ? validDropRows[low - 1] : nil
|
||||
let after = low < validDropRows.count ? validDropRows[low] : nil
|
||||
|
||||
let nearest: Int
|
||||
if let b = before, let a = after {
|
||||
// Both neighbors exist - pick the closer one
|
||||
nearest = (proposedRow - b) <= (a - proposedRow) ? b : a
|
||||
} else if let b = before {
|
||||
nearest = b
|
||||
} else if let a = after {
|
||||
nearest = a
|
||||
} else {
|
||||
nearest = proposedRow // Fallback (shouldn't happen)
|
||||
}
|
||||
|
||||
return IndexPath(row: nearest, section: 0)
|
||||
}
|
||||
|
||||
/// Calculates which day a travel segment would belong to if dropped at a proposed position.
|
||||
///
|
||||
/// Similar to `dayForTravelAt`, but used during the drag (before the move completes).
|
||||
|
||||
Reference in New Issue
Block a user