37 lines
1.3 KiB
TypeScript
37 lines
1.3 KiB
TypeScript
import { toScreen, type Camera, type Viewport } from "./camera";
|
|
import type { MapPoint } from "./projection";
|
|
import type { Scene } from "./scene";
|
|
|
|
export type MapSelection =
|
|
{ kind: "location"; id: string } | { kind: "flight"; dispatchId: string };
|
|
|
|
/** How close (screen pixels) a tap must be to a location or ship: about a fingertip. */
|
|
export const TAP_RADIUS = 22;
|
|
|
|
/**
|
|
* What a tap at `tap` (screen pixels) selects: the nearest ship marker or
|
|
* location within reach. Ships win ties, since they are drawn on top.
|
|
*/
|
|
export function hitTest(
|
|
scene: Scene,
|
|
tap: MapPoint,
|
|
viewport: Viewport,
|
|
camera: Camera,
|
|
): MapSelection | null {
|
|
let best: { selection: MapSelection; distance: number } | null = null;
|
|
const consider = (point: MapPoint, selection: MapSelection, bias: number) => {
|
|
const p = toScreen(point, viewport, camera);
|
|
const distance = Math.hypot(p.x - tap.x, p.y - tap.y) - bias;
|
|
if (distance <= TAP_RADIUS && (!best || distance < best.distance)) {
|
|
best = { selection, distance };
|
|
}
|
|
};
|
|
for (const location of scene.locations) {
|
|
consider(location.point, { kind: "location", id: location.id }, 0);
|
|
}
|
|
for (const flight of scene.flights) {
|
|
consider(flight.marker, { kind: "flight", dispatchId: flight.dispatchId }, 4);
|
|
}
|
|
return (best as { selection: MapSelection } | null)?.selection ?? null;
|
|
}
|