kanban-dev-webapp/src/lib/ordering/fractional.ts

61 lines
No EOL
2 KiB
TypeScript

/**
* Fractional index arithmetic for card ordering.
*
* A card's `position` is a float. Inserting between two neighbours takes
* their midpoint, so a drag writes a single row instead of renumbering the
* whole column. The tradeoff is finite precision: each midpoint insertion
* into the same slot halves the gap, and around the 50th the gap reaches
* the limit of a float64 and two cards silently collide. `needsRebalance`
* detects that approach; `rebalance` renumbers the column before it happens.
*/
/** Distance between consecutive cards in a freshly numbered column. */
export const SPACING = 1000;
/**
* Smallest adjacent gap tolerated before a column is renumbered. Well above
* float64 epsilon at these magnitudes, so rebalancing happens with room to
* spare rather than at the moment of failure.
*/
export const MIN_GAP = 1e-6;
/**
* The position for a card dropped between `before` and `after`.
* Pass null for either side to mean "no neighbour there".
*/
export function positionBetween(
before: number | null,
after: number | null
): number {
if (before === null && after === null) return SPACING;
if (before === null) return after! - SPACING;
if (after === null) return before + SPACING;
if (before >= after) {
throw new Error(
`positionBetween: neighbours out of order (before=${before}, after=${after})`
);
}
return (before + after) / 2;
}
/** True when any adjacent pair has drifted too close to stay distinguishable. */
export function needsRebalance(positions: number[]): boolean {
const sorted = [...positions].sort((a, b) => a - b);
for (let i = 1; i < sorted.length; i++) {
if (sorted[i] - sorted[i - 1] < MIN_GAP) return true;
}
return false;
}
/**
* Evenly spaced positions for a column, given its card ids in display order.
* The caller writes these back; order is preserved exactly.
*/
export function rebalance(
orderedIds: string[]
): Array<{ id: string; position: number }> {
return orderedIds.map((id, index) => ({
id,
position: (index + 1) * SPACING,
}));
}