Extract + unit-test the padding-aware cell geometry (Codex nice-to-have)
Pulled the pointer→cell math out of cellAt() into a pure cellFromPoint(rect, x, y, n) in $lib/wordsearch.js (only getBoundingClientRect stays in the component), and covered it with vitest — including the last-column case that was drifting under the old overflowing layout, plus clamping and a scrolled-origin rect. 11 vitest tests now; real-device testing remains the final validator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getJSON } from '$lib/api.js';
|
import { getJSON } from '$lib/api.js';
|
||||||
import { lineFrom, matchWord } from '$lib/wordsearch.js';
|
import { lineFrom, matchWord, cellFromPoint } from '$lib/wordsearch.js';
|
||||||
|
|
||||||
let { size = 'med', onstatus } = $props();
|
let { size = 'med', onstatus } = $props();
|
||||||
|
|
||||||
@@ -86,12 +86,7 @@
|
|||||||
function summary() { return { game: 'wordsearch', size, date, status, found: foundWords.length, total: words.length, ms: resultMs }; }
|
function summary() { return { game: 'wordsearch', size, date, status, found: foundWords.length, total: words.length, ms: resultMs }; }
|
||||||
|
|
||||||
function cellAt(e) {
|
function cellAt(e) {
|
||||||
const rect = gridEl.getBoundingClientRect();
|
return cellFromPoint(gridEl.getBoundingClientRect(), e.clientX, e.clientY, n);
|
||||||
const pad = 7; // grid padding (6) + border (1)
|
|
||||||
const cw = (rect.width - 2 * pad) / n; // even 1fr columns share the inner width
|
|
||||||
const c = Math.min(n - 1, Math.max(0, Math.floor((e.clientX - rect.left - pad) / cw)));
|
|
||||||
const r = Math.min(n - 1, Math.max(0, Math.floor((e.clientY - rect.top - pad) / cw)));
|
|
||||||
return [r, c];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function down(e) {
|
function down(e) {
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ export function lineFrom(start, end, size) {
|
|||||||
return cells.length ? cells : [start];
|
return cells.length ? cells : [start];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a pointer position to a grid cell. Pure geometry (the DOM rect is passed
|
||||||
|
* in) so it's testable: `rect` is {left, top, width}, cells share the inner width
|
||||||
|
* (grid width minus padding/border) evenly, and the result is clamped to range.
|
||||||
|
* @returns {[number,number]} [row, col]
|
||||||
|
*/
|
||||||
|
export function cellFromPoint(rect, clientX, clientY, n, pad = 7) {
|
||||||
|
const cw = (rect.width - 2 * pad) / n;
|
||||||
|
const c = Math.min(n - 1, Math.max(0, Math.floor((clientX - rect.left - pad) / cw)));
|
||||||
|
const r = Math.min(n - 1, Math.max(0, Math.floor((clientY - rect.top - pad) / cw)));
|
||||||
|
return [r, c];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the letters along `cells` off `grid` (array of row strings) and return
|
* Read the letters along `cells` off `grid` (array of row strings) and return
|
||||||
* the matching not-yet-found word (forward or reversed), or null. Re-selecting
|
* the matching not-yet-found word (forward or reversed), or null. Re-selecting
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { lineFrom, matchWord } from './wordsearch.js';
|
import { lineFrom, matchWord, cellFromPoint } from './wordsearch.js';
|
||||||
|
|
||||||
const colinear = (cells) => {
|
const colinear = (cells) => {
|
||||||
if (cells.length < 2) return true;
|
if (cells.length < 2) return true;
|
||||||
@@ -30,6 +30,32 @@ describe('lineFrom', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cellFromPoint', () => {
|
||||||
|
// pad=7; inner = 462-14 = 448; with n=14 → cw = 32px per column.
|
||||||
|
const rect = { left: 0, top: 0, width: 462 };
|
||||||
|
const at = (x, y) => cellFromPoint(rect, x, y, 14);
|
||||||
|
const colCentre = (col) => 7 + col * 32 + 16; // pad + col*cw + half a cell
|
||||||
|
|
||||||
|
it('maps a cell centre to that cell', () => {
|
||||||
|
expect(at(colCentre(0), colCentre(0))).toEqual([0, 0]);
|
||||||
|
expect(at(colCentre(7), colCentre(3))).toEqual([3, 7]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps the LAST column correctly (the overflow regression)', () => {
|
||||||
|
expect(at(colCentre(13), colCentre(13))).toEqual([13, 13]); // not 12, not off-grid
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps points outside the grid to the nearest cell', () => {
|
||||||
|
expect(at(-50, -50)).toEqual([0, 0]);
|
||||||
|
expect(at(9999, 9999)).toEqual([13, 13]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects a non-zero rect origin (scrolled page)', () => {
|
||||||
|
const r2 = { left: 100, top: 200, width: 462 };
|
||||||
|
expect(cellFromPoint(r2, 100 + colCentre(5), 200 + colCentre(2), 14)).toEqual([2, 5]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('matchWord', () => {
|
describe('matchWord', () => {
|
||||||
const grid = ['CALM', 'XXXX', 'XXXX', 'XXXX']; // CALM across row 0
|
const grid = ['CALM', 'XXXX', 'XXXX', 'XXXX']; // CALM across row 0
|
||||||
const words = ['CALM', 'PEACE'];
|
const words = ['CALM', 'PEACE'];
|
||||||
|
|||||||
Reference in New Issue
Block a user