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:
@@ -165,6 +165,53 @@ struct RanksTests {
|
||||
#expect(Ranks.insertionRank(amongVisible: [1024, 1024], at: 2) == 2048)
|
||||
}
|
||||
|
||||
// MARK: - A contiguous run's ranks (multi-drag)
|
||||
|
||||
@Test func insertionRanksPlaceARunAtOneSpotInOrder() throws {
|
||||
let orders = [1024.0, 2048.0, 3072.0]
|
||||
|
||||
// Between two siblings: `count` evenly spaced points, strictly inside and ascending.
|
||||
let interior = try #require(Ranks.insertionRanks(amongVisible: orders, at: 1, count: 3))
|
||||
#expect(interior == [1280, 1536, 1792])
|
||||
#expect(interior.first! > orders[0] && interior.last! < orders[1])
|
||||
#expect(zip(interior, interior.dropFirst()).allSatisfy { $0 < $1 })
|
||||
|
||||
// The ends spread whole gaps, ascending, so the run lands as a block.
|
||||
#expect(Ranks.insertionRanks(amongVisible: orders, at: 3, count: 2) == [4096, 5120])
|
||||
#expect(Ranks.insertionRanks(amongVisible: orders, at: 0, count: 2) == [-1024, 0])
|
||||
}
|
||||
|
||||
@Test func insertionRanksAgreeWithTheSingleRankTwinForOneItem() {
|
||||
let orders = [1024.0, 2048.0, 3072.0]
|
||||
for index in -1...4 {
|
||||
#expect(Ranks.insertionRanks(amongVisible: orders, at: index, count: 1)
|
||||
== Ranks.insertionRank(amongVisible: orders, at: index).map { [$0] },
|
||||
"position \(index)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func insertionRanksAreTotalOnEdgeInputs() {
|
||||
#expect(Ranks.insertionRanks(amongVisible: [], at: 0, count: 3) == [1024, 2048, 3072])
|
||||
#expect(Ranks.insertionRanks(amongVisible: [1024], at: 99, count: 2) == [2048, 3072])
|
||||
#expect(Ranks.insertionRanks(amongVisible: [1024], at: -3, count: 2) == [-1024, 0])
|
||||
// A run of nothing is nothing, not a failure: a drag emptied by a foreign reload cancels
|
||||
// itself, and the write it would have made is simply empty.
|
||||
#expect(Ranks.insertionRanks(amongVisible: [1024], at: 0, count: 0) == [])
|
||||
}
|
||||
|
||||
@Test func insertionRanksReportAnExhaustedGapRatherThanInventingOne() {
|
||||
// The duplicate-order tie and adjacent Doubles are both renumber triggers, exactly as for
|
||||
// the single-rank twin — and a gap that fits one rank need not fit three.
|
||||
#expect(Ranks.insertionRanks(amongVisible: [1024, 1024], at: 1, count: 2) == nil)
|
||||
#expect(Ranks.insertionRanks(amongVisible: [1024, 1024.0000000000002], at: 1, count: 1) == nil)
|
||||
let tight = [1.0, 1.0.nextUp.nextUp]
|
||||
#expect(Ranks.insertionRanks(amongVisible: tight, at: 1, count: 1) != nil)
|
||||
#expect(Ranks.insertionRanks(amongVisible: tight, at: 1, count: 4) == nil)
|
||||
|
||||
// The ends never exhaust.
|
||||
#expect(Ranks.insertionRanks(amongVisible: [1024, 1024], at: 2, count: 2) == [2048, 3072])
|
||||
}
|
||||
|
||||
// MARK: - Precision exhaustion → renumber, deterministically
|
||||
|
||||
@Test func precisionExhaustionThenRenumberIsDeterministic() {
|
||||
|
||||
Reference in New Issue
Block a user