P5.1: drag → adjacent-swap intent mapping (pure sx)
Add swipe.sx: a pure swipe_intent(layout, start, end) -> ?Swap that turns a touch drag into an optional adjacent-swap intent (A, B). A is the cell under the drag start (via BoardLayout.point_to_cell); B is its orthogonal neighbour along the drag's dominant axis (larger of |dx|,|dy|, ties horizontal). Returns null for sub-threshold drags (a tap, threshold = cell_size * 0.5), starts off the board, or neighbours off the board. No rendering, no model mutation. Lock the logic with tests/swipe_intent.sx (+ expected golden), feeding synthetic down/up positions for right/left/up/down, sub-threshold tap, diagonal→dominant (both axes), edge-outward off-board, and start-off-board. bash tools/run_tests.sh passes (13/13); sx build --target ios-sim main.sx compiles.
This commit is contained in:
52
swipe.sx
Normal file
52
swipe.sx
Normal file
@@ -0,0 +1,52 @@
|
||||
// Pure drag → adjacent-swap intent mapping (Phase 5 input). Turns a touch drag —
|
||||
// a down position and an up/move position, both in the same view-local coordinate
|
||||
// space BoardLayout uses — into an optional swap intent (A, B): A is the cell
|
||||
// under the drag start, B its orthogonal neighbour along the drag's dominant axis.
|
||||
// Owns no rendering and mutates no model, so it is unit-testable headless; P5.2
|
||||
// feeds the resulting Swap straight into `commit_swap`.
|
||||
#import "modules/std.sx";
|
||||
#import "modules/math";
|
||||
#import "modules/ui/types.sx";
|
||||
#import "board.sx";
|
||||
#import "board_layout.sx";
|
||||
|
||||
// A drag whose dominant-axis travel is below this fraction of a cell is treated
|
||||
// as a tap, not a swipe, and yields no intent. Scaling to cell_size keeps the
|
||||
// feel constant across screen sizes, since the layout sizes cells to the device.
|
||||
SWIPE_THRESHOLD_FRACTION :f32: 0.5;
|
||||
|
||||
// Map a drag to the adjacent-swap intent it expresses, or null when the gesture
|
||||
// is not a board swipe. Returns null when: the start point is off the board; the
|
||||
// dominant-axis travel is below the swipe threshold (a tap, not a swipe); or the
|
||||
// resolved neighbour B would fall off the board. A and B are always orthogonally
|
||||
// adjacent — the intent never spans more than one cell. The dominant axis is the
|
||||
// larger of |dx|, |dy| (an exact tie resolves horizontal), and its sign picks the
|
||||
// direction: +x → right, -x → left, +y → down, -y → up (screen y grows downward,
|
||||
// matching `cell_frame`). Reuses `point_to_cell` so the start resolves to exactly
|
||||
// the cell drawn under the finger.
|
||||
swipe_intent :: (layout: *BoardLayout, start: Point, end: Point) -> ?Swap {
|
||||
if a := layout.point_to_cell(start) {
|
||||
dx := end.x - start.x;
|
||||
dy := end.y - start.y;
|
||||
adx := abs(dx);
|
||||
ady := abs(dy);
|
||||
|
||||
threshold := layout.cell_size * SWIPE_THRESHOLD_FRACTION;
|
||||
if adx < threshold and ady < threshold { return null; } // a tap, not a swipe
|
||||
|
||||
bcol := a.col;
|
||||
brow := a.row;
|
||||
if adx >= ady {
|
||||
if dx > 0.0 { bcol += 1; } else { bcol -= 1; }
|
||||
} else {
|
||||
if dy > 0.0 { brow += 1; } else { brow -= 1; }
|
||||
}
|
||||
|
||||
if bcol < 0 or bcol >= BOARD_COLS or brow < 0 or brow >= BOARD_ROWS {
|
||||
return null; // neighbour off the board
|
||||
}
|
||||
|
||||
return Swap.{ a = a, b = Cell.{ col = bcol, row = brow } };
|
||||
}
|
||||
null // start off the board
|
||||
}
|
||||
1
tests/expected/swipe_intent.exit
Normal file
1
tests/expected/swipe_intent.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
12
tests/expected/swipe_intent.stdout
Normal file
12
tests/expected/swipe_intent.stdout
Normal file
@@ -0,0 +1,12 @@
|
||||
grid origin (100,0) cell 75 threshold 37
|
||||
right: (3,5)->(4,5)
|
||||
left: (3,5)->(2,5)
|
||||
up: (3,5)->(3,4)
|
||||
down: (3,5)->(3,6)
|
||||
short: none
|
||||
diag-horizontal: (3,5)->(4,5)
|
||||
diag-vertical: (3,5)->(3,4)
|
||||
offboard-right: none
|
||||
offboard-up: none
|
||||
start-offboard: none
|
||||
ok: swipe-intent mapping resolves all cases
|
||||
82
tests/swipe_intent.sx
Normal file
82
tests/swipe_intent.sx
Normal file
@@ -0,0 +1,82 @@
|
||||
// Swipe-intent golden (P5.1): lock the pure drag → adjacent-swap mapping
|
||||
// `swipe_intent`. Feeds SYNTHETIC down/up screen positions (built from the SAME
|
||||
// BoardLayout the renderer uses) and asserts the resolved swap intent (A, B), so
|
||||
// a drift in axis/direction/threshold logic changes both the dump and the exit
|
||||
// code. No rendering, no model mutation — links headless like tests/hit_test.sx.
|
||||
//
|
||||
// Layout: 800×600, no safe inset → 600px square grid, cell 75, origin (100, 0);
|
||||
// swipe threshold = cell_size * 0.5 = 37.5px on the dominant axis. A 60px drag
|
||||
// clears it; a 10px drag does not. Failure is signalled via a non-zero exit code
|
||||
// (the runner checks exit code AND stdout).
|
||||
#import "modules/std.sx";
|
||||
#import "board.sx";
|
||||
#import "board_layout.sx";
|
||||
#import "swipe.sx";
|
||||
|
||||
cell_center :: (lay: *BoardLayout, col: s64, row: s64) -> Point {
|
||||
cf := lay.cell_frame(col, row);
|
||||
Point.{ x = cf.mid_x(), y = cf.mid_y() }
|
||||
}
|
||||
|
||||
// Print the resolved intent (locked in the golden) and report whether it matches
|
||||
// the expected adjacent pair (A, B). Drives the exit code alongside the dump.
|
||||
expect_swap :: (label: string, got: ?Swap, ac: s64, ar: s64, bc: s64, br: s64) -> bool {
|
||||
if s := got {
|
||||
print("{}: ({},{})->({},{})\n", label, s.a.col, s.a.row, s.b.col, s.b.row);
|
||||
return s.a.col == ac and s.a.row == ar and s.b.col == bc and s.b.row == br;
|
||||
}
|
||||
print("{}: none\n", label);
|
||||
false
|
||||
}
|
||||
|
||||
expect_none :: (label: string, got: ?Swap) -> bool {
|
||||
if s := got {
|
||||
print("{}: ({},{})->({},{})\n", label, s.a.col, s.a.row, s.b.col, s.b.row);
|
||||
return false;
|
||||
}
|
||||
print("{}: none\n", label);
|
||||
true
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
lay : BoardLayout = ---;
|
||||
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
|
||||
print("grid origin ({},{}) cell {} threshold {}\n",
|
||||
cast(s64) lay.origin.x, cast(s64) lay.origin.y, cast(s64) lay.cell_size,
|
||||
cast(s64) (lay.cell_size * SWIPE_THRESHOLD_FRACTION));
|
||||
|
||||
fails : s64 = 0;
|
||||
|
||||
// A known interior cell; every cardinal swipe from it stays on the board.
|
||||
start := cell_center(@lay, 3, 5);
|
||||
D : f32 = 60.0; // dominant-axis travel, well above the 37.5 threshold
|
||||
|
||||
if !expect_swap("right", swipe_intent(@lay, start, Point.{ x = start.x + D, y = start.y }), 3, 5, 4, 5) { fails += 1; }
|
||||
if !expect_swap("left", swipe_intent(@lay, start, Point.{ x = start.x - D, y = start.y }), 3, 5, 2, 5) { fails += 1; }
|
||||
if !expect_swap("up", swipe_intent(@lay, start, Point.{ x = start.x, y = start.y - D }), 3, 5, 3, 4) { fails += 1; }
|
||||
if !expect_swap("down", swipe_intent(@lay, start, Point.{ x = start.x, y = start.y + D }), 3, 5, 3, 6) { fails += 1; }
|
||||
|
||||
// Below threshold on both axes → a tap, not a swipe.
|
||||
if !expect_none("short", swipe_intent(@lay, start, Point.{ x = start.x + 10.0, y = start.y + 5.0 })) { fails += 1; }
|
||||
|
||||
// Diagonal drags resolve to the dominant axis (larger of |dx|, |dy|).
|
||||
if !expect_swap("diag-horizontal", swipe_intent(@lay, start, Point.{ x = start.x + D, y = start.y + 25.0 }), 3, 5, 4, 5) { fails += 1; }
|
||||
if !expect_swap("diag-vertical", swipe_intent(@lay, start, Point.{ x = start.x + 20.0, y = start.y - D }), 3, 5, 3, 4) { fails += 1; }
|
||||
|
||||
// Edge cell swiped outward → neighbour off the board → no intent.
|
||||
edge_right := cell_center(@lay, 7, 0);
|
||||
if !expect_none("offboard-right", swipe_intent(@lay, edge_right, Point.{ x = edge_right.x + D, y = edge_right.y })) { fails += 1; }
|
||||
edge_top := cell_center(@lay, 2, 0);
|
||||
if !expect_none("offboard-up", swipe_intent(@lay, edge_top, Point.{ x = edge_top.x, y = edge_top.y - D })) { fails += 1; }
|
||||
|
||||
// Drag starting off the board (left of the grid) → no intent.
|
||||
off_start := Point.{ x = lay.origin.x - 5.0, y = lay.origin.y + 10.0 };
|
||||
if !expect_none("start-offboard", swipe_intent(@lay, off_start, Point.{ x = off_start.x + D, y = off_start.y })) { fails += 1; }
|
||||
|
||||
if fails == 0 {
|
||||
print("ok: swipe-intent mapping resolves all cases\n");
|
||||
return 0;
|
||||
}
|
||||
print("FAIL: {} swipe-intent checks failed\n", fails);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user