Build the drop-slot model and the drop commits — drag & drop, first half

The pathfinder's drag-reorder model, ported and generalized (DRAG-REORDER.md
travels with it, rewritten for lanes, the interior masonry, multi-drag,
cross-board sessions, the re-grounding trio, and the committed-overlay hold):

- DropSlotMath — resting-layout zones from analytic lane arithmetic and the
  pure masonry placement (MasonryLayout now lays out through the same
  MasonryPlacement the drag reads, so geometry cannot drift), span-capped
  triggers sized to the dragged run's future footprint, hysteresis holds with
  the fresh-entry fallback, boundary ties, own-slot no-ops; nil means hold.
- DragAutoScrollMath — the activation bands and velocity ramp, pure.
- The drop commits, one performWrite bracket each: moveCards/copyCards within
  a board (insertion ranks touch only the dragged cards; renumber fallback);
  receiveCards/receiveLanes/receiveRestoredCards on the destination store for
  cross-board copy and ⌘-move with the import-boundary remint, lane copies
  stripping tombstoned cards while moves carry them; restoreByDrag is now
  positional, writing order only when the drop names a new one.

Gestures, sessions, previews, and delegates are the second half.

773 unit tests (87 new since the keyboard grammar).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 20:10:24 -04:00
parent 4035ba7986
commit 21a5a6dbfd
14 changed files with 2641 additions and 55 deletions
+49
View File
@@ -61,6 +61,55 @@ enum Ranks: Sendable {
return midpoint(between: orders[index - 1], and: orders[index])
}
/// The `count` ranks a **contiguous run** takes when it lands at display position `index`
/// among `orders` `insertionRank(amongVisible:at:)` for a multi-drag, whose whole set inserts
/// at one spot in preserved order (04-interactions.md Drag and drop, DRAG-REORDER.md §
/// Multi-drag).
///
/// `orders` is the visible siblings **in display order with the run itself already excluded**
/// the resting layout's convention, the same one the geometry's index is counted in.
///
/// The three cases mirror the single-rank twin, spread over `count` values:
///
/// - at or before the head `count` whole gaps *below* the first sibling, ascending;
/// - at or past the end (an empty `orders` included) `count` whole gaps above the last;
/// - between two siblings `count` evenly spaced points strictly inside their interval.
///
/// **`nil` means the gap is exhausted, not that the insertion is illegal** the interior case
/// fails when the two neighbours are close enough that `count` distinct, strictly increasing
/// `Double`s do not fit between them (adjacent doubles, or the duplicate-order tie). That is
/// the renumber trigger (01-storage-format.md § Ordering) and the caller's cue to compact and
/// ask again, exactly as an exhausted midpoint is everywhere else.
static func insertionRanks(amongVisible orders: [Double], at index: Int, count: Int) -> [Double]? {
guard count > 0 else { return [] }
if orders.isEmpty || index >= orders.count {
let base = orders.max() ?? 0
return (1...count).map { base + gap * Double($0) }
}
if index <= 0 {
let base = orders.min() ?? 0
// Ascending, and every value below `base`: the deepest is `count` gaps down.
return (1...count).map { base - gap * Double(count - $0 + 1) }
}
let lower = orders[index - 1]
let upper = orders[index]
guard lower < upper else { return nil }
let step = (upper - lower) / Double(count + 1)
var ranks: [Double] = []
var previous = lower
for position in 1...count {
let rank = lower + step * Double(position)
// Every rank must sit strictly inside the interval *and* strictly above the last one:
// at the precision floor the arithmetic silently collapses onto a neighbour, and a
// duplicate rank would hand display order to the folder-name tie-break.
guard rank > previous, rank < upper else { return nil }
ranks.append(rank)
previous = rank
}
return ranks
}
/// `count` fresh ranks, whole multiples of 1024 in ascending order
/// (1024, 2048, ) the renumber target when midpoint precision is
/// exhausted. Deterministic by construction; the writer applies these,